Skip to content

Migrating to Supabase CLI v3

Breaking changes between Supabase CLI v2 and v3, and what to change in your scripts, CI, and config.

Supabase CLI v3 is a major release. Most of its breaking changes fall into five areas:

  • Database commands target your local stack by default. Add --remote to reach your hosted project.
  • Commands that change a hosted project ask for confirmation. Unattended runs, such as CI, need --yes.
  • Flags and environment variables override config.toml, including [remotes.<name>] blocks.
  • pg-delta is the default schema diff engine.
  • Deprecated commands, flags, config keys, and environment variables are removed.

Two smaller changes can affect a production project: config push now applies [auth.sms] enable_confirmations as written, and secrets unset no longer defaults to yes.

A removed command or flag still parses: it exits with code 1 and prints its replacement, so a failing run tells you what to change.

Before you upgrade#

  1. Update to the latest v2 release. v2.122.0 and later warn about removed commands, flags, config keys, and environment variables, and about commands whose default target changes in v3. Run your usual workflows, including CI and deploy scripts, and fix each warning. v2 doesn't warn about the confirmation, config precedence, diff engine, or SMS changes, so read those sections too.
  2. Stop your local stack with supabase stop if supabase/config.toml has no project_id. Local container and volume names can change in v3. See Local container names.

Installing v3#

Every install channel (npm, Homebrew, Scoop, and the Linux packages) installs v3 by default. See Install and run the CLI for each one. If your project installs the CLI from npm:

npm install supabase@latest --save-dev

GitHub Actions#

supabase/setup-cli installs latest, which is now v3, when you don't set version, unless a bun.lock, pnpm-lock.yaml, or package-lock.json at the root of your repository pins supabase. Pin v2 to upgrade on your own schedule:

steps:
- uses: supabase/setup-cli@v3
with:
version: v2.stable

From supabase/setup-cli v3.2.0, version also accepts an npm semver range, such as ^2 or ~2.122.0. The action installs npm's latest version if it matches the range, and otherwise the highest matching version that isn't deprecated. v2.stable and ranges need supabase/setup-cli@v3. Moving the action to @v3 doesn't upgrade the CLI while version pins v2.

During the transition, some commands work the same way on both majors. For example, supabase db push --linked --yes runs against the linked project on v2 and v3. v3 warns about --linked, which you can switch to --remote once you've upgraded.

Staying on v2#

v2 continues to receive security fixes and fixes for broken behavior, but no new features. Install v2 from these channels:

ChannelCommand
npmnpm install supabase@v2.stable --save-dev
Homebrewbrew install supabase/tap/supabase@2
Scoopscoop install supabase-v2-stable
GitHub Actionsversion: v2.stable or version: ^2 on supabase/setup-cli@v3

The supabase and supabase@2 Homebrew formulas conflict, so if you installed v3 with Homebrew, run brew uninstall supabase before you install supabase@2.

Quick reference for CI#

v2v3
supabase db pushsupabase db push --yes --remote
supabase db push --db-url "$DB_URL"supabase db push --yes --remote="$DB_URL"
supabase migration list --linkedsupabase migration list --remote
supabase gen types typescript --project-id "$REF"supabase gen types typescript --remote="$REF"
supabase gen types typescript --localsupabase gen types typescript
supabase db dump --linked -f schema.sqlsupabase db dump -f schema.sql --remote
supabase functions deploysupabase functions deploy --yes
supabase secrets set --env-file .env.productionsupabase secrets set --env-file .env.production --yes
supabase config pushsupabase config push --yes
echo y | supabase <command>supabase <command> --yes

The rest of this guide explains each change.

Database commands target the local stack by default#

In v2, some database commands defaulted to the linked project and others to the local stack. In v3, every command that can run against the local stack does so unless you pass --remote:

  • db push, db pull, and db dump
  • migration fetch, migration list, and migration repair
  • inspect db * and inspect report
  • gen types
  • storage ls, storage cp, storage mv, and storage rm

Use --remote to choose a hosted target:

You passTarget
NothingThe local stack
--remoteThe linked project
--remote=<project-ref>That project
--remote=<branch>A branch of the linked project, by name or ID
--remote=<postgres-url>That database, as a postgres:// or postgresql:// URL. Database commands only.

Commands that only work against a hosted project, such as functions deploy, secrets set, and config push, still default to the linked project. Pass --remote=<project-ref> or --remote=<branch> to point them at another project or branch.

SUPABASE_PROJECT_ID no longer selects a hosted project on its own. It now only sets which project a bare --remote means.

See Which project a command runs against for how the CLI picks a target.

Use = with --remote#

--remote works on its own, so a value must be attached with =: write --remote=<branch>, not --remote <branch>. When a word after a bare --remote could be read either way, the CLI fails with a "did you mean" error instead of guessing. To decide, it checks the word against the branches of the linked project, so the run also fails if it can't list them, for example without a valid access token.

Put --remote last when a command also takes an argument:

supabase db pull add_profiles --remote

Deprecated target flags#

These flags still work in v3. They're hidden from help, print a warning on stderr that names the replacement, and are removed in v4.

v2v3
--linked--remote
--project-ref <ref>--remote=<ref>
--db-url <url>--remote=<url>
gen types --project-id <ref>gen types --remote=<ref>
--localNothing, because local is the default
db diff --from linked / --to linkeddb diff --from=remote / --to=remote

Don't swap --linked for --remote in place. Move --remote to the end of the command, so that supabase migration repair --linked 20240101000000 --status reverted becomes supabase migration repair 20240101000000 --status reverted --remote.

supabase link --project-ref <ref> is unchanged, and so is supabase link <ref>. link doesn't take --remote.

Other target changes#

  • --remote=<value> can't be combined with any of the deprecated flags.
  • --project-ref on a database command no longer needs --linked.
  • An empty --project-ref= is an error instead of being ignored.
  • --password only applies to a hosted project. It fails with the local stack or --remote=<url>, so put the password in the URL instead.
  • db diff rejects --remote together with --from or --to. Use --from=remote or --to=remote.
  • db query rejects SQL passed both as an argument and with --file.
  • A bare --remote in a checkout that isn't linked opens the project picker in an interactive terminal. Database and storage commands used to fail.

gen types and declarative schemas#

  • gen types with no target generates types from the local database, instead of opening a project picker or failing. In a linked checkout with no local stack running, it suggests --remote.
  • db schema declarative generate no longer asks which target to use.
  • db schema declarative sync with no declarative files generates them from the local database, and no longer offers the linked project as the source. To start from the linked project, run supabase db schema declarative generate --remote first.

Hosted changes need confirmation#

Every command that changes a hosted project asks for confirmation first. The question defaults to No, except for commands that only create something: projects create, orgs create, branches create, domains create, and bootstrap. Pressing Enter now declines db push, deploys, secret changes, and migration history updates.

When the CLI can't ask, the command fails with HostedMutationConsentRequiredError and changes nothing, unless you pass --yes or export SUPABASE_YES=1 in your shell. The CLI can't ask when:

  • stdin or stdout isn't a terminal, as in CI
  • the output is machine-readable: --output-format json or stream-json, or -o json, yaml, toml, or env
  • an AI coding agent runs the command. Agents never get a prompt, even in a terminal, and the error tells the agent to ask you before it passes --yes.

See When a command asks first for how the CLI decides when to ask.

Commands that need --yes in unattended runs#

AreaCommands
Database (hosted target)db push; db pull when it updates the remote migration history; pull, except with --dry-run; migration up, down, squash, and repair; db reset; test db; db query, or pass --read-only for reads
Storage (hosted target)seed buckets, storage cp, storage mv, and storage rm
Functions and configfunctions deploy, functions delete, secrets set, secrets unset, and config push
Projects and branchesprojects create, projects delete, orgs create, bootstrap, backups restore, and branches create, delete, disable, pause, unpause, and update
Project settingsencryption update-root-key, network-bans remove, network-restrictions update, ssl-enforcement update, postgres-config update and delete, sso add, update, and remove, vanity-subdomains activate and delete, domains create, activate, and delete, and compute push and delete
Othernotebooks push and feedback delete

Other confirmation changes#

  • db push, db reset against a hosted target (including seeding), config push, and functions deploy --prune each ask one combined question instead of several.
  • functions deploy --prune confirms the deploy and the prune before anything is deployed, and now prunes even when every function is already up to date.
  • secrets unset --all lists the secrets before it asks.
  • Declining fails with HostedMutationDeclinedError ("context canceled") and exit code 1. The command-specific cancellation errors are removed.
  • Declining db pull or pull stops before any file is written. There's no longer a files-only path.
  • Removing network bans automatically while connecting to a hosted database needs --yes, even for read-only commands such as db dump --remote. Without it, the command fails and points you to supabase network-bans remove.
  • migration down and migration repair --all against the local database no longer prompt.

Output changes#

  • Commands that resolve a target print Target: … once on stderr, including read-only commands against a hosted project. config push no longer prints its own "Pushing config to" line.
  • JSON object results and JSON error envelopes gain a target object. The target in db reset output changes from a string to that object, and config push JSON no longer reports declined_addons or a skipped status. Arrays and generated artifacts, such as gen types output, are unchanged.
  • migration up against a hosted target reports "Remote database is up to date." instead of "Local database is up to date.", and migration list labels the history column "Local DB" when the target is local.

secrets unset defaults to no#

  • The prompt defaults to No.
  • A run that can't prompt fails unless you pass --yes or set SUPABASE_YES=1, for named secrets and --all alike.
  • Piped answers are no longer read.
  • Running it with no secret names fails, instead of unsetting every secret. Pass --all to unset every secret.

Flags and environment variables override config#

Every config value now resolves in one order:

  1. Command-line flag
  2. Shell environment variable
  3. Project env files
  4. supabase/config.json or supabase/config.toml, with a matched [remotes.<name>] block over the base config
  5. Default

Project env files are .env.<SUPABASE_ENV>.local, .env.local, .env.<SUPABASE_ENV>, and .env, read from supabase/ and then the project root. SUPABASE_ENV defaults to development.

The change matters most if you use [remotes.<name>] blocks. In v2, a matched block beat SUPABASE_* environment variables. In v3 the variable wins, and the CLI prints a warning for each value it overrides. If you deploy to a project with a [remotes.<name>] block, check for variables that now take effect:

# In CI
env | grep ^SUPABASE_
# In your repository
grep -h ^SUPABASE_ supabase/.env* .env*

Set SUPABASE_ENV in CI so that .env.development doesn't apply, and keep .env.local out of CI, because it still takes precedence over .env.<SUPABASE_ENV>.

supabase config push now pushes these overrides. For example, a SUPABASE_AUTH_SITE_URL in .env.local replaces the site_url in [remotes.production]. Push labels each value that came from the environment, and supabase config diff shows the same values, so run config diff before your first v3 push. See Managing config and secrets.

See Which settings a command uses for the precedence order and how to see where a value came from.

Seeding#

  • Seeding a project that matches a [remotes.<name>] block asks first, unless the block itself sets db.seed.enabled = true. db push --include-seed and a hosted db reset exit with code 1 in CI unless you pass --yes.
  • --include-seed and SUPABASE_DB_SEED_ENABLED=true now override enabled = false in the base [db.seed] block.
  • db push still seeds only with --include-seed.

Database passwords#

  • --password with the local stack or a database URL exits with code 1 on db push, db pull, db dump, db schema declarative generate, migration list, and migration repair. Put the password in the URL, or set [db].password for the local database.
  • migration squash targets the local database by default, so migration squash --password also needs --remote.
  • In a directory linked to one project, SUPABASE_DB_PASSWORD is no longer sent to a different project. Pass --password, or provide an access token so the CLI can create a temporary login role.
  • gen types against the local database uses [db].password, not SUPABASE_DB_PASSWORD.
  • supabase link --password is deprecated and ignored.

Stricter validation#

  • An invalid value in config or in a SUPABASE_* variable fails every command that loads project config, including db push, stop, and services. For example, SUPABASE_STUDIO_PORT=abc fails with CliConfigValueError, and the error names the variable or file.
  • Boolean environment variables, including SUPABASE_EXPERIMENTAL_*, accept 1, t, T, true, True, TRUE, 0, f, F, false, False, or FALSE. Anything else fails.
  • Ports with a leading zero, such as 054322, are rejected.
  • inspect report needs [[experimental.inspect.rules]] (an array of tables) with string fields. A single [experimental.inspect.rules] table fails.

Local container names#

SUPABASE_PROJECT_ID now names local containers even when a [remotes.<name>] block matches. Without a project_id in config.toml, local containers and volumes are named after the project folder, not the project ref. Run supabase stop before you upgrade, or set project_id in config.toml.

If v3 starts a fresh local stack and your local data seems to be missing, it's still in the volumes under the old name. Find that name with docker volume ls --filter name=supabase_db_, set it as project_id in config.toml, and restart the stack.

Finding the project directory#

  • Without --workdir, the CLI uses the nearest directory that contains supabase/config.toml or supabase/config.json. A nested config.json project now wins over an outer config.toml one.
  • services and the functions commands read config.json before config.toml.

config push applies SMS confirmations as written#

In v2, config push applied [auth.sms] enable_confirmations inverted: true skipped the phone code, and false required it. v3 applies the setting as written, so phone confirmation on your project flips on your next config push:

  • false, the default that supabase init writes, turns phone confirmation off.
  • true turns phone confirmation on, so phone sign-ups need a working SMS provider.

pg-delta is the default diff engine#

pg-delta is now the default schema diff engine for every project, so the SQL that db diff and db pull generate can change if you hadn't opted in. See Diff engines for how the two engines differ.

Under pg-delta, [db.migrations].schema_paths and the declarative files in supabase/schemas are no longer inputs to db diff or to a migration-style db pull. The "edit supabase/schemas, then run db diff -f" workflow no longer picks up your edits. Use supabase db schema declarative sync to generate migrations from declarative schema files.

To keep using migra for every run, set this in supabase/config.toml:

[experimental.pgdelta]
enabled = false

To use migra for a single run, pass --use-migra to db diff or --diff-engine migra to db pull, or set SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED=false.

  • SUPABASE_EXPERIMENTAL_PG_DELTA is ignored. Use SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED.
  • db diff --use-pg-delta=false now turns pg-delta off even when config or the environment turns it on.

Removed commands and flags#

Each removed command or flag exits with code 1 and prints its replacement.

RemovedUse instead
supabase db remote changessupabase db diff --remote
supabase db remote commitsupabase db pull --remote
supabase gen keyssupabase projects api-keys --remote=<ref>
supabase db branch create, delete, list, and switchNo local replacement. Manage hosted preview branches with supabase branches.
supabase inspect db total-index-sizesupabase inspect db index-stats
supabase inspect db total-table-sizes and table-index-sizessupabase inspect db table-stats
db diff --use-pg-schemaOmit the flag to use the default engine, or pass --use-pg-delta
db pull --use-pg-deltadb pull --declarative
db pull --experimental, or SUPABASE_EXPERIMENTAL set, without --declarativedb pull --declarative, or --experimental=false for a migration pull
functions download --legacy-bundlefunctions download --use-api. If that fails and the function was deployed with a CLI older than 1.120.0, redeploy it with the current CLI first.
--include-raw-output on domains create, get, reverify, activate, delete-o json
--create-ticket (global flag)supabase issue bug

Removed config keys and environment variables#

Removed config keys fail, so you see them on the first run. Some removed environment variables are ignored instead, so check for these by hand.

RemovedIn v3Use instead
[inbucket] and [remotes.<name>.inbucket]Fails[local_smtp]
[experimental] orioledb_version (non-empty)Fails[db] orioledb_version
SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION (non-empty)FailsSUPABASE_DB_ORIOLEDB_VERSION
[auth.external.linkedin] and [auth.external.slack]Fails, even when disabled[auth.external.linkedin_oidc] and [auth.external.slack_oidc]
A function's own supabase/functions/<name>/import_map.jsonFailsRename it to deno.json, or set [functions.<name>] import_map
--override-name inbucket.urlFails--override-name mailpit.url
INBUCKET_URL in status and stack status outputNo longer printedMAILPIT_URL
SUPABASE_EXPERIMENTAL_PG_DELTAIgnoredSUPABASE_EXPERIMENTAL_PGDELTA_ENABLED
COBRA_COMPLETION_DESCRIPTIONSIgnoredSUPABASE_COMPLETION_DESCRIPTIONS

If a script reads INBUCKET_URL from supabase status -o env, it now gets an empty value. The shared supabase/functions/import_map.json fallback still works.

Error names and messages#

If your scripts match on error names in JSON output, update them:

v2v3
MigrationPasswordFlagsErrorDbPasswordFlagsError
Command-specific cancellation errorsHostedMutationDeclinedError

v3 also adds CliConfigValueError for an invalid config or environment value, and HostedMutationConsentRequiredError for a hosted change, including seeding, without --yes in a run that can't prompt.

If your scripts match on error text, these messages are reworded:

  • Invalid boolean flag values say expected a boolean, instead of strconv.ParseBool: ….
  • Invalid unsigned values, such as storage cp --jobs and db reset --last, say expected an unsigned integer or value out of range, instead of strconv.ParseUint: ….
  • Oversized migration or seed statements report scanner: token too long, instead of bufio.Scanner: token too long.
  • gen bearer-jwt claim and JWK errors read invalid claims: … and failed to parse JWK: … and name the JSON shape they expected.
  • Config decode errors drop the decoding failed due to the following error(s): header and read <key>: expected a string, got <type>, <key>: unknown keys: …, or <key>: must be one of ….
  • SUPABASE_PROFILE errors are reworded, for example unknown keys: …, <key> is required, and <key> must be an http(s) URL.
  • failed to unmarshal … messages read failed to parse …, and TOML output errors drop the toml: prefix.

Deprecated in v3#

These still work in v3 and are removed in v4:

  • --linked, --project-ref (except on supabase link), --db-url, --local, and gen types --project-id. Use --remote.
  • The linked value of db diff --from and --to. Use remote.
  • supabase link --password, which is ignored.
  • enable_signup in [auth.email] and [auth.sms], including [remotes.<name>.auth.email] and [remotes.<name>.auth.sms]. Use enabled.

The CLI warns while your config uses the old enable_signup keys. The new name says what the key does: it turns the email or phone provider on or off, so false also stops existing users from signing in that way. To block new sign-ups only, set enable_signup = false under [auth], which is not deprecated.

supabase-go is no longer installed#

v3 ships a single supabase executable on every install channel. v2 also installed a separate supabase-go executable next to it, which a few commands ran through. Most projects don't need to change anything, but check for these:

  • Scripts that call supabase-go directly fail, because v3 doesn't install it. Call supabase instead.
  • SUPABASE_GO_BINARY is ignored.
  • The commands that ran through supabase-go in v2 are removed. See Removed commands and flags.
  • If your security tooling allowlists the CLI's executables, supabase is the only one left.

Try the experimental supabase stack commands#

v3 includes the experimental supabase stack commands. They run a separate local project for each app, git worktree, git branch, or --stack name on one machine, so coding agents working in parallel each get their own database. They can also run without Docker: the native runtime runs each service as a process on macOS on Apple silicon and on Linux. See A local Supabase project per agent, with or without Docker for an overview.

The stack commands are off by default, so supabase start behaves as described in the rest of this guide until you turn them on. To try them in a new app:

SUPABASE_EXPERIMENTAL_STACK=1 supabase init
supabase start

In an existing app, add stack = true under [experimental] in supabase/config.toml and remove the fixed ports first. With the setting on:

  • supabase start, status, and stop manage the current app's local project.
  • Commands that target the local database by default, such as db push, migration list, and gen types, use that local project.
  • A named local project's database URL counts as a remote target, so --remote=<postgres-url> writes to it ask for confirmation. Pass --yes in scripts.

See Running multiple local projects for setup and limitations, and Docker and native runtimes for platform requirements.

Getting help#

If something on this page doesn't match what you see, or a change breaks a workflow this page doesn't cover, run supabase issue bug or open an issue.