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
--remoteto 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#
- 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.
- Stop your local stack with
supabase stopifsupabase/config.tomlhas noproject_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-devGitHub 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.stableFrom 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:
| Channel | Command |
|---|---|
| npm | npm install supabase@v2.stable --save-dev |
| Homebrew | brew install supabase/tap/supabase@2 |
| Scoop | scoop install supabase-v2-stable |
| GitHub Actions | version: 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#
| v2 | v3 |
|---|---|
supabase db push | supabase db push --yes --remote |
supabase db push --db-url "$DB_URL" | supabase db push --yes --remote="$DB_URL" |
supabase migration list --linked | supabase migration list --remote |
supabase gen types typescript --project-id "$REF" | supabase gen types typescript --remote="$REF" |
supabase gen types typescript --local | supabase gen types typescript |
supabase db dump --linked -f schema.sql | supabase db dump -f schema.sql --remote |
supabase functions deploy | supabase functions deploy --yes |
supabase secrets set --env-file .env.production | supabase secrets set --env-file .env.production --yes |
supabase config push | supabase config push --yes |
echo y | supabase <command> | supabase <command> --yes |
If your job starts a local stack, for example to run tests, a bare supabase db push succeeds against that stack and never touches your project. Add --remote to every command that should reach your hosted project.
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, anddb dumpmigration fetch,migration list, andmigration repairinspect db *andinspect reportgen typesstorage ls,storage cp,storage mv, andstorage rm
Use --remote to choose a hosted target:
| You pass | Target |
|---|---|
| Nothing | The local stack |
--remote | The 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 --remoteDeprecated 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.
| v2 | v3 |
|---|---|
--linked | --remote |
--project-ref <ref> | --remote=<ref> |
--db-url <url> | --remote=<url> |
gen types --project-id <ref> | gen types --remote=<ref> |
--local | Nothing, because local is the default |
db diff --from linked / --to linked | db 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-refon a database command no longer needs--linked.- An empty
--project-ref=is an error instead of being ignored. --passwordonly applies to a hosted project. It fails with the local stack or--remote=<url>, so put the password in the URL instead.db diffrejects--remotetogether with--fromor--to. Use--from=remoteor--to=remote.db queryrejects SQL passed both as an argument and with--file.- A bare
--remotein 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 typeswith 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 generateno longer asks which target to use.db schema declarative syncwith 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, runsupabase db schema declarative generate --remotefirst.
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 jsonorstream-json, or-o json,yaml,toml, orenv - 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.
- Piping an answer, such as
echo y | supabase db push, no longer confirms anything. SUPABASE_YESinsupabase/.env, or in any other project env file, doesn't count.- If a
.env,.env.local, or.env.<NODE_ENV>[.local]file in your working directory definesSUPABASE_YESat all, a shell-exportedSUPABASE_YESdoesn't count either, because the CLI can't tell the two apart. Use--yes.
See When a command asks first for how the CLI decides when to ask.
Commands that need --yes in unattended runs#
| Area | Commands |
|---|---|
| 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 config | functions deploy, functions delete, secrets set, secrets unset, and config push |
| Projects and branches | projects create, projects delete, orgs create, bootstrap, backups restore, and branches create, delete, disable, pause, unpause, and update |
| Project settings | encryption 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 |
| Other | notebooks push and feedback delete |
Other confirmation changes#
db push,db resetagainst a hosted target (including seeding),config push, andfunctions deploy --pruneeach ask one combined question instead of several.functions deploy --pruneconfirms the deploy and the prune before anything is deployed, and now prunes even when every function is already up to date.secrets unset --alllists the secrets before it asks.- Declining fails with
HostedMutationDeclinedError("context canceled") and exit code 1. The command-specific cancellation errors are removed. - Declining
db pullorpullstops 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 asdb dump --remote. Without it, the command fails and points you tosupabase network-bans remove. migration downandmigration repair --allagainst 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 pushno longer prints its own "Pushing config to" line. - JSON object results and JSON error envelopes gain a
targetobject. Thetargetindb resetoutput changes from a string to that object, andconfig pushJSON no longer reportsdeclined_addonsor a skipped status. Arrays and generated artifacts, such asgen typesoutput, are unchanged. migration upagainst a hosted target reports "Remote database is up to date." instead of "Local database is up to date.", andmigration listlabels 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
--yesor setSUPABASE_YES=1, for named secrets and--allalike. - Piped answers are no longer read.
- Running it with no secret names fails, instead of unsetting every secret. Pass
--allto unset every secret.
Flags and environment variables override config#
Every config value now resolves in one order:
- Command-line flag
- Shell environment variable
- Project env files
supabase/config.jsonorsupabase/config.toml, with a matched[remotes.<name>]block over the base config- 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 CIenv | grep ^SUPABASE_# In your repositorygrep -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 setsdb.seed.enabled = true.db push --include-seedand a hosteddb resetexit with code 1 in CI unless you pass--yes. --include-seedandSUPABASE_DB_SEED_ENABLED=truenow overrideenabled = falsein the base[db.seed]block.db pushstill seeds only with--include-seed.
Database passwords#
--passwordwith the local stack or a database URL exits with code 1 ondb push,db pull,db dump,db schema declarative generate,migration list, andmigration repair. Put the password in the URL, or set[db].passwordfor the local database.migration squashtargets the local database by default, somigration squash --passwordalso needs--remote.- In a directory linked to one project,
SUPABASE_DB_PASSWORDis no longer sent to a different project. Pass--password, or provide an access token so the CLI can create a temporary login role. gen typesagainst the local database uses[db].password, notSUPABASE_DB_PASSWORD.supabase link --passwordis deprecated and ignored.
Stricter validation#
- An invalid value in config or in a
SUPABASE_*variable fails every command that loads project config, includingdb push,stop, andservices. For example,SUPABASE_STUDIO_PORT=abcfails withCliConfigValueError, and the error names the variable or file. - Boolean environment variables, including
SUPABASE_EXPERIMENTAL_*, accept1,t,T,true,True,TRUE,0,f,F,false,False, orFALSE. Anything else fails. - Ports with a leading zero, such as
054322, are rejected. inspect reportneeds[[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 containssupabase/config.tomlorsupabase/config.json. A nestedconfig.jsonproject now wins over an outerconfig.tomlone. servicesand thefunctionscommands readconfig.jsonbeforeconfig.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 thatsupabase initwrites, turns phone confirmation off.trueturns phone confirmation on, so phone sign-ups need a working SMS provider.
Check enable_confirmations in [auth.sms], and in any [remotes.<name>.auth.sms] block, and run supabase config diff before your first v3 push.
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 = falseTo 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_DELTAis ignored. UseSUPABASE_EXPERIMENTAL_PGDELTA_ENABLED.db diff --use-pg-delta=falsenow 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.
| Removed | Use instead |
|---|---|
supabase db remote changes | supabase db diff --remote |
supabase db remote commit | supabase db pull --remote |
supabase gen keys | supabase projects api-keys --remote=<ref> |
supabase db branch create, delete, list, and switch | No local replacement. Manage hosted preview branches with supabase branches. |
supabase inspect db total-index-size | supabase inspect db index-stats |
supabase inspect db total-table-sizes and table-index-sizes | supabase inspect db table-stats |
db diff --use-pg-schema | Omit the flag to use the default engine, or pass --use-pg-delta |
db pull --use-pg-delta | db pull --declarative |
db pull --experimental, or SUPABASE_EXPERIMENTAL set, without --declarative | db pull --declarative, or --experimental=false for a migration pull |
functions download --legacy-bundle | functions 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.
| Removed | In v3 | Use instead |
|---|---|---|
[inbucket] and [remotes.<name>.inbucket] | Fails | [local_smtp] |
[experimental] orioledb_version (non-empty) | Fails | [db] orioledb_version |
SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION (non-empty) | Fails | SUPABASE_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.json | Fails | Rename it to deno.json, or set [functions.<name>] import_map |
--override-name inbucket.url | Fails | --override-name mailpit.url |
INBUCKET_URL in status and stack status output | No longer printed | MAILPIT_URL |
SUPABASE_EXPERIMENTAL_PG_DELTA | Ignored | SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED |
COBRA_COMPLETION_DESCRIPTIONS | Ignored | SUPABASE_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:
| v2 | v3 |
|---|---|
MigrationPasswordFlagsError | DbPasswordFlagsError |
| Command-specific cancellation errors | HostedMutationDeclinedError |
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 ofstrconv.ParseBool: …. - Invalid unsigned values, such as
storage cp --jobsanddb reset --last, sayexpected an unsigned integerorvalue out of range, instead ofstrconv.ParseUint: …. - Oversized migration or seed statements report
scanner: token too long, instead ofbufio.Scanner: token too long. gen bearer-jwtclaim and JWK errors readinvalid claims: …andfailed 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_PROFILEerrors are reworded, for exampleunknown keys: …,<key> is required, and<key> must be an http(s) URL.failed to unmarshal …messages readfailed to parse …, and TOML output errors drop thetoml:prefix.
Deprecated in v3#
These still work in v3 and are removed in v4:
--linked,--project-ref(except onsupabase link),--db-url,--local, andgen types --project-id. Use--remote.- The
linkedvalue ofdb diff --fromand--to. Useremote. supabase link --password, which is ignored.enable_signupin[auth.email]and[auth.sms], including[remotes.<name>.auth.email]and[remotes.<name>.auth.sms]. Useenabled.
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-godirectly fail, because v3 doesn't install it. Callsupabaseinstead. SUPABASE_GO_BINARYis ignored.- The commands that ran through
supabase-goin v2 are removed. See Removed commands and flags. - If your security tooling allowlists the CLI's executables,
supabaseis 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 initsupabase startIn 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, andstopmanage the current app's local project.- Commands that target the local database by default, such as
db push,migration list, andgen 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--yesin scripts.
The stack commands are experimental, and their flags and output can change between releases. A local project started with them keeps its own data, separate from the one you started without the setting.
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.