Neon CLI
The neon package is a command-line interface that lets you manage Neon — Lakebase Postgres, Object Storage, Functions, Managed Better Auth, and the AI Gateway — directly from the terminal. For the complete documentation, see Neon CLI.
The legacy neonctl package is a lightweight compatibility package that depends
on this package and invokes the same CLI entry point. The implementation and
build artifacts live only here.
Install the Neon CLI
npm
npm i -g neon
Requires Node.js 20.19 or higher.
Howebrew
brew install neonctl
Binary (macOS, Linux, Windows)
Download a neon-<platform> binary from the releases page, which carries one asset per platform for each neon@<version> tag.
Upgrade
npm
npm update -g neon
Requires Node.js 20.19 or higher.
Howebrew
brew upgrade neonctl
Binary (macOS, Linux, Windows)
To upgrade a binary version, download the latest binary file, as described above, and replace your old binary with the new one.
Global npm, pnpm, Bun, and Homebrew installs check for updates once per day.
When an update is available, terminal commands print the matching upgrade command
every three days. Set NO_UPDATE_NOTIFIER=1 to disable these checks. The notifier
does not run in CI, for machine-readable output, or from standalone binaries.
On macOS and Linux, the same terminal commands warn once a day when more than one
global neon install is on PATH, listing each one in PATH order. A project's
node_modules/.bin, which npx and package scripts add, doesn't count. When one
of them is Homebrew, the warning recommends brew uninstall neonctl, which
leaves neon running the other install, such as npm. While duplicates remain, the update notice is held back;
NO_UPDATE_NOTIFIER=1 silences both.
Commands also warn once a day when Neon agent skills are behind their source, in this
directory's skills-lock.json or the user-level skills lock (see
neon skills status). This warning runs without a
terminal too, so coding agents see it. It checks GitHub once a day in the background and
follows the same CI, machine-readable output, standalone binary, and
NO_UPDATE_NOTIFIER=1 rules.
Connect
Run the following command to authenticate a connection to Neon:
neon login
neon auth is an alias of neon login. The login command launches a browser window where you can authorize the Neon CLI to access your Neon account. Running a Neon CLI command without authenticating with neon login automatically launches the browser authentication process, except for neon me, which reports that you are not signed in.
Alternatively, you can authenticate a connection with a Neon API key using the --api-key option when running a Neon CLI command. For example, an API key is used with the following neon projects list command:
neon projects list --api-key <neon_api_key>
For information about obtaining an Neon API key, see Authentication, in the Neon API Reference.
To check which account and credential a command uses, run neon me with the same --profile or --api-key option and environment as that command. The Authentication row names the source, such as OAuth (profile DEFAULT), API key (profile work), API key (--api-key), or API key (NEON_API_KEY). When no credential is usable, neon me exits 1 with Not signed in and the neon login command to run; it never starts a sign-in itself.
Create a project without an account
neon claim create provisions a temporary Claimable Neon project for an agent without
requiring a Neon account or opening a browser:
# Lakebase Postgres is always included
neon claim create
# Request Managed Better Auth and the Data API too
neon claim create --service auth --service data-api
# Create the project in another region (default: aws-us-east-2)
neon claim create --region-id aws-eu-central-1
When the current directory has a neon.ts, claim create also requests every service
declared there. Explicit --service values are added to that set. If that policy enables
the Data API, identity includes a snake_case data_api object. A neon.ts with
dataApi: { authProvider: "external", jwksUrl: "https://idp.example.com/jwks.json" }
sends:
{
"type": "anonymous",
"capabilities": ["postgres", "data_api"],
"source": "neon_cli",
"data_api": {
"auth_provider": "external",
"jwks_url": "https://idp.example.com/jwks.json"
}
}
neon claim create --service data-api with no neon.ts Data API block omits data_api:
{
"type": "anonymous",
"capabilities": ["postgres", "data_api"],
"source": "neon_cli"
}
Object Storage, Functions, and the AI Gateway passed with --service are sent so
demand is recorded, then reported as unavailable until the project is claimed. The CLI
does not drop them from the request.
The bundled env pull still follows neon.ts. A neon.ts that names AI Gateway,
Functions, or Object Storage fails the pull, and the create is rolled back. Use
--config for a different policy file (registration and that pull), or --no-env-pull
to skip the dotenv write.
The command writes:
- a
.neoncontext with the project id and pinned branch; - an owner-only identity assertion under the CLI config directory;
DATABASE_URL,DATABASE_URL_UNPOOLED,NEON_BRANCH, and Auth / Data API variables whenneon.tsdeclares them (or, with noneon.ts, when they are provisioned) to.envor.env.local(disable this with--no-env-pull). The write is the sameenv pullused after create.
Subsequent project commands find that assertion by the linked project id, exchange it for a short-lived agent token, and send API calls to Claimable Neon. The service decides which operations are allowed before claim.
neon claim status # lifecycle and transfer status
neon projects get <project-id> # regular CLI command, same agent token
neon psql --role-name neondb_owner -- -c "select now()"
neon config plan
neon checkout main
neon env pull
neon claim accept # create a claim code and open the transfer URL
neon claim delete --yes # permanently delete an unclaimed project
neon claim list # local records, including expired
neon claim delete <project-id> --yes
status, accept, and delete take an optional project id from claim list, so a
project stays manageable after its original directory is gone. list prints state
(unclaimed or expired) from the identity assertion clock and the project
expiry, plus project_expires_at. delete also drops a
local record whose identity assertion has expired or been revoked.
neon claimable is an alias for neon claim. For local service development, set
CLAIMABLE_NEON_HOST=http://localhost:8787; non-local origins must use HTTPS.
Project and branch creation
Choose the PostgreSQL version when creating a project:
neon projects create --name my-project --pg-version 18
Supported versions are 14 through 19; version 19 is available only in
regions where it has been enabled.
Create a protected branch when it should not be modified or deleted by routine automation:
neon branches create --project-id <project-id> --name production --protected
Project and branch creation include connection credentials in their output by
default for backward compatibility. When the output may be logged or passed to
an agent, use --no-secrets to return only the created resource metadata:
neon projects create --name my-project --output json --no-secrets
neon branches create --project-id <project-id> --name preview --output json --no-secrets
The flag omits the complete connection_uris block rather than redacting one
field. Retrieve a connection string separately when it is actually needed.
Enable logical replication
Enable logical replication for every endpoint in an existing project:
neon projects update <project-id> --enable-logical-replication
The CLI asks for confirmation because enabling logical replication suspends
active endpoints and cannot be undone. For non-interactive automation, pass
--yes explicitly:
neon projects update <project-id> --enable-logical-replication --yes
Connect with psql
The psql command
neon psql [branch] opens a psql session against a branch. It builds the connection string for the branch and launches psql — a shortcut for neon connection-string --psql. See Neon CLI commands — psql for the full reference.
neon psql # default branch
neon psql main # a specific branch
neon psql main@2024-01-01T00:00:00Z # point-in-time (branch@timestamp or branch@lsn)
neon psql --pooled # use the pooled connection
Arguments after -- are forwarded to psql:
neon psql main -- -c "SELECT version()"
neon psql main -- -f script.sql --csv
Options: --project-id, --role-name, --database-name, --pooled, --endpoint-type (read_only | read_write), --ssl, plus the global options.
The --psql flag
Several other commands accept a --psql flag that opens a psql session against the resolved endpoint:
neon connection-string --psql --project-id <id>
neon projects create --psql
neon branches create --psql
Any arguments after -- are forwarded to psql, for example:
neon cs --psql --project-id <id> -- -c "SELECT version()"
neon cs --psql --project-id <id> -- -f script.sql --csv
Embedded psql fallback
If the system has psql installed on $PATH, --psql continues to spawn the native binary — there is no behavior change for existing users.
If psql is not found on $PATH, neon now falls back to an embedded TypeScript implementation. There is nothing to install or configure; it ships with neon. This removes the "no psql binary" trap on machines (and CI runners) that don't have PostgreSQL client tools installed.
Automatic fallback is the intended path — there is normally no flag to set. The embedded implementation can also be force-selected (primarily for tests and CI, e.g. to exercise it even when a native psql is present):
--fallback— force the embedded implementation onconnection-string,projects create, andbranches create. Intentionally hidden from--help: it's a test/CI knob, not a user-facing option (the automatic fallback above is the supported behavior).NEONCTL_PSQL_FALLBACK=1— environment variable with the same effect as--fallback. Convenient for scripts and CI.
The embedded implementation is verified against a conformance suite that diffs its behavior against real PostgreSQL (14–18) and the upstream psql regression + TAP tests.
What works
REPL & scripting
- Interactive REPL with a hand-rolled VT100 line editor (no native bindings); vi and emacs edit modes (
VI_MODEpsql variable) - Persistent command history (
~/.psql_history, libreadline format) ~/.psqlrcautoload (including$PGSYSCONFDIR/psqlrcand version-suffixed variants)- Scripted modes:
-c "SQL",-f script.sql, and stdin;--single-transaction,ON_ERROR_STOP,ECHO,--echo-all SINGLELINE(-S),\timing,\watch(named flagsc=/i=/m=, unbounded continuous mode)
Backslash commands
- All output formats: aligned, unaligned, wrapped, csv, json, html, asciidoc, latex, latex-longtable, troff-ms (
\a \H \t \x \pset \f \C…) - All
\d*describe commands with full upstream parity (columns, indexes, foreign keys, triggers, view definitions, sequences, RLS, replica identity, partitions, tablespaces, access methods, inheritance, FDW, stats objects, publications, subscriptions, per-column FDW options, TOAST owner) \copyto/from file,PROGRAM,STDIN,STDOUT(incl. the\.EOF marker);\g/\gx/\gset/\gdesc/\gexecand\g | programpipes- Extended query + pipeline mode (
\bind,\bind_named,\startpipeline,\parse,\sendpipeline) \crosstabview,\lo_*large objects,\e/\edit(external editor),\s(history),\?/\hhelp,\if/\elif/\else/\endif,\set/\unset,\connect,\encoding(liveSET client_encoding),\!,\cd,\prompt(incl. no-echo-),\password- Tab completion (~88 rules incl. live
pg_settingsGUC lookup, deepALTERsub-actions,JOINclauses, windowOVER)
Connection & authentication
- libpq-equivalent lookup precedence: argv flags > URI >
PG*env vars >~/.pgpass>pg_service.conf> libpq defaults - SCRAM-SHA-256 / SCRAM-SHA-256-PLUS with
tls-server-end-pointchannel binding (channel_binding); MD5 and cleartext;require_auth - Multi-host failover & load balancing:
target_session_attrs(any / read-write / read-only / primary / standby / prefer-standby),load_balance_hosts, DNS fan-out,hostaddr - Unix-domain sockets (host beginning with
/); TCP keepalives (keepalives,keepalives_idle)
TLS
sslmodedisable → verify-full; client certs in PEM or DER viasslcert/sslkey(+sslpasswordfor encrypted keys, with the libpq group/world-readable-key check)- Trust config:
sslrootcert(incl.=systemwithSSL_CERT_FILE/SSL_CERT_DIR), default client-cert discovery (~/.postgresql/postgresql.{crt,key}),sslcertmode - CRL:
sslcrlandsslcrldir;ssl_min_protocol_version/ssl_max_protocol_version;sslsni - Direct-SSL negotiation (
sslnegotiation=direct, PostgreSQL 17+, via ALPN)
What's not supported
- GSSAPI / SSPI (
gssencmode, Kerberos/SSPI auth,requirepeer). GSS transport encryption needs a native Kerberos binding, which the embedded psql deliberately avoids (pure TypeScript, zero native dependencies — the same reason the line editor is hand-rolled).node-postgresdoesn't support it either, and Neon doesn't use it.gssencmode=disable/preferare accepted;gssencmode=requireis rejected with a clear error.requirepeeris parsed but a Unix-socket connection that sets it is refused (Node exposes no peer-credential API — it is not silently ignored). keepalives_interval/keepalives_count— Node's socket API exposes only keepalive enable + initial delay, so these are accepted but not applied.
Known limitations
- TLS cipher is runtime-dependent. The negotiated TLS 1.3 ciphersuite is chosen by the host runtime's TLS library from an offer byte-identical to libpq's. Under Node (OpenSSL) that is
TLS_AES_256_GCM_SHA384, matching vanilla psql; under Bun (BoringSSL) it isTLS_AES_128_GCM_SHA256. Both are TLS 1.3 AEAD suites with no practical security difference, and neither runtime exposes a client-side knob to steer the selection.
Subscribe to a Realtime query
neon realtime subscribe streams a live SQL query until interrupted:
neon realtime subscribe "SELECT id, title, completed FROM realtime_todos ORDER BY id"
The default terminal output is append-only. It identifies the selected project,
branch, and database; prints the initial result once; reports connection-state
changes; and then prints each published change set with its transaction IDs, a compact
+insert ~update -delete summary, and the affected rows. It does not take over the
terminal or require keyboard navigation. Pass --changes to display the raw rows
and changes events instead of the materialized view.
For scripts and agents, JSON output is a JSON Lines stream with one complete event per line. YAML output uses one document per event:
neon realtime subscribe --changes --output json "SELECT * FROM realtime_todos" |
jq --unbuffered 'select(.type == "changes")'
neon realtime subscribe --output yaml "SELECT * FROM realtime_todos"
Machine-readable output retains the event stream: without --changes, each event is
a complete snapshot; with --changes, events are raw rows and changes objects.
Lifecycle labels and the target header are terminal-only.
Configure autocompletion
The Neon CLI supports autocompletion, which you can configure in a few easy steps. See Neon CLI commands — completion for instructions.
Linking a project
neon link is a Vercel-style command that binds the current directory to a Neon project. It picks (or creates) an organization and a project and writes a .neon file ({ "orgId", "projectId", "branch" }) that subsequent commands run in this directory (or any sub-directory) pick up automatically. Personal-account projects omit orgId.
link resolves what it can and verifies every identifier you pass before writing, so a successful .neon always has a project and a branch (and an organization when the project has one):
- org is inferred from the project (so
--project-idalone is enough); it's omitted only when the project has no organization (personal account).--org-idthat the project does not confirm is an error. - project is taken from
--project-id(or chosen interactively).--org-idwithout a project opens the project picker in a TTY. Without a TTY it needs-y(select the only project, or print IDs) or--project-id. - branch is taken from
--branch, a still-valid pin for the same project, or the project's branch list: one branch is pinned automatically; several prompt in a TTY, or-ypins the default. Zero branches, a stale pin, or several branches with no TTY and no-yfail without writing.
When a branch is pinned, link also runs env pull so the branch's Neon env vars (DATABASE_URL, …) land in a local .env. Pass --no-env-pull to skip the pull (for example when injecting env at runtime with neon-env run or neon dev).
Migrating from
set-context?set-contextis deprecated in favor oflink(see below). It still works exactly as before for now (a raw write), it just prints a deprecation warning. The.neonbranchIdfield is also superseded bybranch(which stores the branch name when known); oldbranchIdfiles are still read and are upgraded tobranchthe next timelink/checkoutwrites the context.
There are two modes:
Interactive (default) — guided prompts for humans:
$ neon link
? Which organization would you like to link? › Personal Org (org-abc123)
? Which project would you like to link? › + Create new project…
? Name for the new project: › my-app
? Which region should the new project run in? › AWS US East (Ohio) (aws-us-east-2)
Created project polished-snowflake-12345678 ("my-app") in aws-us-east-2.
Linked .neon:
orgId: org-abc123
projectId: polished-snowflake-12345678
branch: main
When you link an existing project that has more than one branch, the interactive flow adds a
final step to pick which branch to pin — the same + Create a new branch… + list selector used by
neon checkout (a single-branch project is pinned automatically, no prompt):
$ neon link
? Which organization would you like to link? › Personal Org (org-abc123)
? Which project would you like to link? › my-app (polished-snowflake-12345678)
? Which branch would you like to link? › [default] main (br-main-branch-87654321)
link --project-id … skips org and project. One branch is pinned with no prompt. Several branches
in a TTY show the branch prompt; -y pins the default; no TTY without -y or --branch exits 1:
$ neon link --project-id polished-snowflake-12345678
? Which branch would you like to link? › [default] main (br-main-branch-87654321)
Non-interactive (-y, flags, or --params JSON) — for scripts, CI, and agents:
# One organization and one project: link them. Several: print IDs and exit 1.
neon link -y
# After choosing an organization from that list, discover its project the same way
neon link -y --org-id org-abc123
# Link to an existing project (org is inferred). Pins the only branch;
# several branches prompt in a TTY, or -y pins the default.
neon link --project-id polished-snowflake-12345678
# Same, pin the project's default branch when several exist
neon link --project-id polished-snowflake-12345678 -y
# Same, but also pin a branch (name or id — resolved and stored as its name)
neon link --project-id polished-snowflake-12345678 --branch main
# Pin/switch the branch in the already-linked project
neon link --branch main # alias: --branch-id
# Create a new project and link it (pins the new project's default branch)
neon link --org-id org-abc123 --project-name my-app --region-id aws-us-east-2
# Same payload, one JSON blob
neon link --params '{"orgId":"org-abc123","projectName":"my-app","regionId":"aws-us-east-2"}'
# Forget the current context
neon link --clear
# Offline write — no API calls, no verification (see --no-checks below)
neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-12345678 --branch main
Every supplied identifier is checked before anything is written, with actionable errors — e.g. Project '…' not found, You don't have access to project '…', Organization '…' not found, or your API key doesn't have access to it, Project '…' belongs to organization 'A', not 'B', or Branch '…' not found in project '…'. Available branches: ….
Agents and scripts: neon link -y selects the only organization and project. If several exist, it prints their IDs (human table, or --output json / --output yaml) and names the flag to pass. --org-id alone, without -y and without a TTY, is not a completed link — pass --project-id or -y. neon link --help prints the explicit-flag recipe.
neon link -y
neon link -y --org-id <org-id>
neon link -y --project-id <project-id>
neon orgs list --output json
neon projects list --org-id <org-id> --output json
neon link --project-id <project-id> [--branch <name> | -y]
neon link --org-id <org-id> --project-name <name> --region-id aws-us-east-2
Organization-scoped API keys cannot list user organizations (orgs list) or call the regions endpoint:
- Pass
--org-id(Neon Console → Settings) or--project-id(org is inferred from the project). - If the key is org-scoped and at least one project already exists, interactive
linkauto-detects the org from the first project and prints an informational message. - If no projects exist yet, interactive
linkerrors pointing at--org-id. - When the regions endpoint is not allowed, interactive create falls back to a built-in static region list. Non-interactive create already requires
--region-id.
Offline writes (--no-checks) — write the .neon with no API calls at all: no org inference, no existence/access verification, no env pull. Because nothing can be resolved offline, it requires --org-id, --project-id, and --branch (stored verbatim). Handy for scripted/CI setups or re-creating a .neon from values you already trust:
neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-12345678 --branch main
set-context is deprecated
set-context is deprecated in favor of link and prints a deprecation warning (to stderr, so it never pollutes stdout or scripts). For backward compatibility its behavior is unchanged: it's still a raw, offline write of exactly the fields you pass (no org inference, no verification, no env pull), and bare set-context still clears the file. Nothing breaks today — but new work should use link, and set-context will be removed in a future major release.
How today's set-context uses map onto link:
set-context (deprecated) |
Recommended link equivalent |
|---|---|
neon set-context --project-id <id> |
neon link --project-id <id> (infers org + verifies; pins the only branch, or -y for the default) |
neon set-context --org-id <id> |
Not a completed link. Pass --project-id / -y, or an --org-id flag on the org-scoped command |
neon set-context --branch-id <id> |
neon link --branch <name|id> or neon checkout <branch> |
neon set-context (clear) |
neon link --clear |
| a raw local write (no network) | neon link --no-checks --org-id <id> --project-id <id> --branch <name> |
The key difference: link resolves and verifies before writing a complete context, whereas set-context writes whatever you give it verbatim. The closest like-for-like replacement for the old raw write is link --no-checks with org, project, and branch.
open
open launches the linked project's page in the Neon Console. It reads the closest .neon file without authenticating or calling the Neon API, so it works from any sub-directory of a linked project.
neon open
# Open a project without changing the linked context
neon open --project-id polished-snowflake-12345678
A branch pinned in .neon does not change the destination. .neon stores the branch as a name, while the Console route requires its ID; resolving it would turn this local command into an authenticated API call.
checkout
checkout [id|name] switches the pinned branch in a linked folder. It resolves the branch (by name or id) against the project, then heals the .neon file: it always (re)writes projectId, branch, and orgId (when the project has one), so a .neon that was missing fields or drifted ends up complete and consistent. The branch is stored as its name when known (matching link). When orgId isn't already known (from --org-id or the existing .neon), it's looked up from the project itself.
The branch argument is optional: run neon checkout with no branch in an interactive terminal to fetch the project's branches and pick one from a list. In a non-interactive context (CI or no TTY), a branch must be passed explicitly.
Branch id vs name is detected automatically (a br-… value is treated as an id):
- id — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one, including with
--create). - name — matched by name. If the name doesn't exist, pass
--createto create it (equivalent toneon branch create --name <name>: branched from the project's default branch with a read-write compute, or fromneon.tswhen that file is present), then check it out.--createis a no-op when the name already exists. Without--create, an interactive terminal offers to create the branch; in CI or with no TTY the error names--create.
The project is resolved through the standard neon chain, each entry winning over the next:
--project-id <id>flagprojectIdfrom the closest.neonfile (found by walking up from the current directory — see "Where.neonlives" below)- If still unresolved and the API key maps to exactly one project, that project is auto-detected (same behaviour as
branchesandconnection-string)
If none of those resolve a project, checkout prints a telling error explaining the chain above. In an interactive terminal it then offers to run neon link in the current folder so you can pick (or create) a project on the spot; once linked, it continues and pins the requested branch. In non-interactive contexts (CI or no TTY) it exits with a non-zero code and the same guidance instead of prompting.
The resolved branch is then written (by name) to the same .neon file link uses:
$ neon checkout main --project-id polished-snowflake-12345678
Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
$ neon checkout dev --create --project-id polished-snowflake-12345678
Created branch dev (br-dev-branch-12345678).
Checked out branch br-dev-branch-12345678 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
$ cat .neon
{
"orgId": "org-abc123",
"projectId": "polished-snowflake-12345678",
"branch": "dev"
}
After pinning the branch, checkout also runs env pull by default, so the branch's Neon env vars are written to your local .env and you can start building right away — the branch-first loop is just link + checkout. Pass --no-env-pull to skip it (for example when env is injected at runtime via neon-env run / neon dev, or to keep secrets out of the working tree). A pull failure never undoes the checkout: the branch stays pinned and the failure is surfaced as a warning pointing you at neon env pull (or neon deploy if a neon.ts-declared service is missing).
diff
diff [compare-branch] prints a git-style schema diff between the branch you're on and another branch — the top-level companion to checkout. It reads the branch pinned in .neon as the side under review (the +++ side) and compares it against the branch you name (the ---, reference side), so + lines are what your current branch adds on top of the reference:
# On feature/add-comments (pinned in .neon), see how it differs from main:
$ neon diff main
→ Comparing schema main → feature/add-comments
diff --neon database neondb
--- main (br-crimson-snow-12345678)
+++ feature/add-comments (br-dry-salad-87654321)
@@ -63,7 +64,8 @@
CREATE TABLE public.users (
id integer NOT NULL,
email text NOT NULL,
- created_at timestamp with time zone DEFAULT now()
+ created_at timestamp with time zone DEFAULT now(),
+ display_name text
);
compare-branchis optional. Omit it to compare the current branch against its parent (neon diffanswers "what did I change since branching?"). It accepts a branch name orbr-…id.--branch, -b <name|id>overrides the side under review instead of reading.neon— e.g.neon diff main --branch feature/checkoutdiffs an explicit branch againstmain.--database, --db <name>limits the diff to one database; by default every database on the current branch is compared (each rendered as its owndiff --neon database <name>block). A database missing on the reference side shows as fully added.--output json|yamlemits a structured result per database ({ database, base_branch, compare_branch, has_changes, diff }) for scripting; the default renders the colorized git-style diff (respecting--no-colorand non-TTY pipes).
The human-readable summary line goes to stderr and the diff body to stdout, so neon diff main > changes.patch captures just the diff. When the schemas match, diff prints No schema differences … and writes nothing to stdout. For history-aware comparisons (a branch against its own past state at a timestamp or LSN), use branches schema-diff.
env pull
env pull writes the linked branch's Neon environment variables into a local dotenv file: an existing .env if you have one, otherwise .env.local (override with --file <path>). Only Neon-managed keys are written (see the table below); any other lines in the file are preserved. The branch comes from the closest .neon file, so no --branch is needed (pass --branch <id|name> to target another branch).
What gets pulled, in precedence order:
--serviceand/or--env, when you pass either — their union is the complete selection, ignoringneon.tsand unselected branch variables.--serviceadds a service's complete variable bundle;--envadds only the individual variables you name.neon.ts, when the working directory has one — the policy is the source of truth, same asneon devandneon deploy. Declared function URLs are derived from the branch connection host; the function does not have to be deployed.- Everything the branch has otherwise — Postgres, Neon Auth, the Data API, object storage, and function invocation URLs read back from the branch, plus the AI Gateway. The gateway has no branch-level state to read back, so a bare
env pullasks for it rather than detecting it and may mint a branch credential. To leave it out, name only what you do want with--serviceand/or--env.
On an unclaimed Claimable Neon project, neon.ts is still the source of truth when it only declares Postgres, Auth, and the Data API. A neon.ts that declares AI Gateway, Functions, or Object Storage fails: those cannot be used until the project is claimed. Without a neon.ts, a bare pull writes provisioned Postgres, Auth, and Data API. Naming AI Gateway, Functions, or Object Storage with --service / --env warns and writes nothing for them. --config selects the policy file, the same flag as claim create and config plan.
If the gateway can't be resolved, it is dropped with a warning and the rest of the pull still lands. Gateway variables already in your file for this branch are left alone — a pull that couldn't reach the gateway is no evidence the branch has stopped having one — while ones left over from a different branch are pruned like any other stale value.
# Refresh the linked branch's vars in place
neon env pull
# Pull a specific branch into a specific file
neon env pull --branch preview --file .env.preview
# Use a specific neon.ts
neon env pull --config ./claimable.ts
# Only the AI Gateway
neon env pull --service ai-gateway
# Repeat the flag or comma-separate; -s, --service and --services are all accepted
neon env pull -s postgres -s data-api
neon env pull -s postgres,auth
# Pull one exact variable; repeat -e or comma-separate for more
neon env pull -e DATABASE_URL
neon env pull -e DATABASE_URL,NEON_AUTH_BASE_URL
# The selectors compose as a union: all Auth vars plus DATABASE_URL
neon env pull -s auth -e DATABASE_URL
# Function invocation URLs
neon env pull -s functions
neon env pull -e NEON_FUNCTION_HELLO_BASE_URL
Every services flag in the CLI takes those three spellings, the same value syntax, and the same service names — see config init --services.
--service |
Variables |
|---|---|
postgres |
DATABASE_URL, DATABASE_URL_UNPOOLED |
auth |
NEON_AUTH_BASE_URL, NEON_AUTH_JWKS_URL |
data-api |
NEON_DATA_API_URL |
functions |
NEON_FUNCTION_<SLUG>_BASE_URL for each deployed function |
object-storage |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION |
ai-gateway |
NEON_AI_GATEWAY_TOKEN, NEON_AI_GATEWAY_BASE_URL |
-e, --env accepts any variable in the table plus NEON_BRANCH, or NEON_FUNCTION_<SLUG>_BASE_URL where <SLUG> is the function slug uppercased. It is case-sensitive and rejects unknown names rather than silently widening the pull. NEON_BRANCH is written by unscoped and service-scoped pulls because it is branch identity, not a service; an env-only pull writes it only when you select it.
--env never narrows --service: neon env pull -s postgres -e DATABASE_URL still pulls the complete Postgres bundle (DATABASE_URL, DATABASE_URL_UNPOOLED, and NEON_BRANCH). The two selectors always form a union.
A scoped pull is scoped in both directions. An unscoped env pull owns the Neon-named variables: pointing a directory at a branch without Neon Auth prunes the stale NEON_AUTH_* lines, and a branch without those functions prunes stale NEON_FUNCTION_*_BASE_URL lines. --service narrows that to the services you named, while --env narrows it to the exact keys you named, so env pull -e DATABASE_URL never touches DATABASE_URL_UNPOOLED. (AWS_* is never pruned by any pull: those names collide with credentials you may set yourself, so env pull only ever writes them.)
A scoped pull also never revokes a credential. Where an unscoped pull revokes the credential it replaces, a scoped one leaves the old one live — it can't tell which other variables still use it. It says so when it happens; revoke it in the Neon Console if nothing does.
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be selected together. Neon issues them as one object-storage credential; pulling only one would pair a fresh half with whatever old half remains in the file.
Naming a service the branch does not have is an error, not an empty pull:
--service auth: branch br-snowy-frost-12345 has no Neon Auth integration, so there are no
auth env vars to pull. Provision it first (`neon deploy`, `neon config apply`, or the Neon
Console), or drop auth from --service.
link, checkout, and config apply invoke env pull automatically (see above). Those bundled pulls follow rules 2 and 3 above without the implied AI Gateway: minting a credential for a service you never named isn't something a side effect of another command should do. Run neon env pull to get it.
If you'd rather not keep env vars on disk, inject them at runtime instead with neon-env run -- <your dev command> (from @neon/env) or neon dev, and pass --no-env-pull to link / checkout.
neon dev resolves the same set, by the same rules — including the AI Gateway on a branch with no neon.ts. A function running locally gets the same Postgres / Auth / Data API / storage / AI Gateway vars a deploy would inject. Each NEON_FUNCTION_<SLUG>_BASE_URL this process is serving is rewritten to http://localhost:<port>: every neon.ts function, or the matching slug when --source names one. neon env pull and neon-env run write the production URL (https://<branchId>-<slug>.compute.…).
Before neon.ts is evaluated, dev loads the project directory's .env, then .env.local, which overrides it, into the environment, so Function env values written as process.env.X! resolve the same way they do for neon deploy --env. A variable already set in the shell wins over both. Local Functions inherit every loaded variable, including keys neon.ts doesn't declare; the deployed runtime gets only declared and Neon-injected env. --env <file> loads only that file, resolved from the current directory, matching neon deploy --env:
neon dev # loads .env, then .env.local (overrides .env)
neon dev --env .env.staging # loads only .env.staging
dev writes nothing. To reuse the branch credential behind the AI Gateway and object storage, it reads only the file env pull writes (.env when it exists, otherwise .env.local), so an override in the other file cannot pair one credential's key ID with another's secret. Without a file to read from it issues one on every start and leaves the last one live — it has nowhere to keep it, and so cannot name it to revoke it. It says so when it happens; run env pull (or just link / checkout) once and restarts reuse the credential instead.
Where .neon lives: link writes .neon into the current working directory by default. If an existing .neon is found in any parent directory, that file is reused — so commands run from a sub-directory of a linked project still pick up the project's context. To pin the location explicitly, pass --context-file <path>. neon init always writes .neon into the current directory.
The project directory is the directory holding that .neon, or the current directory when there is none. The project's neon.ts and the default .env / .env.local live there and nowhere else: a neon.ts in a parent directory never applies. An explicit --config or --file path still wins.
.gitignore scaffolding: when .neon is created for the first time, the CLI also makes sure a .gitignore sits alongside it listing .neon. If .gitignore doesn't exist it's created with a single .neon line; if it does exist, .neon is appended only when missing (no duplicates, your other entries are left alone). On subsequent updates to an existing .neon, .gitignore is left untouched — so if you deliberately un-ignore .neon (e.g. to commit shared context), the entry is not re-added on every command.
Config as code (config / deploy)
Describe a branch's desired state in a neon.ts policy and reconcile it from the CLI — the Neon equivalent of terraform status / plan / apply. A policy splits into a static existential set — top-level auth / dataApi / aiGateway / functions / buckets / triggers (aiGateway, functions, and buckets still work under deprecated preview) that decide what exists — and a dynamic branch closure that tunes each branch (compute settings, TTL, protection, parent) based on the branch it's evaluated for (name, isDefault, …):
// neon.ts
import { defineConfig } from '@neon/config/v1';
export default defineConfig({
// Static: what exists on every branch (drives the typed env).
auth: true,
// Dynamic: per-branch tuning only — cannot add/remove services.
branch: (branch) => {
if (branch.isDefault) {
return { protected: true };
}
return { parent: 'main', ttl: '7d' };
},
});
Getting a neon.ts (config init)
neon config init scaffolds the policy and installs @neon/config / @neon/env, so a project can go straight to plan / apply. It is purely local — no auth, no API calls. In an interactive terminal it asks which services the policy should declare:
? Which Neon services should neon.ts declare? (space to toggle, enter to confirm) ›
◉ Postgres (always included)
Every Neon project includes a Postgres database.
◯ Managed Better Auth
Authentication with users and sessions stored in Postgres.
◯ Data API
PostgREST-compatible HTTP API. Also declares Auth; the default provider needs it.
◯ Functions
Long-running, without timeouts, and closer to your database.
◯ Object Storage
S3-compatible blob storage that branches with your projects.
◯ AI Gateway
All models, one API, one bill. Powered by Databricks. Not available on the Neon free plan.
Postgres cannot be unselected. Selecting nothing else is a valid answer: you get the starter policy, which is also what a non-interactive run (CI, no TTY) writes. Pass --services to skip the prompt anywhere:
# Pick interactively (TTY) or take the starter policy (CI)
neon config init
# Declare services with no prompt
neon config init --services auth,functions,object-storage,ai-gateway
# Data API also writes auth: true (the default provider requires it)
neon config init --services data-api
# Repeat the flag instead, and shorten it — every services flag takes all three spellings
neon config init -s auth -s functions
# Postgres only: the starter policy, with no prompt
neon config init --services postgres
# Scaffold but print the install command instead of running it
neon config init --no-install
postgres declares nothing, since every branch has Postgres, so it can stand alone or sit next to other services. neon init --services takes the same values.
Object storage is spelled object-storage here, matching env pull --service and the rest of the CLI. The old storage still works and warns; it will be removed.
Choosing Functions also writes the handler the policy points at, since source is only resolved when apply bundles it — a declared function with no file on disk fails at deploy:
// hello.ts
export default async function hello(): Promise<Response> {
return new Response('Hello from Neon Functions');
}
An existing neon.ts (or hello.ts) is never overwritten. To change one, use config add.
Editing a neon.ts (config add)
neon config add declares one service in the neon.ts you already have and leaves the rest of the file (comments, the branch closure, formatting) as it was. It is purely local — no auth, no API calls — and creates the starter neon.ts first when there is none.
neon config add auth # auth: true
neon config add data-api # dataApi: true, and auth: true (the default provider needs it)
neon config add ai-gateway # aiGateway: true
neon config add function sendemail # functions.sendemail + functions/sendemail.ts
neon config add function sendemail --name "Send email" --source ./src/send.ts
neon config add bucket uploads # buckets.uploads = { access: 'private' }
neon config add bucket assets --access public_read
Each command prints what it changed and the next step:
$ neon config add function sendemail
Created functions/sendemail.ts.
Updated neon.ts: added functions.sendemail.
Next: `neon dev` to run it locally, `neon config apply` to deploy.
- Functions take a slug of 1-20 lowercase letters and digits, the same rule
planenforces. The handler file is written first and an existing file is never overwritten, so pointing--sourceat code you already have works. A.jsneon.tsgets a.jshandler. - Enabling a service that
neon.tsalready enables changes nothing and exits 0. Adding a function slug or bucket name that already exists is an error. --config <path>edits a specific file; otherwise the closestneon.tsabove the current directory is used, and the suggestedplan/applycommands repeat the flag.--no-installapplies when the command had to createneon.ts; editing an existing file never installs anything.
config add edits the file as text and checks its own output: it re-applies the edit to the result and fails if that is not a no-op. When the file is in a shape it cannot edit safely (a single-line defineConfig({ auth: true }), a service declared under preview, a value that is not a literal), it changes nothing and prints the exact lines to paste.
Four sub-commands plus two top-level aliases drive it:
# Scaffold a neon.ts and install the config packages (local only)
neon config init
# Inspect the branch's live Neon state (read-only — never mutates)
neon config status
# `neon status` is an alias for `neon config status`
neon status
# Dry-run diff: show exactly what `apply` would change
neon config plan
# Reconcile the policy against the branch
neon config apply
# `neon deploy` is an alias for `neon config apply`
neon deploy
Project & branch resolution follows the same chain as the rest of the CLI, each entry winning over the next:
--project-id <id>flagprojectIdfrom the closest.neonfile (found by walking up from the current directory — see "Where.neonlives" above)- If still unresolved and the API key maps to exactly one project, that project is auto-detected
The branch is chosen with --branch <id|name>; without it the project's default branch is used. The policy is the neon.ts in the project directory (next to .neon, or the current directory without one); pass --config <path> to point at another file.
Confirmations (also on deploy). Two things need a yes before apply changes anything: applying to a branch Neon marks as protected, and overriding settings the branch already has (compute, TTL, protected, Data API settings, custom-domain targets, and disabling an existing Data API). Adding services and deploying functions is not an override, so on an unprotected branch it never asks. In a terminal, apply shows the settings it would override and asks once, default No. Without a terminal (CI, agents, pipes) or with -o json|yaml, it exits 1 before changing anything and names the flag to pass:
--update-existing— override settings that differ fromneon.ts.--allow-protected— apply to a protected branch.-y,--yes— both.
neon deploy --env .env.prod --allow-protected # production main is protected, no drift expected
neon deploy -y # protected branch and drift, no questions
Output: status prints the project, branch, and reverse-engineered config. plan / apply render a git diff-style report (matching neon diff): service changes (Neon Auth, Data API, buckets, functions) list as green + additions, while branch setting changes (TTL, protected, compute) show grouped under a ~ <branch> header, one sorted field → value line each. When apply would override settings already present remotely, it prints them as a sorted before→after diff (current → desired, old in red / new in green) before asking or, without a terminal, before exiting non-zero. Pass --output json (or --output yaml) to emit the full machine-readable result (PushResult) instead, for piping into other tools or CI.
config status --current-branch (alias neon status --current-branch) prints only the branch pinned in the local .neon file — no network, no auth, no analytics — and exits non-zero when none is pinned. This behavior lets it safely drive a shell prompt. Example starship segment:
[custom.neon]
description = "Current Neon database branch"
format = "[$symbol$output]($style) "
style = "bold green"
# `symbol` below uses a Nerd Font glyph; swap it for a plain
# label/emoji if you don't have a Nerd Font installed.
symbol = " "
command = "neon status --current-branch"
# Starship evaluates this on EVERY prompt render. To keep prompts instant
# everywhere outside a Neon project, do a zero-subprocess walk-up for an
# ancestor `.neon` first (the same walk the CLI does, stopping at $HOME and /).
# Only when one is found do we invoke the CLI, whose exit code is the real
# gate: non-zero (no branch pinned) hides the segment cleanly.
when = '''
d="$PWD"
while [ "$d" != "$HOME" ] && [ "$d" != / ]; do
if [ -e "$d/.neon" ]; then
neon status --current-branch >/dev/null 2>&1
exit $?
fi
d=$(dirname "$d")
done
exit 1
'''
# CI gate: fail the build if the branch has drifted from the policy
neon config plan --project-id polished-snowflake-12345678 --output json
# Reconcile a feature branch, overriding any manual tweaks made in the console
neon deploy --branch my-feature --update-existing
Function deploys declared under functions are bundled with esbuild by default. A directory source is discovered as index.ts, then index.js, then index.mjs. Set bundler: "none" to ship a prebuilt directory as-is. neon function deploy --no-bundle is the same switch without a neon.ts.
Point a domain you already own at a function with neon function domains (alias domain):
neon function domains register docs.example.com --slug api
neon function domains list
neon function domains delete docs.example.com
Register prints the record, then an INFO line CNAME <domain> to <cname_target>. The domain goes live after that CNAME resolves and a certificate is issued on the first request.
A neon.ts customDomains list is applied by neon config plan, neon config apply, and neon checkout only on the project's default branch. Other branches still deploy the function. Set customDomains in the branch closure to register a hostname on a child. See @neon/config Default-branch-only fields.
When a package cannot be bundled — a native addon with no esbuild loader, or an optional peer dependency a library references on an untaken code path — list it in that function's externalPackages and the bundler leaves the import alone. neon dev honours the same list. It does not make the package resolvable in the deployed archive (there is no node_modules next to the bundle), so it only unblocks an import that is never evaluated — a dependency the handler actually calls has to be bundled, and a natively-backed one cannot be. See @neon/config.
Scaffold a project (bootstrap)
neon bootstrap copies a Neon starter template into a new (or current) directory — conceptually like degit, but it only pulls from a small set of templates we maintain in the public neondatabase/examples repo. The template copy needs no Neon login: it downloads files from GitHub.
After scaffolding, an interactive terminal asks about dependency install and git, then finishes agent setup before asking whether to link a Neon project. Dependency install is last, except when the template has a neon.ts and you chose to link — then install runs first so the link flow can pull env. --default / -y skips the template, install, git, and agent pickers, then installs agent tooling for project folders, else the host CLI agent. If none are found, it exits: pass --agent <name>, run from a supported agent, or omit --default / -y in a terminal to pick. --agent / -a names coding agents, skips agent selection, and is forwarded to plugins, or to skills and mcp, not both. Linking uses neon link -y: the only organization and project are selected, or their IDs are printed and the command exits. --no-agent-setup and --no-link skip those steps. Non-interactive without --default prints next steps and does not install, set up agents, or link.
Pass a target directory (or . for the current one). In an interactive terminal you pick the template from a list; in CI / non-interactive contexts pass --template <id>.
# Pick a template interactively and scaffold it into ./my-app
$ neon bootstrap my-app
# Scaffold a specific template into the current directory (skips the template picker)
$ neon bootstrap . --template hono
# Skip agent selection; install the plugin for those agents
$ neon bootstrap my-app --agent cursor --agent claude-code
# List templates
$ neon bootstrap --list-templates
# Machine-readable catalog
$ neon bootstrap --list-templates --output json
The target directory must be empty unless you pass --force (a lone .git is ignored, so a freshly git inited folder is fine). Symlinks and executable bits in the template are preserved.
Set up a project (init)
neon init sets up coding agents and this directory for Neon. In a terminal it asks Recommended or Custom. -y is Recommended with no prompts. -y with --skill, MCP flags, --no-agent-setup, or --claimable is Custom using those flags.
Recommended installs the Neon plugin for every detected agent (global config, project folders, and the host CLI). Plugin-capable agents get the plugin; the rest get skills and MCP. If none are detected, it installs the default Neon skills for Cursor and Codex in this directory (./.agents/skills) and does not configure MCP. It then links a project when the CLI is authenticated, and writes a default neon.ts (Postgres only) using the package manager already in the directory.
Unauthenticated -y skips linking and prints the next step: sign up at https://neon.com/signup, then neon login, neon link, or neon claim create (no account, expires in 72 hours unless claimed). -y never opens a browser. -y with --project-id or other account flags links when the CLI is already signed in. Without credentials, --org-id, --project-id, and --branch together write .neon offline (as neon link --no-checks does), and init prints neon login as the next step; Neon checks the IDs on the first command that reaches it. With only some of those flags, init still installs agent tooling and writes neon.ts, then prints neon login and the neon link command with those flags.
When init picks MCP auth (no --mcp-auth) and the CLI credential is an organization or project-scoped API key, which cannot mint the MCP server's key, init configures MCP with OAuth instead and continues. With --mcp-auth api-key, init skips MCP, finishes linking and neon.ts, then reports Neon setup failed. with the neon mcp commands to retry and exits 1.
Custom asks how to add Neon to coding agents (plugin, skills and MCP separately, or skip), then how to get a project (sign in and link, or a claimable project when you are not signed in), then which services neon.ts should declare. Skills and MCP use separate agent pickers, and either can be skipped. Postgres is listed first as always included. The package manager is inferred from the directory; Custom asks only when none is detected. --skill selects skills (not the plugin) and skips that picker. MCP flags (--mcp-config-location, --mcp-auth, --mcp-project-scoped) select skills and MCP. --mcp-config-location is where the config is written: global maps to neon mcp, and project maps to neon mcp --project. With -y, the location defaults to global; interactive Custom asks. If some selected agents cannot write that location, init warns and skips MCP for those agents while continuing with the supported agents. It fails when none of the selected agents support the location. --mcp-project-scoped limits MCP tools to the linked Neon project by passing its ID to neon mcp --project-id. Omitted is false, including in Custom; there is no prompt. --no-agent-setup skips agent setup.
Empty directories are set up in place. neon init does not scaffold starter files.
$ neon init
$ neon init -y
$ neon init --agent cursor --agent claude-code --no-link
$ neon init --no-agent-setup --claimable
$ neon init -y --skill neon --mcp-auth oauth --mcp-config-location project
$ neon init -y --agent cursor --project-id <project-id> \
--mcp-auth oauth --mcp-config-location project --mcp-project-scoped
Without a TTY, pass -y or enough flags to answer every question. -y alone is Recommended. -y with --skill, MCP flags, --no-agent-setup, or --claimable is Custom: those flags, Recommended defaults for the rest. --skill selects skills (not the plugin). MCP flags select skills and MCP. --agent skips agent selection and, without -y, selects Custom. --no-link skips project linking without asking. --no-config skips neon.ts. --services implies creating neon.ts. --org-id, --project-id, --project-name, --region-id, and --branch select linking. With --claimable, --region-id picks the claimable project's region.
--project-id, --org-id, --project-name, --region-id, and --branch are forwarded to link. They select linking. A linked directory is not relinked unless you pass one of those flags.
Init pulls the branch's env vars once, after the neon.ts step, so the pull includes what neon.ts declares. The AI Gateway and Functions need no deploy and are pulled immediately. When neon.ts declares Auth, the Data API, Object Storage, or Realtime, init pulls Postgres, plus the AI Gateway and the scaffolded function's URL when declared. neon deploy provisions the rest and pulls their env vars. An existing neon.ts is pulled by link as usual.
A failed step stops the rest. --profile and --config-dir are forwarded to each child. --output json and --output yaml are refused; the commands init runs print their own output.
skills needs Node.js 22.20 or newer. See plugins, skills, link, and mcp for what those commands write.
Install the Neon MCP server (mcp)
neon mcp writes the hosted Neon MCP server (https://mcp.neon.tech/mcp) into coding-agent config files.
# Interactive: global or project, then agents, then API key or OAuth.
$ neon mcp
# Skip prompts. Global config, installed apps else the host CLI agent, reuse or mint an API key.
$ neon mcp -y
# OAuth: no API key minted. The agent prompts for Neon sign-in on first use.
$ neon mcp --oauth
# Named agents.
$ neon mcp --agent cursor --agent claude-code
# Project-level config. A minted key is still account-wide unless a project is pinned.
$ neon mcp --project
# Hide write tools. Does not change the minted key.
$ neon mcp --read-only
# Pin MCP tools to one project. A newly minted API key is limited to that project.
$ neon mcp --project-id <project-id>
# Limit which tool categories are visible.
$ neon mcp --category querying --category schema
On a TTY the command asks for config location (global is the default), then agents, then API key vs OAuth, then writes. Detected agents start selected: globally installed agents or project-folder markers such as .cursor when the install is project.
-y skips those questions. neon mcp -y writes https://mcp.neon.tech/mcp into global config for globally installed apps, else the host CLI agent, reuses an existing Neon MCP API key or mints an account-wide key, leaves write tools enabled, exposes every tool category, and does not pin a project (including from .neon). --agent, --project, --oauth, --read-only, --project-id and --category still apply with -y. --read-only and --category are flags only and are never prompted. A linked project-folder install asks whether to pin MCP tools to that .neon project (?projectId=). If you pin and selected API-key auth, the minted key is limited to that project too. An unlinked project folder does not ask. Global installs never add that param unless you pass --project-id. Without a TTY, pass -y to mint into every detected agent, --agent <name> to name them, or --oauth to write the URL only. If -y finds no agent, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick. neon mcp --help lists the server URL, those -y defaults, the supported agent names, and the --category values.
Supported agents: antigravity, cline, cline-cli, claude-code, codex, cursor, gemini-cli, goose, github-copilot-cli, grok-build, mcporter, opencode, vscode, windsurf, zed. Project installs drop antigravity, cline, cline-cli, goose and windsurf. claude-desktop is a known name that is then skipped.
The default mints an account-wide API key (or reuses the Bearer already configured for Neon at https://mcp.neon.tech/mcp) and writes it into each selected agent's config. That key reaches everything the account can, in every organization. Revoke it with neon api-keys revoke <id>. --oauth writes the URL with no Authorization header; the agent signs in on first use. --project writes into the project config (.cursor/mcp.json and similar). --read-only adds ?readonly=true. --project-id adds ?projectId= and, when a key is minted, limits that key to the named project. Accepting the linked-project pin does the same. Revoke a project-scoped key with neon api-keys revoke <id> --org-id <org>. A reused Bearer keeps the scope it already has. --category adds ?category= (repeatable or comma-separated: projects, branches, schema, querying, neon_auth, data_api, observability, docs). --read-only and --category restrict which MCP tools the server exposes; they do not change what the minted key can do.
Install Neon agent skills (skills)
neon skills installs Neon agent skills by running npx skills add. It does not call the Neon API. This command needs Node.js 22.20 or newer. The rest of the CLI supports Node.js 20.19 or newer.
# Interactive: this directory, then agents, then skills.
$ neon skills
# Skip prompts. This directory, detected agents (project folders, else the host CLI agent), the default skills.
$ neon skills -y
# Named skills into detected agents.
$ neon skills -y -s neon -s neon-ai-gateway
# Named skills into a named agent.
$ neon skills -s neon -s neon-ai-gateway --agent cursor
# One skill, no prompts.
$ neon skills -s neon-auth --agent cursor -y
# User-level skills.
$ neon skills --global
# Update installed skills in this directory.
$ neon skills update
$ neon skills update -y
$ neon skills update --global -y
# Check whether installed Neon skills are behind their source.
$ neon skills status
$ neon skills status --global
neon skills status compares each Neon skill in this directory's skills-lock.json (or, with --global, the user-level skills lock) with the skill folder on its source repository's default branch, and prints up to date, outdated, or not found upstream per skill. It reads GitHub anonymously; set GITHUB_TOKEN or GH_TOKEN if the GitHub API rate limit is reached. Skills installed through an agent plugin are not in either lock and are not listed.
On a TTY the command asks which agents and which skills, then installs. Detected agents start selected from project-folder markers such as .cursor. Default skills start selected. neon-postgres-agent-platforms is offered and starts unselected.
-y skips those questions and installs the default skills into detected agents: project-folder markers such as .cursor, else the agent driving the CLI. --agent / -a names coding agents and skips the agent picker. --skill / -s names specific skills and skips the skill picker; it does not select agents. --global -y uses installed apps, else the host CLI agent. Without a TTY, pass -y, or --skill <name> (add --agent <name> to name agents). If -y finds no agent, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick.
--skill names by source repo: neondatabase/agent-skills (neon, neon-ai-gateway, neon-auth, neon-functions, neon-object-storage, neon-postgres, neon-postgres-branches, neon-postgres-egress-optimizer, neon-realtime); neondatabase/neon-for-agent-platforms (neon-postgres-agent-platforms).
Supported agents match neon mcp, minus agents that cannot install skills: antigravity, cline, cline-cli, claude-code, claude-desktop, codex, cursor, gemini-cli, goose, github-copilot-cli, grok-build, opencode, vscode, windsurf, zed. mcporter is a known MCP name that is then skipped. neon skills --help lists the same skill and agent values, and that -y leaves out neon-postgres-agent-platforms.
Ask the Neon assistant (ask)
neon ask --prompt asks the hosted Neon assistant a question about Neon. It does not log in and does not use your Neon account.
neon ask --prompt "How do schema-only branches work?"
neon ask --prompt "How do schema-only branches work?" --output json
Default table output is the assistant's text. On a TTY that is a spinner, then the streamed reply. --output json and --output yaml print { "text": "…" } after the full response.
Send feedback (feedback)
neon feedback --message sends feedback to Neon. It does not log in, and it sends only your message, not your account, project, or branch.
neon feedback --message "The branch docs were unclear"
neon feedback --message "The branch docs were unclear" --output json
Default table output is Feedback received. Thank you!. --output json and --output yaml print { "received": true }. Messages are limited to 10,000 characters. Do not include passwords, API keys, connection strings, or other secrets.
Search neon.com (docs search)
neon docs search <query> searches neon.com: the docs, API reference, PostgreSQL tutorial, guides, FAQs, changelog, and blog. It does not log in and does not use your Neon account. It uses the same search as the neon.com search box. The search service records the query, the CLI version, and the detected coding agent; --no-analytics turns that off.
neon docs search "reset a branch"
neon docs search "create branch" --category api --category docs
neon docs search "pg_dump" --limit 5 --output json
--category limits results to docs, api, postgresql, guides, faqs, blog, or changelog; repeat it for more than one. --limit is 1 to 40, default 10. Default output is one block per result: Title › Section on one line, cut to the terminal width, and the full URL under it. --output json and --output yaml print an array of hits with title, url, category, breadcrumb, heading, and excerpt, in rank order. No match prints No results for "<query>". and exits 0 ([] in JSON).
Install the Neon plugin (plugins)
neon plugins installs the Neon agent plugin (neon-postgres) by running npx plugins add. It does not call the Neon API.
# Interactive: pick agents, then install.
$ neon plugins
# Skip prompts. Detected agents (project folders, else the host CLI agent).
$ neon plugins -y
# Named agents.
$ neon plugins --agent cursor --agent claude-code
# User-level install.
$ neon plugins --global
On a TTY the command asks which agents, then installs. Detected agents start selected from project-folder markers such as .cursor. There is one plugin (neon-postgres); there is no plugin picker and no update subcommand.
-y skips those questions and installs into detected agents: project-folder markers such as .cursor, else the agent driving the CLI. --agent / -a names coding agents and skips the agent picker. --global -y uses installed apps, else the host CLI agent. Without a TTY, pass -y or --agent <name>. If -y finds no agent, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick.
Default scope is project. --global is user. On macOS and Linux, Cursor and Claude Code store the plugin cache under ~/.claude/plugins; on Windows, Cursor installs into Cursor extensions. project vs user is the scope field the plugins CLI records, not a directory in the repo. VS Code, GitHub Copilot CLI, and Grok Build only install user-level: they are skipped at the default scope with a warning, and a VS Code-only project fails if nothing else is selected. Pass --global for those.
Supported agents with a plugins mapping: claude-code, claude-desktop, codex, cursor, github-copilot-cli, grok-build, vscode. claude-desktop installs as Claude Code; detecting both produces one install and lists both names in the table. mcporter is a known MCP name that is then skipped.
The plugins CLI installs every plugin it finds in the Neon plugin package. Today that is neon-postgres from neondatabase/agent-skills. It includes the Neon MCP server (https://mcp.neon.tech/mcp) and these skills: neon, neon-ai-gateway, neon-auth, neon-functions, neon-object-storage, neon-postgres, neon-postgres-branches, neon-postgres-egress-optimizer, neon-realtime. It does not include neon-postgres-agent-platforms.
neon plugins --help lists the plugin, those contents, and the supported agent names.
Snapshots (snapshots)
neon snapshots (alias neon snapshot) manages snapshots — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
Every sub-command resolves the project through the standard chain (--project-id, then the .neon context file, then a single-project auto-detect). Branch-scoped sub-commands (create, schedule) default to the branch pinned in .neon, falling back to the project's default branch, and accept --branch <id|name>. The get, update, delete, and restore sub-commands take a snapshot id or name as their positional argument (an id wins; an ambiguous name errors and asks you to use the id).
# Snapshot the head of the current/default branch
neon snapshots create --name pre-migration
# Snapshot a specific branch at a point in time (RFC 3339 timestamp OR LSN — mutually exclusive)
neon snapshots create --branch main --timestamp 2025-01-01T00:00:00Z
neon snapshots create --branch main --lsn 0/1F3C8A0 --expires-at 2025-12-31T23:59:59Z
# List / inspect
neon snapshots list
neon snapshots get pre-migration
# Rename or change expiration (omit both to error; --expires-at and --clear-expiration conflict)
neon snapshots update snap-1234 --name nightly
neon snapshots update snap-1234 --expires-at 2030-01-01T00:00:00Z
neon snapshots update snap-1234 --clear-expiration # keep indefinitely
# Restore a snapshot to a NEW branch
neon snapshots restore snap-1234 --name recovered
# Restore ONTO an existing branch. Without --finalize the restore is left un-finalized
# so you can inspect it first, then swap it in:
neon snapshots restore snap-1234 --target-branch main
neon snapshots finalize br-restored-1234 # commit the swap
# …or do it in one step:
neon snapshots restore snap-1234 --target-branch main --finalize
# Delete
neon snapshots delete snap-1234
# Automatic snapshot (backup) schedule of a branch
neon snapshots schedule get --branch main
neon snapshots schedule set --branch main --frequency daily --hour 3 --retention 604800
neon snapshots schedule set --branch main --schedule '[{"frequency":"weekly","day":1,"hour":2},{"frequency":"daily","hour":3}]'
All sub-commands honor the global options, including --output json|yaml|table.
Function templates (functions new)
neon functions new <template> fetches a template from the Neon Function Registry, writes its modules into functions/<slug>/ next to neon.ts, and declares the result in neon.ts (creating it when missing), the same way config add function does.
neon functions templates list
neon functions new
neon functions new basic
neon functions new basic --slug hello --no-add-to-config
neon functions new <template> --operation <id> --operation <id> --install
Without a template, a terminal asks which one to use; anything else exits with the available IDs. Like bootstrap, it asks once whether to install the template's dependencies (plus @neon/config and @neon/env when it creates neon.ts): --yes installs, --no-install skips, and a run without a terminal leaves the install command under Next:.
A template's layout decides what you get. router writes one module per operation plus a generated index.ts that dispatches on the URL path, and declares one function. separate declares each operation as its own function under the slug the template gives it. Without --operation or --all-operations, a TTY asks which operations to include and anything else uses the template's recommended ones.
Variables a template needs are declared on each function in neon.ts (env: { API_KEY: process.env.API_KEY! }) and added, without values, to .env.example next to it. Values are never written to .env or .env.local for you: a blank placeholder there would deploy as an empty string and delete the live value. Put the real value in the file env pull uses and deploy with neon config apply --env .env.local. neon dev reads function env from your shell, so export it there as well.
Nothing is written until every check passes: a file that already exists with different contents needs --force, and a slug neon.ts already declares needs --slug, --force, or --no-add-to-config. A separate template sets its own slugs, so --slug only changes its directory; remove the existing declarations from neon.ts, or use --force or --no-add-to-config.
The registry lives in registry/ in neondatabase/examples. The CLI reads the index from https://neon.com/functions/registry/registry.json, which serves main through a CDN cache, and falls back to raw GitHub when neon.com is unreachable. Template files are always read from raw GitHub. Index and template JSON are validated against the schemas at https://neon.com/functions/registry/schemas/ (same GitHub fallback). Set NEON_FUNCTION_REGISTRY_DIR to a local checkout to try templates before they are merged, or NEON_FUNCTION_REGISTRY_URL to read an index from somewhere else.
Function triggers (triggers)
neon triggers (alias neon trigger) manages Function triggers on a branch. Types are schedule (a five-field UTC cron expression) and storage_object_created (an object-storage bucket, with an optional object-key prefix).
Every sub-command resolves the project through the standard chain (--project-id, then the .neon context file, then a single-project auto-detect) and the branch through --branch <id|name>, the .neon pin, or the project's default branch.
neon triggers create --function-slug uptime --name uptime-check --cron '*/15 * * * *'
neon triggers create --function-slug ingest --name on-upload --bucket assets --prefix 'logos/' --function-path /object
neon triggers list
neon triggers get trigger-test-123
neon triggers update trigger-test-123 --cron '0 3 * * *'
neon triggers update trigger-storage-123 --bucket assets --prefix 'incoming/'
neon triggers enable trigger-test-123
neon triggers disable trigger-test-123
neon triggers delete trigger-test-123
enable / disable are wrappers over update --enabled. List shows inherited when the effective config was authored on an ancestor branch. create takes --cron or --bucket, not both.
Branch credentials (credentials)
neon credentials (alias neon credential) lists, issues, reveals, rotates, and revokes branch-scoped credentials — the tokens behind Object Storage (AWS_*) and the AI Gateway (NEON_AI_GATEWAY_TOKEN). Beta. In regions where those services exist, a new project already has default credentials named Default AI gateway credential and Default object storage credential; list then reveal recovers their secrets without minting another token.
neon credentials list
neon credentials create --name app --scope storage:read --scope storage:write
neon credentials reveal nak_live_…
neon credentials rotate nak_live_…
neon credentials revoke nak_live_…
create always issues a customer-managed (user) credential. --scope is repeatable; --help lists the values. reveal and rotate print api_token and s3_secret_access_key. Rotation keeps the same token_id (it is the AWS_ACCESS_KEY_ID) and is not idempotent: a retry after a lost response mints another secret.
All sub-commands honor the global options, including --output json|yaml|table.
Database diagnostics (inspect)
neon inspect db stalled-queries takes a read-only snapshot of active queries that have run for more than 30 seconds and groups parallel workers with their leader. Oldest group first. Table output shows duration, wait event, blocking pids, role, query group, and query. --output json adds timestamps, query IDs, pids, database, and the rest of the row. A blocking pid can belong to an idle-in-transaction backend this command does not list; neon inspect db locks shows lock holders.
neon inspect db stalled-queries
neon inspect db stalled-queries --output json
Logs (logs)
neon logs reads the log records the services on a branch emit — Neon Functions, object storage, and Postgres computes. Logs are currently available only for projects in aws-us-east-2, aws-us-east-1, aws-eu-central-1, and aws-ap-southeast-1.
Every sub-command resolves the project through the standard chain (--project-id, then the .neon context file, then a single-project auto-detect), and takes --branch <id|name>, defaulting to the project's default branch. logs query searches the previous hour by default; logs field-values searches the previous six hours. The maximum time window is seven days.
# The last 30 minutes on the default branch
neon logs query --since 30m
# Postgres compute errors on main, oldest first
neon logs query --branch main --source pg_endpoint --minimum-severity error --sort-order asc
# An explicit window (--start-time replaces --since; --end-time works with either)
neon logs query --start-time 2025-01-01T00:00:00Z --end-time 2025-01-01T01:00:00Z
# One request trace, across every service that took part in it
neon logs query --trace-id 4bf92f3577b34da6a3ce929d0e0e4736
# What the structured filters cannot express: a raw LogQL selection. It replaces
# them, but the window, --limit, --sort-order and --cursor still apply.
neon logs query --since 1h --logql '{entity_type="function"} |= "timeout"'
# Which fields this branch supports, and the values it has actually seen
neon logs fields
neon logs field-values service_name --since 6h --source function
A response holds at most --limit records (1–1000; default 100). When more matched, table output prints the --cursor to repeat the same query with; --output json|yaml returns is_truncated and next_cursor on the envelope instead, so nothing but the payload lands on stdout. Table output shows the common fields; use structured output for the complete records:
$ neon logs query --since 24h --output json
{
"logs": [
{
"timestamp": "2025-01-01T00:00:02.000Z",
"message": "GET /api/todos 200",
"source": "function",
"service_name": "api",
"severity_text": "INFO",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"attributes": { "http_status": 200 }
}
],
"next_cursor": "eyJvZmZzZXQiOjEwMH0",
"is_truncated": true
}
Two combinations are rejected before the request: --since with --start-time, and --logql with any of --source, --service-name, --scope-name, --minimum-severity, --severity-text, --body-contains or --trace-id. --minimum-severity and --severity-text are independent filters and combine with AND. If Neon reports that --minimum-severity is unsupported, use --severity-text instead; neon logs field-values severity_text lists the exact values present on a branch.
--body-contains compares a case-sensitive substring against the rendered message. Structured bodies, including object storage records, are rendered as compact JSON, so match the JSON form (for example, "http_status":200).
Profiles
The CLI holds one Neon account by default. A profile adds another, and is a pointer: a
credentials file path, or the sentinel "keyring" when the secret is in the OS keyring.
~/.config/neon/
├── credentials.json # this IS the DEFAULT profile
├── credentials.work.json # created by `neon profile create work`
└── profiles.json # created once a second profile exists, or DEFAULT is keyring
neon profile create work # a browser sign-in, or an API key — see below
neon profile create work --keyring # same, stored in the OS keyring
neon profile list
neon profile remove work
$ neon profile list
Profiles
Active Name Account Auth Credentials Scope
* DEFAULT me@example.com oauth credentials.json account
work me@example.com api key keyring account
ci org-abc-123 api key credentials.ci.json project proj-1
Scope is what the credential can reach. An OAuth session and an unscoped user API key both
show account. A project or org key names that project or org. Credentials is the path
relative to the config directory, or keyring. A path outside the config directory is shown
absolute. --output json keeps the absolute path, and also includes file (ok, invalid,
missing, or unreadable) and storage (file or keyring). A keyring get of null is
unreadable, not missing: the addon cannot tell those apart.
Select one per invocation with --profile, or per shell with NEON_PROFILE. There is no profile use command and nothing is stored about which profile is "current", so what you type is always what runs.
Entries in profiles.json are pointers — a path, or the exact string "keyring" — and a path may point anywhere, which is how you adopt a directory you already have without moving or re-authenticating anything:
{
"version": 1,
"profiles": {
"DEFAULT": { "credentials": "keyring" },
"work": { "credentials": "../neonctl-work/credentials.json" }
}
}
An unreadable profiles.json fails every command that needs a profile, including a
file-only DEFAULT. That file is the only record of where each account's credentials live,
so the CLI will not guess past it. Fix or delete it.
neon profile remove revokes what the profile holds — an OAuth refresh token at the
authorization server, or an API key this CLI minted — rather than only forgetting it locally. A
key you supplied is the exception and stays live, because nothing records its id; the command
says so. It asks for confirmation first, which --yes skips; without a terminal on stdin, in
CI or behind a pipe, it refuses rather than prompting into the void. It deletes the credentials
file only when the CLI created it: an adopted path like the one above is unlinked and left on
disk, and the command says so. A keyring profile's OS item is deleted when the store confirms
it is gone. If it cannot, remove still resets the profile and warns that a leftover may remain
in the OS store; it is unused once the profile is gone. Removing the last named profile
deletes profiles.json unless DEFAULT itself is keyring — that entry is the only record that
the secret is not in credentials.json. neon profile remove DEFAULT signs you out.
Where the secret is stored
The default is a credentials file. A profile uses the OS keyring (macOS Keychain, Windows
Credential Manager, Linux Secret Service) only when its profiles.json pointer is "keyring".
That is per profile. Reads never migrate.
neon login --keyring # sign DEFAULT into the OS keyring
neon login --keyring --profile work # sign work into the OS keyring
neon profile create work --keyring # create a named profile in the keyring
neon profile remove work --yes # drop a keyring profile, then create it again as a file
File to keyring is neon login --keyring or neon profile create … --keyring: a new
sign-in, then the previous credential is revoked and the owned file is deleted.
create on an existing name always revokes after a successful write. login revokes
when it writes to the keyring, including a re-login that follows an existing pointer.
login that overwrites a file does not. Keyring to file is remove, then create or
log in again. create and login without --keyring follow an existing "keyring"
pointer, so a keyring profile cannot leave the OS store until remove succeeds.
--api-key and NEON_API_KEY skip both stores. A GitHub-release neon-<platform>
binary recognizes a "keyring" pointer and refuses: it cannot load the OS keyring addon.
Use the npm-installed neon. Older releases treat the sentinel as a relative path.
A "keyring" pointer whose OS item cannot be read is not treated as signed-out. Commands that
would otherwise open a browser fail: could not read the OS keyring item. Unlock it and
retry, or run neon login --profile DEFAULT. To reset the profile: neon profile remove DEFAULT --yes.
A missing credentials.json with no profiles.json is still
signed-out, and those commands start OAuth.
A profile holds either a sign-in or an API key
neon profile create makes a profile, and how you call it decides which kind of credential it holds. A key-backed profile is what you want for an agent, a shared machine, or anything that must never be interrupted by a browser:
neon profile create work # sign in; replaces work if it already exists
neon profile create work --keyring # same, stored in the OS keyring
neon profile create work --api-key "$KEY" # store a key you already have
echo "$KEY" | neon profile create work --api-key - # or pipe it, keeping it out of argv
neon profile create ci --mint # sign in once, keep only a minted key
neon profile create ci --mint --org-id org-abc-123 # minted for an organization
neon profile create ci --mint --project-id proj-1 # minted for one project only
neon profile rotate-key work # mint a replacement, revoke the old one
Replacing a profile revokes the credential it held, so a key this CLI minted stops working
everywhere it was pasted, and an OAuth session is signed out. To keep a working profile and
swap only its key, use rotate-key.
create and rotate-key print the profile they wrote, so an agent needn't follow up with
list. Under --output json that is a record, and it never carries the secret:
$ neon profile create ci --mint --org-id org-abc-123 --output json
{"name":"ci","account":"org-abc-123","auth":"api key","scope":"org org-abc-123","keyId":3239771,"credentials":"/home/me/.config/neon/credentials.ci.json"}
One flag takes the key, because the shell already covers the variations: --api-key "$(cat ~/keys/work)" reads a file and --api-key "$KEY" takes a variable. Those put the key in the
process arguments, where ps and shell history can see it, so --api-key - reads it from stdin
instead — the usual convention for a piped value. --mint avoids the question entirely, because
the key never leaves the CLI.
A profile is one kind or the other, never both. type in the credentials file states which:
// oauth: what a plain `create` (or `neon login --profile`) writes. An absent `type` means this.
{ "access_token": "…", "refresh_token": "…", "expires_at": 1786…, "user_id": "…" }
// api_key: what `--api-key` writes
{ "type": "api_key", "api_key": "napi_…", "user_id": "…" }
// api_key from `--mint --org-id`, which records the scope it was issued at
{ "type": "api_key", "api_key": "napi_…", "key_id": 123, "org_id": "org-…" }
Nothing is carried over when a profile is replaced, so one profile can never hold two credentials — or two different accounts. The secret stays in the credentials file or the OS keyring and never goes into profiles.json, so listing profiles cannot leak one. Files are written owner-only through a temporary file and a rename, which also repairs the permissions of a file created too permissively.
--mint is the one to reach for. It signs you in through the browser once, mints a key with that session, stores only the key, and signs the session back out — so afterwards nothing about the profile can open a browser, and no half-forgotten login is left behind. --org-id and --project-id narrow what the minted key can reach, exactly as they do on neon api-keys create; a project-scoped key cannot create projects, mint keys, or read any other project.
Every key is verified against the API before it is stored, and the account it belongs to is recorded so profile list can show it. Only a real API key is accepted: an OAuth access token authenticates today and then expires with nothing to refresh it.
rotate-key mints at the scope the profile already has — replacing an org key with an account key would quietly widen everything it reaches — and stores the new key before revoking the old one, so a failed write leaves the old key working.
One thing it cannot do: an organization key cannot mint its own replacement. Neon only accepts a personal credential when creating organization keys, so rotating an org- or project-scoped profile means signing in again — neon profile create ci --mint --org-id org-abc-123. rotate-key checks this before minting and says so, rather than letting the API answer with a rule you had no reason to expect.
Two things the CLI cannot do for a key you supplied rather than minted. It cannot revoke it, because GET /api_keys exposes no prefix and a stored secret cannot be matched to a listing entry, so both rotate-key and profile remove say the old key is still live and point you at neon api-keys list. For a key you supplied it records the organization the API reports, but cannot know whether that key was narrowed to a single project — so rotate-key will not suggest an organization-wide replacement without telling you to check neon api-keys list first.
If a stored key stops working there is nothing to refresh, so recovery is one browser sign-in: neon profile create work --mint.
Which credential an invocation uses
An explicit flag always beats an environment variable:
| Given | What runs |
|---|---|
--api-key and --profile |
neither — contradictory, so the command fails |
--api-key and NEON_PROFILE |
the flag's key |
--profile and NEON_API_KEY |
the profile |
NEON_API_KEY and NEON_PROFILE |
the key, and the ignored profile is named in a warning |
--profile or NEON_PROFILE alone |
that profile |
| nothing | DEFAULT |
Passing both flags fails rather than picking a winner: --api-key supplies a credential and --profile selects a stored one, so there is no reading of the command that makes both true.
When both are only environment variables the key wins, which keeps a CI pipeline that injects NEON_API_KEY working even if a NEON_PROFILE leaks into the environment — but the disregarded profile is named on stderr rather than passed over silently.
neon login and the profile subcommands are outside all of this, because they read the same flags to mean something else: neon login --profile work names where to write a credential, and neon profile create work --api-key … names one to store.
neon init forwards --profile and --config-dir to agent and config children and uses them directly for its link flow. An explicit --api-key is passed to authenticated children through NEON_API_KEY, not argv.
API passthrough (api)
neon api sends an authenticated request to any Neon API route. neon api --list catalogs the routes. neon api <path> --describe prints the OpenAPI request shape for one operation so you can fill -F and -Q without guessing. Body field names are dotted to match -F.
neon api --list
neon api /projects --describe
neon api /projects -X POST --describe -o json
neon api /projects/{project_id}/branches -X POST --describe
neon api /projects/foo-bar-123/branches -X POST -F branch.name=dev
--describe does not send the selected API request. It uses the same CLI authentication as --list. -X defaults to GET; if that method is missing, the error names the methods that exist.
API keys (api-keys)
neon api-keys list # your account's keys
neon api-keys list --org-id org-… # an organization's, with scope shown
neon api-keys create --name ci # account key
neon api-keys create --name ci --org-id org-… # organization key
neon api-keys create --name agent --project-id frosty-… # can access only that project
neon api-keys revoke <id> [--org-id org-…]
The key is returned once, on create, and cannot be retrieved again. It prints on its own line below the metadata, so it can be selected in one gesture regardless of terminal width — and … | tail -1 on stdout yields exactly the key, since both notices go to stderr.
$ neon api-keys create --name agent --project-id proj-in-org
API key
Id 303
Name agent
Project proj-in-org
napi_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
WARNING: Store this key now: it is not shown again.
Limited to proj-in-org: it cannot create projects, mint API keys, or read any other project. It can still change and delete everything inside that project.
--org-id and --project-id are mutually exclusive. A project-scoped key is an organization key, and its organization is looked up from the project rather than chosen separately. With neither flag you get an account key.
Project-scoped keys
A key created with --project-id bounds what it can reach to one project. Verified against a real scoped key:
| Attempt | Result |
|---|---|
| Read and write its own project | works |
| Read any other project | project not found — not even an existence oracle |
neon projects create |
project-scoped keys are not allowed to create projects |
neon projects list |
refused |
neon api-keys create / list |
refused (true of any organization key, not only scoped ones) |
neon orgs list |
works — it can see the id, name and handle of the organization it belongs to |
Anything else about that organization (GET /organizations/{id}, members) |
refused |
It is not read-only. Inside its one project it can do everything the API allows, including deleting branches and the project itself — neon deploy working at all is proof of that. What it bounds is reach, which is what lets you hand it to an agent or a CI job without handing over your account:
neon link --project-id frosty-… # once, as yourself — writes .neon
NEON_API_KEY=napi_… neon deploy # then the agent, reaching only that project
neon link needs --project-id explicitly when using a scoped key: the interactive picker lists your projects, which a scoped key cannot do.
api-keys deliberately ignores the .neon context file, unlike every other project command. Otherwise neon api-keys create --name ci inside a linked directory would silently mint a key scoped to that project instead of the account key you asked for. How far a credential reaches comes only from a flag you typed.
Seeing what is scoped
$ neon api-keys list --org-id org-7
API keys in org-7
Id Name Project Created At Last Used At Last Used From Addr
301 scoped proj-in-org 2026-01-02T00:00:00Z
302 org-wide (all projects) 2026-01-03T00:00:00Z 2026-02-03T00:00:00Z 203.0.113.9
last_used_at and last_used_from_addr are how you spot a key worth revoking.
Commands
| Command | Subcommands | Description |
|---|---|---|
login (auth) |
Authenticate | |
claim (claimable) |
create, status, accept, list, delete |
Manage claimable projects |
| profile | list, create, rotate-key, remove |
Manage named sets of credentials |
| api-keys | list, create, revoke |
Manage API keys |
| api | Call any Neon API route | |
| projects | list, create, update, delete, get |
Manage projects |
| ip-allow | list, add, remove, reset |
Manage IP Allow |
| me | Show current user | |
| branches | list, create, rename, add-compute, set-default, set-expiration, delete, get |
Manage branches |
| databases | list, create, delete |
Manage databases |
| function | deploy, list, get, delete, domains list, domains register, domains delete |
Manage Neon Functions |
| roles | list, create, delete |
Manage roles |
| operations | list |
Manage operations |
| logs | query, fields, field-values |
Query branch logs (Beta) |
| inspect | db stalled-queries |
Inspect Postgres diagnostics |
| snapshots | list, get, create, update, delete, restore, finalize, schedule get, schedule set |
Manage snapshots |
| triggers | list, get, create, update, enable, disable, delete |
Manage function triggers |
| credentials | list, create, reveal, rotate, revoke |
Manage branch credentials |
| connection-string | Get connection string | |
| psql | Connect to a database via psql | |
| set-context | Deprecated; use link |
|
| env | pull |
Manage a branch's env vars |
| checkout | Pin a branch in .neon |
|
| diff | Git-style schema diff vs a branch | |
| link | Link a directory to a project | |
| open | Open the linked project in Console | |
| config | init, status, plan, apply |
Drive a branch from neon.ts |
| deploy | Alias for config apply |
|
| bootstrap | Scaffold a template, then agent tooling and link | |
| init | In place: agents, link, neon.ts | |
| mcp | Install the Neon MCP server | |
| plugins | Install the Neon plugin | |
| skills | update |
Install Neon agent skills |
| ask | Ask a question about Neon | |
| feedback | Send feedback to Neon | |
| bucket | create, list, delete, object list, object get, object put, object delete (incl. --recursive) |
Manage buckets and their objects |
| completion | Generate a completion script |
Global options
Global options are supported with any Neon CLI command.
| Option | Description | Type | Default |
|---|---|---|---|
| -o, --output | Set the Neon CLI output format (json, yaml, or table) |
string | table |
| --config-dir | Path to the Neon CLI configuration directory | string | /home/<user>/.config/neon |
| --profile | Named credentials to use, from profiles.json |
string | DEFAULT |
| --api-key | Neon API key | string | "" |
| --analytics | Manage analytics | boolean | true |
| -v, --version | Show the Neon CLI version number | boolean | - |
| -h, --help | Show the Neon CLI help | boolean | - |
-
Sets the output format. Supported options are
json,yaml, andtable. The default istable. Table output may be limited. Thejsonandyamloutput formats show all data.neon me --output json -
Specifies the path to the
neonconfiguration directory, which holds thecredentials.jsonwritten byneon loginand, when a second profile exists or DEFAULT is keyring,profiles.json. The default is$XDG_CONFIG_HOME/neon, or~/.config/neon; runneon --helpto see the resolved path. This option is only necessary if you keep your configuration somewhere else.The directory was called
neonctlbefore the CLI was renamed. An existing one is still read, and is used in place — nothing is moved or copied, so there is never a second credentials file to go stale. A directory you pass explicitly is used exactly as given and never falls back to the legacy name, so pointing a CI run at a scratch directory cannot pick up local credentials.neon projects list --config-dir /home/dtprice/.config/neon -
Selects a named set of credentials, for holding more than one Neon account at a time. A profile is a pointer to a credentials file, recorded in
profiles.jsonnext to it.neon login --profile work # create it, or sign in again neon profile list # names, accounts, and where each one's credentials live neon projects list --profile work NEON_PROFILE=work neon projects listPrecedence is
--profile, thenNEON_PROFILE, thenDEFAULT.DEFAULTis plaincredentials.json, so an install with a single account needs noprofiles.jsonand behaves exactly as before. See Profiles to list or remove them. -
Specifies your Neon API key. You can authenticate using a Neon API key when running a Neon CLI command instead of using
neon login. For information about obtaining an Neon API key, see Authentication, in the Neon API Reference.neon <command> --api-key <neon_api_key> -
Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to
neon ask --promptand the query passed toneon docs search. Each successful command also reports the Neon agent skills recorded in the user-level skills lock and in the current directory'sskills-lock.json: the skill names, their lock hashes, and whether each is behind its source, read from local files without a network request. The Neon search service records eachneon docs searchquery with the CLI version and the detected coding agent; with--no-analyticsit records nothing. When the CLI fails on an internal bug (a JavaScriptTypeError,ReferenceError,RangeError, orSyntaxError), it also sends that error to Neon's Sentry: the error type, the message only when it is a standard JavaScript message that names nothing but code (any other message is replaced), the stack trace with file paths cut to the package (neon/dist/...), the CLI and Node.js versions, and whether it ran in CI or a coding agent. To opt-out of analytics data collection and error reports, specify--no-analyticsor--analytics false. -
Shows the Neon CLI version number.
$ neon --version 1.15.0 -
Shows the
neoncommand-line help. You can view help forneon, aneoncommand, or aneonsubcommand, as shown in the following examples:neon --help neon branches --help neon branches create --help
Contribute
This repo uses pnpm. The required version is pinned in .tool-versions and package.json's packageManager field. The simplest way to get the right version is mise: mise install reads .tool-versions and installs Node and pnpm. Alternatives: npm install -g pnpm@9.15.9, or Corepack (corepack enable pnpm).
To run the CLI locally, execute the build command after making changes:
pnpm install
pnpm run build
To develop continuously:
pnpm run watch
To run commands from the local build, replace the neon command with node dist; for example:
node dist branches --help
Embedded psql tests
The embedded TypeScript psql implementation has its own conformance test suite that runs the same scripts against the embedded psql and a reference psql binary, then diffs the output.
bun run test:conformance # run against $PSQL_BINARY (defaults to the system psql)
bun run test:conformance:matrix # run across PG 14/15/16/17/18 locally (requires Docker)
Releasing
Maintainers: see the repository's release skill for the Changesets bump and external publish flow.