npm.io
7.3.0 • Published 1 week ago

@neon/sdk

Licence
Apache-2.0
Version
7.3.0
Deps
0
Vulns
0
Weekly
0
Stars
59

@neon/sdk

The official TypeScript SDK for the Neon API — a modern, Fetch-based, zero-dependency, ESM-only client generated from Neon's OpenAPI specification. Successor to @neondatabase/api-client.

Two layers, one package:

  • createNeonClient — an ergonomic client (auth once, { data, error } results, typed errors, retries, readiness polling, auto-pagination, workflows), organized into resource namespaces.
  • raw — the full generated 1:1 surface: every endpoint as a standalone, tree-shakeable function. Also at the @neon/sdk/raw subpath.

This major release takes named parameter objects on ergonomic resource methods.


Install

npm install @neon/sdk

Requires Node.js ≥ 20.19 (or any runtime with a global fetch — Bun, Deno, edge, browser).

Quick start

import { createNeonClient } from "@neon/sdk";

const neon = createNeonClient({ apiKey: process.env.NEON_API_KEY! });

// create a project and get a ready-to-use connection string
const { data, error } = await neon.projects.createAndConnect({
  name: "my-app",
  pooled: true,
});
if (error) throw error;
const { project, connectionString } = data;

Client configuration

createNeonClient(config):

Option Type Default Description
apiKey string | (() => string | Promise<string>) — (required) Neon API key, or a function returning it (sync/async). Sent as a Bearer token. Missing or "" throws a "client"-kind error when the client is created, not a 401 on the first call.
throwOnError boolean false When true, methods return the resource directly and throw on error. When false, they return { data, error }. Narrows return types at the type level.
waitForReadiness boolean — (unset) Omit to use per-method defaults: projects.create, projects.createAndConnect, branches.create, and branches.createAndConnect poll until operations finish; other mutations do not. Set false to disable polling on those four. Set true to poll on every mutation that returns operations.
wait { pollIntervalMs?: number; timeoutMs?: number } 1000 / 300000 Tuning for the readiness poller.
retries number 2 Automatic retries on always-safe statuses (423, 429, 503) with backoff. Must be a non-negative integer (0 disables retries). NaN, Infinity, a fraction, or a negative throws a client-kind error at construction.
requestTimeoutMs number — (unbounded) Deadline for a request and its retries. Aborts the request and resolves with a NeonTimeoutError. Pass Infinity per call to opt out of a client-wide value. Separate from wait.timeoutMs.
orgId string — Default organization for project create/list and as the transfer source org. Client-wide — not a CallOptions key. Override via method input (org_id on list/create, fromOrgId on transfer).
baseUrl string https://console.neon.tech/api/v2 Override the API base URL.
fetch typeof fetch global fetch Custom fetch implementation (proxies, tests, non-global runtimes).

CallOptions — { throwOnError?, waitForReadiness?, requestTimeoutMs?, wait?, signal? } — is accepted per call as the last options argument, overriding the client default where one exists (signal is call-only). retries, orgId, baseUrl, and fetch are client-wide. Resource identifiers and request fields live together in the first named parameter object; paginated methods still take CallOptions second. pooled, confirmSelfDemotion, and confirmSelfLockout belong in that input object. Passing them on CallOptions is a client error, including when they ride along on a shared options variable.


Migrating from v4

// v4
await neon.branches.delete(projectId, branchId);
await neon.operations.waitFor(operations);
await neon.projects.createAndConnect({ name: "app" }, { pooled: false });

// v5
await neon.branches.delete({ projectId, branchId });
await neon.operations.waitFor({ operations });
await neon.projects.createAndConnect({ name: "app", pooled: false });

throwOnError, waitForReadiness, requestTimeoutMs, and wait stay on CallOptions and on createNeonClient. signal is CallOptions-only. Database rename uses databaseName for the current name and name for the replacement. Payload field spelling is unchanged. The raw client is unchanged.

The result model

By default every method resolves to a discriminated { data, error } envelope — no try/catch needed:

const { data, error } = await neon.projects.get({ projectId: "late-frost-12345" });
if (error) {
  // error is NeonErrorUnion — discriminate on error.kind
  return;
}
data; // narrowed to Project

Set throwOnError (on the client or per call) to get the bare resource and throw instead — and the return types narrow accordingly:

const neon = createNeonClient({ apiKey, throwOnError: true });
const project = await neon.projects.get({ projectId: "…" }); // Project (throws on error)
const res = await neon.projects.get(
  { projectId: "…" },
  { throwOnError: false },
); // { data, error }

Errors

The error channel carries a typed hierarchy (all Error subclasses with a kind discriminant); the same value is thrown when throwOnError is set.

Class kind Notable fields
NeonError (base) message, kind
NeonApiError "api" status, code, requestId, response, body
NeonNotFoundError "not_found" (404) — extends NeonApiError
NeonAuthError "auth" (401/403)
NeonRateLimitError "rate_limit" (429, after retries)
NeonOperationError "operation" operationId, status — an awaited operation failed
NeonTimeoutError "timeout" abstract base of the two rows below; instanceof still matches both
NeonRequestTimeoutError "timeout" source: "request", timeoutMs — requestTimeoutMs ran out
NeonWaitTimeoutError "timeout" source: "wait", timeoutMs, operations — readiness budget ran out; pass operations to neon.operations.waitFor
NeonAbortError "aborted" the caller's signal fired
NeonNetworkError "network" reason — transport failure (no response)
NeonClientError "client" SDK-side errors (e.g. ambiguous connection-string selection, invalid requestTimeoutMs)

The error channel is typed as NeonErrorUnion, the union of every row below the base.

const { error } = await neon.branches.get({ projectId: pid, branchId: "nope" });
if (error?.kind === "not_found") {
  error.status;    // 404
  error.requestId; // string | undefined
}
if (error?.kind === "network") {
  error.reason;    // "ECONNRESET"
}

Branch on kind rather than name or message. name is a stable string literal on every class, so it survives bundling, but message is not a contract.

A kind check narrows to the class, so subclass fields are visible without instanceof. instanceof still works — every class extends NeonError, and instanceof NeonApiError matches the 404/401/429 subclasses too.

With throwOnError, the thrown value is a NeonErrorUnion member at runtime, but catch (e) types it as unknown. instanceof NeonError only yields the base class (whose kind does not narrow). Use isNeonError:

import { isNeonError } from "@neon/sdk";

try {
  await neon.projects.get({ projectId: pid }, { throwOnError: true });
} catch (e: unknown) {
  if (!isNeonError(e)) throw e;
  if (e.kind === "not_found") e.status;   // number
  if (e.kind === "network")   e.reason;   // string
}

NeonNetworkError.reason carries the most specific reason the platform gave — an errno code such as ECONNRESET when one is available, otherwise the innermost non-empty message. It is also interpolated into message, so transport faults are distinguishable in logs and error trackers instead of collapsing onto one string:

import { NeonNetworkError } from "@neon/sdk";

const { error } = await neon.projects.get({ projectId: id });
if (error instanceof NeonNetworkError) {
  error.reason; // "ECONNRESET"
  error.message; // 'Network error: no response received from the Neon API (ECONNRESET).'
}

Validating ids and other path parameters before passing them in is the caller's responsibility. An empty path parameter builds a URL with an empty segment, which the Neon API answers with a redirect rather than a 400; that can surface as a "network" error rather than anything that names the argument.

Cancellation & deadlines

Pass a signal to cancel a call, or requestTimeoutMs to bound it. Both arrive on the error channel as typed errors — a cancelled call never rejects with a raw DOMException, and a throwOnError client throws the same NeonErrorUnion member it would have returned:

const controller = new AbortController();
const { error } = await neon.projects.list({}, { signal: controller.signal }).all();
if (error?.kind === "aborted") { /* the caller stopped it */ }

const result = await neon.projects.get(
  { projectId: id },
  { signal: controller.signal },
);
if (result.error?.kind === "aborted") { /* the caller stopped it */ }
const bounded = createNeonClient({ apiKey, requestTimeoutMs: 30_000 });
const slow = await bounded.projects.get(
  { projectId: id },
  { requestTimeoutMs: 5_000 },
);
if (slow.error?.kind === "timeout" && slow.error.source === "request") {
  /* the request deadline was exceeded */
}

await bounded.storage.objects.get(
  { projectId, branchId, bucketName: "bucket", objectKey: "big.tar" },
  { requestTimeoutMs: Number.POSITIVE_INFINITY },
);

"aborted" and "timeout" are deliberately distinct: a timeout is worth retrying, a cancellation is not. "timeout" still covers both budgets; source says which one fired:

const { data, error } = await neon.projects.create(
  { name: "app" },
  { wait: { timeoutMs: 30_000 } },
);

if (error?.kind === "timeout" && error.source === "wait") {
  // The project exists and is still provisioning. Poll again with a fresh budget
  // instead of calling create a second time.
  const resumed = await neon.operations.waitFor({ operations: error.operations }, {
    timeoutMs: 120_000,
  });
  if (resumed.error) throw resumed.error;
}

requestTimeoutMs must be a positive number of milliseconds up to 2147483647, or Infinity. Anything else — 0, a negative, NaN, or a value past that range — is rejected with a NeonClientError when the client is created or the call is made, rather than silently becoming an instant timeout or no timeout at all.

Cancellation reaches everything the SDK controls: the request and its retries, readiness polling, each page of a paginated walk, and the multi-request postgres.connectionString resolver. The one thing it cannot interrupt is your own code — a snapshots.restore preview callback receives the signal as its second argument so it can cooperate, and the restore is left un-finalized if the call is cancelled around it.

Calls are unbounded unless you set requestTimeoutMs, which is what they have always been. requestTimeoutMs covers one request and all of its retries; readiness polling keeps its own budget in wait.timeoutMs, so setting one does not silently cap the other.

retries interacts with it: a Retry-After is never shortened, because retrying earlier than the server asked is worse than not retrying. If honouring it would take longer than 10s or than the remaining deadline, the SDK stops retrying and hands you the real 423/429/503 instead of waiting or reporting a timeout that hides the status.

Pagination

Cursor-paginated list() methods return a lazy Paginated<T>:

const { data: all } = await neon.projects.list().all();      // every page → { data, error }
const { data: page } = await neon.projects.list().page();    // one page
for await (const project of neon.projects.list()) { … }      // stream; throws on a page error

// per-call options come after the query
const { data } = await neon.projects
  .list({ search: "prod" }, { signal, requestTimeoutMs: 10_000 })
  .all();

// throwOnError applies here too — client-wide or per call
const throwing = createNeonClient({ apiKey, throwOnError: true });
const projects = await throwing.projects.list().all(); // ProjectListItem[]; throws on failure
const { data: again } = await throwing.projects
  .list(undefined, { throwOnError: false })
  .all();

page() and all() follow throwOnError like every other method. The for await stream always throws on a page error.

A deadline covers one consumption — a whole all(), or a whole iteration — rather than each page, since that is the unit a caller waits on. A Paginated is lazy and reusable, so consuming it twice gets a fresh deadline each time.

Readiness & workflows

Neon mutations are asynchronous (they return operations). waitForReadiness blocks until they settle.

Omit the client option to use per-method defaults: projects.create, projects.createAndConnect, branches.create, branches.createAndConnect, and realtime.enable poll; other mutations (for example projects.update) do not. createNeonClient({ waitForReadiness: false }) disables polling on those five. createNeonClient({ waitForReadiness: true }) enables it on every mutation that returns operations, and on the neon.realtime mutations, which poll the Realtime state instead. Per-call { waitForReadiness } still wins. The connect workflows also hand back a connection string. Per-call wait overrides the client's poll interval and timeout for that call, field by field: { wait: { timeoutMs: 600_000 } } keeps the client's pollIntervalMs. The primitive is neon.operations.waitFor({ operations }).

const neon = createNeonClient({ apiKey });
await neon.projects.create({ name: "app" }); // polls (method default)
await neon.projects.update({ projectId: id, name: "renamed" }); // does not poll

const skipWait = createNeonClient({ apiKey, waitForReadiness: false });
await skipWait.projects.create({ name: "app" }); // returns before operations finish
await skipWait.projects.create({ name: "app" }, { waitForReadiness: true }); // polls anyway

const alwaysWait = createNeonClient({ apiKey, waitForReadiness: true });
await alwaysWait.projects.update({ projectId: id, name: "renamed" }); // polls

await neon.projects.create(
  { name: "app" },
  { wait: { timeoutMs: 600_000 } },
);

API reference

Legend: [P] returns Paginated<T> — page()/all() resolve to the resource (or { data, error }) per throwOnError · [W] workflow (multi-step) · →void resolves to void. Unless noted, methods take one named parameter object followed by an optional CallOptions argument and resolve to the resource (or { data, error }). Zero-input methods remain zero-input.

neon.projects
Method Returns Notes
list(query?) [P] ProjectListItem query: { search?, org_id?, limit? } (cursor managed for you)
get({ projectId }) Project
create(input?) Project API always provisions default-branch compute. No connection string. Readiness polling on by default. input: { name?, region_id?, pg_version?, org_id?, autoscaling_limit_min_cu?, autoscaling_limit_max_cu?, settings? }
createAndConnect({ …input, pooled? }, options?) [W] { project, connectionString } one call + readiness; pooled default true
update({ projectId, name?, settings? }) Project name is the new project name
delete({ projectId }) Project
recover({ projectId }) Project beta — recover a soft-deleted project
transfer({ fromOrgId?, toOrgId, projectIds }) →void fromOrgId defaults to client orgId
transferFromUser({ toOrgId, projectIds }) →void personal account → org
// Provision a project and get a pooled connection string in one call
const { data } = await neon.projects.createAndConnect(
  { name: "tenant-42", region_id: "aws-us-east-1", pooled: true },
);
// data: { project, connectionString }

// Upgrade path: move projects from the sponsored org to the paid org
await neon.projects.transfer({
  fromOrgId: sponsoredOrgId,   // defaults to the client's `orgId`
  toOrgId: paidOrgId,
  projectIds: ["late-frost-12345"],
});
neon.branches
Method Returns Notes
list({ projectId, …query }) [P] Branch query: { search?, sort_by?, sort_order?, include_deleted? }
get({ projectId, branchId }) Branch
create({ projectId, …input }) Branch RW compute on by default. noCompute: true skips the endpoint. No connection string. Readiness polling on by default. input: { name?, parent_id?, parent_lsn?, parent_timestamp?, protected?, compute?: { minCu?, maxCu?, suspendTimeoutSeconds? }, noCompute? }
update({ projectId, branchId, …input }) Branch input: { name?, protected?, expires_at? }
delete({ projectId, branchId }) →void
createAndConnect({ projectId, …input, pooled? }, options?) [W] { branch, endpoint, connectionString } input: { name?, parentId?, compute?: { minCu?, maxCu?, suspendTimeoutSeconds? } }; pooled default true
getDefault({ projectId }) Branch resolves the default branch by the default flag
setDefault({ projectId, branchId }) Branch
resetFromParent({ projectId, branchId, preserveUnderName? }, options?) Branch parent HEAD only; discards writes since the branch diverged. preserveUnderName is required when the branch has children. Pass { waitForReadiness: true } before using the branch
compareSchema({ projectId, branchId, …input }) { diff? } input: { databaseName, baseBranchId?, lsn?, timestamp?, baseLsn?, baseTimestamp? }. Omitting baseBranchId compares against the parent
finalizeRestore({ projectId, branchId, name? }) →void commits a restore previewed with snapshots.restore({ finalize: false })

There is no recover. Neon stopped publishing POST /projects/{project_id}/branches/{branch_id}/recover in its OpenAPI spec, so the wrapper is gone until it returns. The endpoint still answers, so a soft-deleted branch can still be recovered through the low-level client — note that this envelope carries the API's own error body rather than a NeonError:

import type { BranchRecoverResponse } from "@neon/sdk";

const { data } = await neon.client.post<{ 200: BranchRecoverResponse }>({
  url: "/projects/{project_id}/branches/{branch_id}/recover",
  path: { project_id: projectId, branch_id: branchId },
});
const branch = data?.branch;

neon.projects.recover is a different endpoint — it recovers a deleted project, not a branch.

// Resolve the project's default ("production") branch
const { data: prod } = await neon.branches.getDefault({ projectId });

await neon.branches.create({
  projectId,
  name: "preview/pr-123",
  parent_id: prod?.id,
});

await neon.branches.create({
  projectId,
  name: "schema-only",
  parent_id: prod?.id,
  noCompute: true,
});

// Branch off it with its own compute — returns a ready connection string
const { data } = await neon.branches.createAndConnect({
  projectId,
  name: "preview/pr-123",
  parentId: prod?.id,
  compute: { minCu: 0.25, maxCu: 2 },
});
// data: { branch, endpoint, connectionString }

const { data: schema } = await neon.branches.compareSchema(
  { projectId, branchId: data!.branch.id, databaseName: "neondb" },
);

await neon.branches.resetFromParent(
  { projectId, branchId: data!.branch.id },
  { waitForReadiness: true },
);
neon.postgres

The Postgres data plane of a branch. neon.postgres.connectionString(params, options?) resolves a URI, auto-selecting the default branch and the sole role/database when omitted:

const { data: uri } = await neon.postgres.connectionString({
  projectId,
  branchId?, endpointId?, databaseName?, roleName?, pooled?,  // all optional; pooled default true
});
neon.postgres.endpoints
Method Returns
list({ projectId }) Endpoint[]
listByBranch({ projectId, branchId }) Endpoint[]
get({ projectId, endpointId }) Endpoint
create({ projectId, …input }) Endpoint — input: { branch_id, type, autoscaling_limit_min_cu?, autoscaling_limit_max_cu?, suspend_timeout_seconds?, provisioner? }
update({ projectId, endpointId, …input }) Endpoint
delete({ projectId, endpointId }) →void
start / suspend / restart({ projectId, endpointId }) Endpoint
neon.postgres.roles
Method Returns
list({ projectId, branchId }) Role[]
get({ projectId, branchId, roleName }) Role
create({ projectId, branchId, name, no_login? }) Role
delete({ projectId, branchId, roleName }) →void
password({ projectId, branchId, roleName }) string (reveals the password)
resetPassword({ projectId, branchId, roleName }) Role (carries the new password)
// Reveal a role's password, or rotate it
const roleParams = { projectId, branchId, roleName: "neondb_owner" };
const { data: password } = await neon.postgres.roles.password(roleParams);
const { data: role } = await neon.postgres.roles.resetPassword(roleParams);
// role.password holds the new secret
neon.postgres.databases
Method Returns
list({ projectId, branchId }) Database[]
get({ projectId, branchId, databaseName }) Database
create({ projectId, branchId, name, owner_name }) Database
update({ projectId, branchId, databaseName, name?, owner_name? }) Database
delete({ projectId, branchId, databaseName }) →void
neon.postgres.dataApi
Method Returns
get({ projectId, branchId, databaseName }) DataApiReponse
create({ projectId, branchId, databaseName, …input }) DataApiCreateResponse
update({ projectId, branchId, databaseName, …input }) →void
delete({ projectId, branchId, databaseName }) →void
neon.storage

Branch-scoped object storage (beta). neon.storage.get returns whether storage is enabled and the branch S3 endpoint metadata; buckets and objects are nested underneath.

neon.storage (branch state)
Method Returns
get({ projectId, branchId }) BranchStorage
neon.storage.buckets
Method Returns
list({ projectId, branchId }) Bucket[]
create({ projectId, branchId, name, access_level? }) Bucket — access_level: "private" | "public_read"
delete({ projectId, branchId, bucketName }) →void
neon.storage.objects
Method Returns Notes
list({ projectId, branchId, bucketName, …query }) BucketObjectsListResponse query: { prefix?, delimiter?, cursor?, limit? } — one page (folders, objects, next_cursor)
get({ projectId, branchId, bucketName, objectKey }) Blob raw object bytes
delete({ projectId, branchId, bucketName, objectKey }) →void
deleteByPrefix({ projectId, branchId, bucketName, prefix }) { deleted: number } prefix must end with /
presign({ projectId, branchId, bucketName, objectKey, …input }) PresignResponse input: { operation: "upload" | "download", content_type?, expires_in_seconds? }
// Upload via presigned PUT (same flow as neon bucket object put)
const { data: presign } = await neon.storage.objects.presign(
  {
    projectId,
    branchId,
    bucketName: "avatars",
    objectKey: "user-1.png",
    operation: "upload",
    content_type: "image/png",
  },
);
if (!presign) throw new Error("presign failed");

await fetch(presign.url, {
  method: "PUT",
  headers: { ...presign.headers, "Content-Length": String(bytes.length) },
  body: bytes,
});
neon.functions

Branch-scoped Neon Functions (beta).

Method Returns Notes
list({ projectId, branchId, …query }) [P] NeonFunction query: { limit? }
get({ projectId, branchId, slug }) NeonFunction
update({ projectId, branchId, slug, …input }) NeonFunction input: { name? }
delete({ projectId, branchId, slug }) →void
deploy({ projectId, branchId, slug, …input }) NeonFunctionDeployment multipart — input: { zip?: Blob | File, runtime?: "nodejs24", environment?: string } (environment is a JSON-encoded Record<string, string>)
// Deploy a bundled index.mjs inside a zip (first deploy must include zip)
const zip = await Bun.file("bundle.zip").arrayBuffer();
const { data: deployment } = await neon.functions.deploy({
  projectId,
  branchId,
  slug: "api",
  zip: new File([zip], "bundle.zip", { type: "application/zip" }),
  runtime: "nodejs24",
});
// Poll neon.functions.get until current_deployment.status is "completed"
neon.functions.customDomains

Branch custom domains (beta). v1 can only point a domain at a function (entity_type: "function", entity_id is the function slug). The response includes cname_target (the hostname to CNAME). Optional status, dns_status, binding_status, and status_reason report provisioning; they may be absent right after register. Those strings are not enums — treat any undocumented value as unknown. The domain goes live after DNS resolves and a certificate is issued on the first request.

Method Returns Notes
list({ projectId, branchId, …query }) [P] CustomDomain query: { limit? }
register({ projectId, branchId, …input }) CustomDomain input: { domain, entity_type, entity_id }
delete({ projectId, branchId, domain }) →void
const { data: registered } = await neon.functions.customDomains.register(
  {
    projectId,
    branchId,
    domain: "docs.example.com",
    entity_type: "function",
    entity_id: "api",
  },
);
// Point a CNAME for docs.example.com at registered.cname_target
neon.triggers

Branch-scoped triggers (beta). type is "schedule" or "storage_object_created". A schedule trigger uses a numeric five-field cron expression in UTC. A storage-object-created trigger watches one exact bucket and, optionally, an object-key prefix; matching is case-sensitive with no path normalization. List is the full set visible on the branch (not cursor-paginated). An inherited trigger keeps its project-wide id and stays disabled on the child until enabled there. Editing an inherited trigger writes a child-local shadow. Deleting an inherited trigger writes a tombstone so it does not reappear.

create / update keep the request type on the return type (ScheduleTrigger or StorageObjectCreatedTrigger). list / get return Trigger — narrow on type before reading schedule / next_run_at or storage_object_created. Update requires the existing trigger's type and does not convert kinds. next_run_at is schedule-only. inherited still marks a child-local view of an ancestor trigger; source_branch_id is no longer on the generated schedule trigger type.

Method Returns Notes
list({ projectId, branchId }) Trigger[] mixed kinds; narrow on type
create({ projectId, branchId, …input }) ScheduleTrigger or StorageObjectCreatedTrigger schedule: { type: "schedule", function_slug, name, schedule: { cron }, function_path?, enabled? }; storage: { type: "storage_object_created", function_slug, name, storage_object_created: { bucket_name, prefix? }, function_path?, enabled? }
get({ projectId, branchId, triggerId }) Trigger narrow on type
update({ projectId, branchId, triggerId, …input }) ScheduleTrigger or StorageObjectCreatedTrigger input must include the existing type; other fields optional. A storage storage_object_created patch still needs bucket_name
delete({ projectId, branchId, triggerId }) →void
const { data: trigger, error: createError } =
  await neon.triggers.create({
    projectId,
    branchId,
    type: "schedule",
    function_slug: "worker",
    name: "daily-refresh",
    schedule: { cron: "0 9 * * *" },
    enabled: false,
  });
if (createError) throw createError;

await neon.triggers.update({
  projectId,
  branchId,
  triggerId: trigger.trigger_id,
  type: "schedule",
  enabled: true,
});

const { data: uploadTrigger, error: uploadError } =
  await neon.triggers.create({
    projectId,
    branchId,
    type: "storage_object_created",
    function_slug: "worker",
    name: "process-uploads",
    storage_object_created: {
      bucket_name: "uploads",
      prefix: "incoming/",
    },
  });
if (uploadError) throw uploadError;

await neon.triggers.update({
  projectId,
  branchId,
  triggerId: uploadTrigger.trigger_id,
  type: "storage_object_created",
  enabled: false,
});
await neon.triggers.delete({ projectId, branchId, triggerId: trigger.trigger_id });
neon.credentials

Branch-scoped scoped credentials (beta). create returns api_token and s3_secret_access_key once. reveal recovers those secrets later (POST, so they never ride a GET). rotate replaces the secrets in place and keeps token_id.

Create input scopes are CredentialScope: storage:read, storage:write, ai_gateway:invoke, functions:invoke. List / create / rotate responses use GrantedCredentialScope[], which also includes telemetry:write and must accept unknown values. Echoing created.scopes into create() is a TypeScript error; narrow to CredentialScope at the call site. The SDK does not filter response scopes.

Method Returns Notes
list({ projectId, branchId }) CredentialMeta[]
create({ projectId, branchId, …input }) CreateCredentialResponse input: { name?, scopes, principal_type: "user" }
revoke({ projectId, branchId, tokenId }) →void
reveal({ projectId, branchId, tokenId }) CredentialSecret { token_id, api_token, s3_secret_access_key } — no branch_id. 404 if revoked, expired, or wrong project. 409 if issued before secret retrieval; rotate to obtain one
rotate({ projectId, branchId, tokenId }) RotateCredentialResponse Not idempotent. A lost 200 already committed; create a replacement and revoke this one. After rotate, a replica may briefly accept the old secret
const { data: created, error: createError } = await neon.credentials.create(
  {
    projectId,
    branchId,
    scopes: ["storage:read"],
    principal_type: "user",
  },
);
if (createError) throw createError;

const { data: revealed, error: revealError } = await neon.credentials.reveal(
  { projectId, branchId, tokenId: created.token_id },
);
if (revealError) throw revealError;

const { data: rotated, error: rotateError } = await neon.credentials.rotate(
  { projectId, branchId, tokenId: created.token_id },
);
if (rotateError) throw rotateError;
neon.aiGateway

Branch-scoped AI Gateway endpoint metadata (beta).

Method Returns Notes
get({ projectId, branchId }) BranchAiGateway 404 when AI Gateway is not enabled on the branch
neon.realtime

Branch-scoped Realtime (beta). enable, disable, and rotateSecret are asynchronous on the API (HTTP 202, no operations). With readiness polling on, they poll get until pending is false, under the same wait budget as operation polling. enable polls by default; disable and rotateSecret poll only when waitForReadiness is true on the client or the call.

Method Returns Notes
get({ projectId, branchId }) Realtime { enabled, pending, invocation_url?, revision?, allowed_origins? }
enable({ projectId, branchId, allowed_origins? }) →void Readiness polling on by default. Also applies new options to an enabled branch. Omitting allowed_origins keeps the current or inherited value; [] or ["*"] allows any origin
disable({ projectId, branchId }) →void Discards the shared secret
secret({ projectId, branchId }) RealtimeSecret { secret, pending }. Server-only: the backend seals queries with it (@neon/realtime/server). 404 while Realtime is disabled or before the secret exists
rotateSecret({ projectId, branchId }) →void secret() returns the new value once pending is false

A timeout is a NeonWaitTimeoutError with an empty operations list; the change is still queued, so call get to check on it.

const neon = createNeonClient({ apiKey, throwOnError: true });
const branch = { projectId, branchId };

await neon.realtime.enable({
  ...branch,
  allowed_origins: ["https://app.example.com"],
}); // resolves once pending is false

const { secret } = await neon.realtime.secret(branch);
// secret → NEON_REALTIME_SECRET on the application backend

await neon.realtime.rotateSecret(branch, { waitForReadiness: true });

projects.create and branches.create also accept realtime: { allowed_origins? } to enable Realtime on the new branch.

neon.logs

Branch-scoped logs from the services running on a branch — Neon Functions, object storage, and Postgres computes (private beta).

Being private beta shows, and all three calls behave the same way on a given branch:

Branch state Response
Telemetry not available in the branch's region 404, reason: "telemetry_not_enabled"
Project or branch missing, or no access to it 404, reason: "branch_not_found"
Telemetry available, nothing recorded in the window 200 with an empty logs / values array
Telemetry available, backend down 503, which the client retries on by default

The two 404s need different handling, and only reason tells them apart. Telemetry is region-gated and the region is fixed at project creation, so telemetry_not_enabled is a permanent property of the branch and an ordinary outcome to design around. branch_not_found is a real error — a wrong id or a key without access — and should not be absorbed into the same path. Both arrive as NeonNotFoundError, and reason is not lifted onto the error, so it has to be read off the API's body, which the SDK keeps as unknown:

import { NeonNotFoundError } from "@neon/sdk";

function logsUnavailableReason(error: unknown): string | undefined {
  if (!(error instanceof NeonNotFoundError)) return undefined;
  const body = error.body;
  if (typeof body !== "object" || body === null || !("reason" in body)) return undefined;
  return typeof body.reason === "string" ? body.reason : undefined;
}

const { error } = await neon.logs.fields({ projectId, branchId });
if (error) {
  if (logsUnavailableReason(error) === "telemetry_not_enabled") {
    // no logs on this branch, ever — carry on
  } else throw error;
}

Two more limits worth knowing, because the spec is wider than the backend. source is a three-value enum, but only function and storage were observed emitting; no branch produced a pg_endpoint record. And minimum_severity can be rejected outright with a 400 (NeonApiError, kind: "api") reading "minimum_severity is not supported by this branch's log backend" — so filter on severity_text if you need it to work everywhere.

Method Returns Notes
query({ projectId, branchId, …input }) [P] ProjectBranchLogRecord input: { since?, start_time?, end_time?, limit?, sort_order?, source?, service_name?, scope_name?, minimum_severity?, severity_text?, body_contains?, trace_id?, logql? } — filters combine with AND
fields({ projectId, branchId }) string[] field names this branch has emitted, usable as fieldName below
fieldValues({ projectId, branchId, fieldName, …query }) ProjectBranchLogFieldValuesResponse query: { since?, start_time?, end_time?, source?, limit? } — check is_truncated

Give the window as either since ("30m", "6h", "7d") or start_time; supplying both is rejected with conflicting_time_range. logql replaces the seven content filters rather than adding to them — combining them is rejected with conflicting_filters — while limit, sort_order, and the time window still apply alongside it. Seven days is the widest range served. The default window differs between the two calls: query covers the last hour, fieldValues the last six, so a value discovered by one is not guaranteed to appear in the other.

query pages for you, replaying the filters unchanged as the endpoint requires. If a page reports more records than it returned but no cursor to reach them, the walk fails with a NeonClientError rather than handing back a partial result as if it were complete.

// Errors from a function over the last 6 hours, newest first
const { data: errors } = await neon.logs
  .query({
    projectId,
    branchId,
    since: "6h",
    source: "function",
    // some branches' log backends reject minimum_severity; severity_text always works
    minimum_severity: "error",
  })
  .all();

// Paging replays the filters for you — the endpoint returns wrong results otherwise
for await (const line of neon.logs.query({ projectId, branchId, since: "1h" })) {
  console.log(line.timestamp, line.message);
}

// Discover what you can enumerate, then read one field's values
const { data: fields } = await neon.logs.fields({ projectId, branchId });
// e.g. ["service_name", "severity_text", "scope_name", "entity_type"]

const { data: services } = await neon.logs.fieldValues(
  { projectId, branchId, fieldName: "service_name", since: "24h" },
);
console.log(services?.values);
if (services?.is_truncated) {
  // an arbitrary subset — narrow `since` or `source` before filtering on it
}

fieldName must be a name fields returned. The enumerable set and the filterable set overlap rather than nest: source is a filter — on query and on fieldValues' own query — but is not enumerable, so fieldValues({ …, fieldName: "source" }) answers 400 with reason: "unknown_field"; entity_type is enumerable but is not a filter on query.

fields returns a bare string[] because its response carries nothing else. fieldValues returns the whole response, because is_truncated is what decides whether the values can be trusted and unwrapping would hide it.

neon.snapshots
Method Returns Notes
list({ projectId }) Snapshot[]
create({ projectId, branchId, …input }) Snapshot input: { name?, timestamp?, lsn?, expiresAt? } (point-in-time)
update({ projectId, snapshotId, …input }) Snapshot input: { name?, expiresAt? } — pass expiresAt: null to clear the expiration
delete({ projectId, snapshotId }) →void
restore({ projectId, snapshotId, …input }) Branch see below
getSchedule({ projectId, branchId }) BackupSchedule frequency stays a wide string: a branch can still hold a schedule created when the API accepted other values
setSchedule({ projectId, branchId, schedule }) →void schedule[].frequency is narrowed to SnapshotFrequency ("daily" | "weekly" | "monthly")
// Snapshot a branch at a point in time (or an `lsn`), with a name + TTL
const { data: snapshot } = await neon.snapshots.create({
  projectId,
  branchId,
  name: "pre-migration",
  timestamp: "2026-06-01T00:00:00Z",
  expiresAt: "2026-07-01T00:00:00Z",
});

restore input: { name?, targetBranchId?, finalize?, preview?, keepOnAbort? }.

  • Restoring as a new branch (no targetBranchId) finalizes by default → ready to use.
  • Restoring onto an existing branch doesn't finalize by default, so you can preview first.
  • Transaction-style with preview: it restores un-finalized, runs your callback against the restored branch, then finalizes (commit) if it returns true or deletes the preview branch (abort) if false (unless keepOnAbort):
await neon.snapshots.restore({
  projectId,
  snapshotId,
  targetBranchId,
  // second argument carries the call's signal; the SDK cannot interrupt your callback
  preview: async (branch, { signal }) =>
    (await checks(branch, { signal })) === "ok", // true → commit · false → abort
});
neon.operations
Method Returns Notes
list({ projectId }) [P] Operation
get({ projectId, operationId }) Operation
waitFor({ operations }, options?) →void options: { pollIntervalMs?, timeoutMs?, signal?, throwOnError? } — the readiness primitive. requestTimeoutMs, waitForReadiness, and wait are excluded. A wait timeout's error.operations is the still-outstanding subset; pass it here to resume.
// Wait on operations from a raw call (or when waitForReadiness is off)
const { data } = await raw.createProjectBranch({
  client: neon.client,
  path: { project_id: projectId },
  body: { branch: { name: "wip" } },
});
const { error } = await neon.operations.waitFor(
  { operations: data!.operations },
  { timeoutMs: 120_000 },
);
neon.consumption

Cursor-paginated billing metrics. Each takes { from, to, granularity, project_ids?, orgId? } (perBranchV2 requires project_ids; v2 also requires metrics). orgId defaults to the client's orgId. Snake_case org_id is still accepted; orgId wins when both are set. v2 methods error if no org is set.

Method Returns
perProject(query) [P] ConsumptionHistoryPerProject
perProjectV2(query) [P] ConsumptionHistoryPerProjectV2
perBranchV2(query) [P] ConsumptionHistoryPerBranchV2
const neon = createNeonClient({
  apiKey: process.env.NEON_API_KEY!,
  orgId: "org-...",
});

for await (const project of neon.consumption.perProject({
  from: "2026-06-01T00:00:00Z",
  to: "2026-06-30T00:00:00Z",
  granularity: "daily",
})) {
  console.log(project);
}

await neon.consumption.perProject({
  from: "2026-06-01T00:00:00Z",
  to: "2026-06-30T00:00:00Z",
  granularity: "daily",
  orgId: "org-other",
}).all();
neon.apiKeys
Method Returns Notes
list() ApiKeysListResponseItem[]
create({ keyName }) ApiKeyCreateResponse the key token is shown once
revoke({ keyId }) ApiKeyRevokeResponse
neon.regions / neon.user
Method Returns
regions.list() RegionResponse[]
user.me() CurrentUserInfoResponse
user.organizations() Organization[]
neon.auth

Branch-scoped Neon Auth (the legacy project-scoped endpoints are deprecated and stay raw-only).

Method Returns Notes
get({ projectId, branchId }) NeonAuthIntegration
create({ projectId, branchId, …input }) NeonAuthCreateIntegrationResponse enable the integration
disable({ projectId, branchId, deleteData? }) →void
updateConfig({ projectId, branchId, …input }) NeonAuthConfigResponse
oauthProviders.list({ projectId, branchId }) NeonAuthOauthProvider[]
oauthProviders.add({ projectId, branchId, …input }) NeonAuthOauthProvider
oauthProviders.update({ projectId, branchId, providerId, …input }) NeonAuthOauthProvider
oauthProviders.delete({ projectId, branchId, providerId }) →void
trustedDomains.list({ projectId, branchId }) NeonAuthRedirectUriWhitelistDomain[] redirect-URI whitelist
trustedDomains.add({ projectId, branchId, domain, auth_provider }) →void redirect-URI whitelist
trustedDomains.delete({ projectId, branchId, auth_provider, domains }) →void redirect-URI whitelist
users.create({ projectId, branchId, …input }) NeonAuthCreateNewUserResponse
users.delete({ projectId, branchId, authUserId }) →void
users.updateRole({ projectId, branchId, authUserId, roles }) role
neon.projects.permissions
Method Returns
list({ projectId }) ProjectPermission[]
grant({ projectId, email }) ProjectPermission
revoke({ projectId, permissionId }) ProjectPermission

For an org-owned project, roles for existing organization members live on neon.projects.members below instead.

neon.projects.members

Per-project roles for members of the owning organization. Distinct from neon.projects.permissions, which shares a project with an individual by email: these act on existing org members by member id, and clearing a grant leaves the member's organization-role default in force rather than removing their access. Org-owned projects only — a personal project answers 404.

Method Returns Notes
list({ projectId, …query }) [P] ProjectMember query: { limit? }
setRole({ projectId, memberId, role, confirmSelfDemotion? }, options?) ProjectMemberRoleResponse role: "viewer" | "editor" | "admin"; idempotent
removeRole({ projectId, memberId, confirmSelfLockout? }, options?) ProjectMemberRoleResponse clears the explicit grant; idempotent

A ProjectMember carries several role-ish fields, and they are not interchangeable. Read effective_project_permission for "what can this member actually do" — VIEWER / EDITOR / ADMIN, uppercase. org_default_project_permission and explicit_project_permission are the two inputs it was resolved from, and grant_source says which one won (explicit, org_role_default, org_admin_override, unassigned).

The two lowercase fields are a different axis: project_role is the explicit grant you set with setRole and shares its "viewer" | "editor" | "admin" type, and org_role is the member's organization role. Reading back project_role after a setRole tells you the grant landed — it does not tell you the member's effective access, which the org-role default can still exceed.

The two confirmations are off by default so a call cannot silently cost you access to your own project. Pass one only when you mean to lower your own role (confirmSelfDemotion) or to drop your own grant when that removes your management access (confirmSelfLockout); without it the API rejects the call.

const { data: grant } = await neon.projects.members.setRole(
  { projectId, memberId, role: "editor" },
);
// A downgrade can leave credentials the member still holds
if (grant?.credential_rotation_recommended) { /* rotate database credentials */ }
if (grant?.org_api_key_rotation_recommended) { /* rotate project-scoped org keys */ }

Raw layer (every endpoint, 1:1)

Anything not wrapped above is available raw. Pass neon.client to reuse the client's auth:

import { raw } from "@neon/sdk";
// or, for guaranteed tree-shaking: import { getProjectBranchSchema } from "@neon/sdk/raw";

const { data, error } = await raw.getProjectBranchSchema({
  client: neon.client,
  path: { project_id, branch_id },
  query: { db_name: "neondb" }, // db_name is required
});

The raw layer speaks the exact same result contract as the ergonomic client. By default a raw call resolves to a { data, error } NeonResult with the typed NeonErrorUnion on the error channel; pass throwOnError: true to get the bare resource and throw instead — and the return type narrows accordingly:

// bare resource, throws a NeonErrorUnion member on failure
const { project } = await raw.getProject({
  client: neon.client,
  path: { project_id },
  throwOnError: true,
});

There is no responseStyle switch — throwOnError is the only one. neon.client is the underlying configured Fetch client; raw.* are the wrapped generated functions, and all request/response/error types are re-exported flat from @neon/sdk for import type { Project, Branch, … }.

Regenerating the client

The client is generated from a vendored, pinned copy of the spec in spec/neon-openapi.json using @hey-api/openapi-ts.

pnpm --filter @neon/sdk spec:pull   # refresh the vendored spec from neon.com
pnpm --filter @neon/sdk generate     # regenerate src/client
pnpm --filter @neon/sdk build        # typecheck + bundle

A coverage test (src/neon/coverage.test.ts) fails CI whenever the generated operation set changes, so every new endpoint is consciously wrapped or left raw-only. When you wrap new endpoints, also update this README (see AGENTS.md → The SDK package).

License

Apache-2.0

Keywords