api-baas-neon-945285 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-baas-neon-945285 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Use@neondatabase/serverlessfor edge/serverless database access. Prefer theneon()HTTP function for single queries (faster, stateless) andPool/Clientfor interactive transactions. Use pooled connection strings (-poolersuffix) for serverless workloads, direct connections only for migrations. Branch your database for dev/preview environments using copy-on-write semantics. Always handle cold starts from scale-to-zero (200-500ms wake-up).
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)(You MUST use the `neon()` HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)
(You MUST close `Pool`/`Client` connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)
(You MUST use pooled connection strings (`-pooler` suffix) for serverless workloads -- direct connections exhaust the limited connection slots)
(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)
(You MUST use `sql.unsafe()` only for trusted, known-safe strings like table/column names -- never for user input)
</critical_requirements>
Auto-detection: Neon, @neondatabase/serverless, neon(), neonConfig, neon serverless driver, neon database, neon branch, neonctl, neon connection pooling, neon scale-to-zero, neon autoscaling, neon postgres, ep-\*-pooler
When to use:
Key patterns covered:
neon() HTTP queries with SQL tagged templates and composable fragmentsPool/Client WebSocket connections with proper lifecycle management-pooler) vs direct connection strings and when to use eachsql.transaction() for non-interactive HTTP transactionsWhen NOT to use:
pg driver with TCP)Detailed Resources:
Driver & Queries:
Branching & Operations:
<philosophy>
Neon separates storage and compute for PostgreSQL, enabling serverless features impossible with traditional Postgres: scale-to-zero, instant branching, and autoscaling. The @neondatabase/serverless driver replaces TCP with HTTP and WebSockets, making Postgres accessible from edge runtimes that lack TCP support.
Core principles:
neon() function uses HTTP fetch (~3 round trips) for single queries. Pool/Client use WebSockets (~8 round trips) when you need sessions or interactive transactions. Pick the right transport for the job.neon() as a regular function is a type error, preventing accidental injection.When to use Neon serverless driver:
When NOT to use:
pg over TCP)</philosophy>
<patterns>
neon()The neon() function creates an HTTP-based query function using SQL tagged templates. It is the fastest path for single, non-interactive queries.
import { neon } from "@neondatabase/serverless";
const DATABASE_URL = process.env.DATABASE_URL!;
const sql = neon(DATABASE_URL);
// Tagged template -- parameters are auto-parameterized (safe from injection)
const userId = "abc-123";
const posts =
await sql`SELECT id, title FROM posts WHERE author_id = ${userId}`;Why good: Tagged template auto-parameterizes values preventing SQL injection, HTTP is ~3 round trips vs ~8 for WebSocket, stateless (no connection to manage)
// BAD: Calling neon() as a regular function (v1.0+ type error)
const sql = neon(DATABASE_URL);
const result = await sql(`SELECT * FROM posts WHERE id = ${id}`); // TYPE ERROR + SQL injection riskWhy bad: Since v1.0, calling the query function as a regular function (not a tagged template) is a runtime and type error -- this was changed specifically to prevent SQL injection from string interpolation
#### sql.query() for Dynamic SQL
When the query string is in a variable (not a template literal), use the .query() method with numbered placeholders:
const q = "SELECT * FROM posts WHERE id = $1 AND status = $2";
const posts = await sql.query(q, [postId, "published"]);When to use: Dynamic SQL strings built at runtime, or queries stored in variables. Parameters are still safely parameterized via $1, $2, etc.
Template queries support composition with automatic parameter numbering across fragments.
import { neon } from "@neondatabase/serverless";
const sql = neon(DATABASE_URL);
// Build queries from reusable fragments
const whereClause = sql`WHERE status = ${"active"} AND role = ${"admin"}`;
const orderClause = sql`ORDER BY created_at DESC`;
const PAGE_SIZE = 20;
const limitClause = sql`LIMIT ${PAGE_SIZE}`;
const users =
await sql`SELECT id, name, email FROM users ${whereClause} ${orderClause} ${limitClause}`;Why good: Parameters are renumbered automatically across composed fragments, named constant for page size, fragments are reusable across queries
#### Dynamic Table/Column Names with sql.unsafe()
// ONLY for trusted, known-safe values -- never user input
const TABLE_NAME = "posts";
const results =
await sql`SELECT * FROM ${sql.unsafe(TABLE_NAME)} WHERE id = ${postId}`;When to use: Only when you need to interpolate trusted identifiers (table names, column names) that cannot be parameterized in SQL. Never pass user-supplied values to sql.unsafe().
sql.transaction()Execute multiple queries atomically via HTTP without needing a WebSocket connection.
import { neon } from "@neondatabase/serverless";
const sql = neon(DATABASE_URL);
// Array form -- all queries execute in a single HTTP round trip
const [posts, totalCount] = await sql.transaction([
sql`SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 10`,
sql`SELECT count(*) FROM posts`,
]);
// Function form with transaction options
const [, transferResult] = await sql.transaction(
(txn) => [
txn`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`,
txn`UPDATE accounts SET balance = balance + ${amount} WHERE id = ${toId}`,
],
{ isolationLevel: "Serializable" },
);Why good: Atomic execution without WebSocket overhead, array form is concise for read-only batches, function form supports transaction options, isolation level controls consistency guarantees
// BAD: Running related queries as separate HTTP requests
const posts = await sql`SELECT * FROM posts WHERE author_id = ${authorId}`;
const author = await sql`SELECT * FROM users WHERE id = ${authorId}`;
// Two separate HTTP round trips, not atomic, race conditions possibleWhy bad: Separate HTTP calls are not atomic, data can change between queries, double the network latency
#### Transaction Options
isolationLevel: ReadUncommitted | ReadCommitted | RepeatableRead | SerializablereadOnly: boolean (default false)deferrable: boolean (default false, only effective with readOnly: true + Serializable)Pool/ClientUse Pool and Client for interactive transactions or node-postgres API compatibility. In serverless environments, connections must be created and closed within a single request handler.
import { Pool } from "@neondatabase/serverless";
export async function handleRequest(request: Request): Promise<Response> {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
// ... queries ...
} finally {
await pool.end(); // ALWAYS close within the same request
}
}Key rule: Create Pool inside the handler, close with pool.end() in finally. A global Pool leaks WebSocket connections between serverless invocations.
See examples/core.md -- Patterns 5-7 for request-scoped pools, interactive transactions, and Node.js WebSocket configuration.
Neon provides two connection string formats: pooled (via PgBouncer) and direct.
# Pooled connection (note: -pooler suffix on endpoint ID)
# Use for: serverless functions, web apps, high-concurrency workloads
DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=require
# Direct connection (no -pooler suffix)
# Use for: migrations, pg_dump, LISTEN/NOTIFY, session-level features
DIRECT_DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=requireWhy good: Pooled handles up to 10,000 concurrent clients via PgBouncer in transaction mode, direct provides full session features for admin tasks, separate env vars make the distinction explicit
#### PgBouncer Transaction Mode Limitations
The pooled connection runs PgBouncer in transaction mode, which means connections return to the pool after each transaction. This prohibits:
SET / RESET statements (use ALTER ROLE ... SET instead)LISTEN / NOTIFYWITH HOLD CURSORPREPARE / DEALLOCATE (protocol-level prepared statements up to 1,000 are supported)PRESERVE / DELETE ROWSNeon computes auto-suspend after 5 minutes of inactivity (default). Cold starts take 200-500ms. Design for this with appropriate timeouts and retry logic.
const CONNECTION_TIMEOUT_MS = 10_000;
const sql = neon(process.env.DATABASE_URL!, {
fetchOptions: { signal: AbortSignal.timeout(CONNECTION_TIMEOUT_MS) },
});Key rule: Set explicit timeouts (10-15s) to accommodate cold start latency. Use exponential backoff retries for reliability.
See examples/core.md -- Pattern 8 for full timeout configuration and retry patterns.
#### Scale-to-Zero Facts
Neon branches use copy-on-write semantics -- branching a 500GB database is instant and allocates no extra storage until data diverges.
# Install neonctl
npm install -g neonctl
# Create a dev branch from main
neonctl branches create --name dev-alice --project-id <project-id>
# Create a branch with automatic expiration (for CI/preview)
neonctl branches create --name preview/pr-42 --project-id <project-id> --expires-at "2025-04-01T00:00:00Z"
# Create schema-only branch (no data copied -- for sensitive environments)
neonctl branches create --name ci-test --schema-only --project-id <project-id>
# Reset a dev branch to match current production
neonctl branches reset dev-alice --parent --project-id <project-id>
# Delete a branch
neonctl branches delete preview/pr-42 --project-id <project-id>Why good: Named branches map to git workflow, TTL expiration auto-cleans CI branches, schema-only branching protects sensitive data, reset syncs dev with production without recreating
#### Branch Connection Strings
Each branch gets its own endpoint. The branch connection string follows the same format but with a different endpoint ID:
# Main branch
postgresql://user:[email protected]/dbname
# Dev branch -- different endpoint ID
postgresql://user:[email protected]/dbnameThe Neon REST API (https://console.neon.tech/api/v2) enables programmatic branch management. Authenticate with Authorization: Bearer <NEON_API_KEY>. Key operations: create branches with TTL expiration, delete branches, list branches for cleanup scripts.
const NEON_API_BASE = "https://console.neon.tech/api/v2";
// POST /projects/{projectId}/branches -- create with { branch: { name, expires_at }, endpoints: [{ type: "read_write" }] }
// DELETE /projects/{projectId}/branches/{branchId} -- delete a branchSee examples/branching.md -- Pattern 3 for a full typed TypeScript branch manager with create, delete, and list operations. See reference.md for the complete API endpoint table.
</patterns>
<decision_framework>
neon()) vs WebSocket (Pool/Client)What kind of database operation?
+-- Single query (SELECT, INSERT, UPDATE, DELETE)
| +-- YES --> Use neon() HTTP function (fastest, ~3 round trips)
+-- Multiple queries that must be atomic?
| +-- Can all queries be determined upfront (non-interactive)?
| | +-- YES --> Use sql.transaction() over HTTP
| | +-- NO --> Use Pool/Client over WebSocket
+-- Need node-postgres (pg) API compatibility?
| +-- YES --> Use Pool/Client over WebSocket
+-- Running in edge runtime (no TCP)?
+-- YES --> Use @neondatabase/serverless (HTTP or WebSocket)
+-- NO --> Standard pg driver with TCP may be simplerWhat is the workload?
+-- Serverless function / edge function --> Pooled (-pooler)
+-- Web application (many concurrent requests) --> Pooled (-pooler)
+-- Schema migration --> Direct (needs session state)
+-- pg_dump / pg_restore --> Direct (uses SET statements)
+-- LISTEN / NOTIFY --> Direct (session-level feature)
+-- Long-running analytics query --> Direct (avoid pool contention)
+-- Default / unsure --> Pooled (-pooler)What do you need the branch for?
+-- Developer working on a feature --> Dev branch (long-lived, manually managed)
+-- PR preview environment --> Preview branch (TTL expiration, auto-cleanup on merge)
+-- CI test run --> Ephemeral branch (short TTL, schema-only if data-sensitive)
+-- Database recovery --> Restore from branch history (up to 30 days on Scale plan)
+-- Load testing --> Branch from production (copy-on-write, no storage cost until diverge)</decision_framework>
<red_flags>
High Priority Issues:
-pooler) connections.sql.unsafe() embeds raw SQL without parameterization. It exists only for trusted identifiers (table/column names). User input in sql.unsafe() is a SQL injection vulnerability.Medium Priority Issues:
pool.end() after using WebSocket connections exhausts available connections across invocations.ALTER ROLE ... SET for role-level defaults or use direct connections.Common Mistakes:
@neondatabase/serverless, not neon-serverless or pg-neon.Pool/Client, install ws and set neonConfig.webSocketConstructor = ws. Node.js v22+ has native WebSocket and needs no extra setup.sql("SELECT ...") is a type error since v1.0. Use ` sqlSELECT ... ` (tagged template).Gotchas & Edge Cases:
arrayMode: true on individual queries inside sql.transaction() is ignored. Set it on the transaction itself.neonctl branches create --schema-only or the REST API with "init_source": "schema-only". Schema-only branches require exactly one read-write compute endpoint.@neondatabase/serverless (v1.0+) requires Node.js 19 or higher.</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)(You MUST use the `neon()` HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)
(You MUST close `Pool`/`Client` connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)
(You MUST use pooled connection strings (`-pooler` suffix) for serverless workloads -- direct connections exhaust the limited connection slots)
(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)
(You MUST use `sql.unsafe()` only for trusted, known-safe strings like table/column names -- never for user input)
Failure to follow these rules will cause connection exhaustion, SQL injection vulnerabilities, or intermittent cold-start failures.
</critical_reminders>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.