Skip to content

feat(cli)!: one --remote flag, local target defaults and one hosted-mutation consent rule - #7188

Open
Coly010 wants to merge 197 commits into
nextfrom
columferry/cli-2695-bc-consistent-local-vs-remote-target-defaults-and-one
Open

Coly010 wants to merge 197 commits into
nextfrom
columferry/cli-2695-bc-consistent-local-vs-remote-target-defaults-and-one

Conversation

@Coly010

@Coly010 Coly010 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Commands that can run against either the local stack or a hosted project didn't agree on what they target by default, and each took a different mix of --local, --linked, --project-ref and --db-url. Hosted mutations also each had their own idea of when to ask. This makes both consistent: every command that can run locally runs locally unless you pass --remote, and every hosted mutation follows one consent rule.

Design: CLI-2695: one --remote flag. Decisions and enforcement: ADR 0033.

Targets

  • One flag, --remote, on every command that can reach a hosted project:

    Value Means
    none (--remote) The linked project (SUPABASE_PROJECT_ID, then the link file)
    20 lowercase letters That project ref
    postgres://… / postgresql://… That database (database commands only)
    anything else A branch name or branch UUID of the linked project, resolved with supabase link's rule (a branch UUID also works without a link)
  • --remote=<value> always works. --remote <value> is rewritten to the = form before parsing wherever it's unambiguous. When a bare --remote is followed by a word the command takes as an operand and that word is, or nearly is, a branch name, the command stops before doing anything and shows both readings.

  • Database commands (db, migration, inspect, storage, seed buckets, test db, gen types, db schema declarative generate) default to the local stack. In a linked checkout, a bare run says default; pass --remote for <project>.

  • Hosted-only commands (functions, secrets, config, branches, sso, …) keep the linked project as their default and take --remote=<ref|branch>. branches * takes --remote=<ref> for the parent project.

  • --linked, --project-ref, --db-url and gen types --project-id still work, routed through --remote, and print a deprecation warning on stderr naming the --remote replacement and v4 as the removal release. --local is accepted, does nothing, and warns. They're hidden from help.

  • db diff --from/--to take the same values plus local and migrations; remote is the linked project and linked is deprecated.

  • --remote=<default branch> is reported as production, not as a branch. A branch UUID used without a link is reported by its project ref alone, since the CLI can't tell whether it's the default branch.

  • Every command declares its target group with withTargetPolicy (local-only, account, hosted, selectable); a tree test fails if a leaf doesn't. Help shows a TARGET section and labels subcommands local only / remote only / local or remote.

  • Commands that resolve a target print one Target: … line on stderr, and JSON results carry a target object.

Hosted consent

  • One rule for every hosted mutation: ask in an interactive terminal, otherwise require --yes or a shell-exported SUPABASE_YES. A SUPABASE_YES from supabase/.env or a working-directory .env doesn't count, stdin is never read as an answer, and agents never get a prompt.
  • Replacing live state defaults to No. Only create-only operations (projects, branches, orgs and domains create, and bootstrap) default to Yes.
  • Commands that used to ask several questions (db push, db reset with seeding, config push, functions deploy --prune) ask one, after showing everything they'll change.
  • Network bans are only removed automatically under --yes.
  • Declining db pull or pull when it would update the remote migration history aborts before any file is written.

New

  • db query --read-only accepts only read statements (SELECT, WITH, VALUES, TABLE, SHOW, EXPLAIN or a parenthesised query), runs them in a read-only transaction with a short lock timeout (or the Management API's read_only mode on a hosted target), and skips consent. Functions with side effects called from a SELECT still run, and the docs say so.

The companion PR to develop, #7186, warns on bare runs of every command whose default flips here.

Linked issue

Closes CLI-2695

🤖 Generated with Claude Code

BREAKING CHANGE: Commands that can run locally now default to the local stack, --remote replaces the target flags, and every hosted mutation follows one consent rule.

  • db push, db pull, migration fetch, db dump, migration list, migration repair, inspect db *, inspect report, gen types and storage ls/cp/mv/rm now target the local stack when run without a target flag. Add --remote to reach the linked project (supabase db push --remote), --remote=<ref> for another project, --remote=<branch> for a branch of the linked project, or --remote=<postgres-url> for a database URL. SUPABASE_PROJECT_ID on its own no longer selects a hosted project for these commands; it only says which project bare --remote means.
  • gen types with no target generates from the local database instead of opening a project picker or failing. db schema declarative generate no longer asks which target to use.
  • --linked, --project-ref, --db-url and gen types --project-id are deprecated: they still work and print a warning on stderr naming the --remote form. --local does nothing and warns. v4 removes all of them, along with db diff's linked (use remote). They no longer appear in help.
  • --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 now an error instead of being ignored. --password only applies to a hosted target.
  • db diff rejects --remote together with --from/--to; use --from=remote or --to=remote.
  • Bare --remote (and the deprecated --linked) opens the project picker in an interactive terminal when the checkout isn't linked. Database and storage commands used to fail.
  • --remote <word> after a command that takes an operand fails with a "did you mean" error when <word> is, or is close to, a branch name of the linked project, or when the branch list can't be fetched. Put the operand before --remote (supabase db pull add_profiles --remote) or use --remote=<branch>.
  • Every hosted mutation now asks for confirmation, and fails with HostedMutationConsentRequiredError in non-interactive, JSON, stream-json, agent or -o json|yaml|toml|env runs unless --yes (or SUPABASE_YES exported in the shell) is set. Commands that newly need this for unattended runs:
    • db push; db pull and pull when they update the remote migration history
    • migration up/down/squash/repair against a hosted target
    • db reset and test db against a hosted target
    • db query against a hosted target (or pass --read-only for reads)
    • seed buckets and storage cp/mv/rm against a hosted target
    • functions deploy/delete; secrets set/unset; config push
    • branches create/delete/disable/pause/unpause/update; backups restore
    • encryption update-root-key; network-bans remove; network-restrictions update; ssl-enforcement update
    • postgres-config update/delete; sso add/update/remove
    • vanity-subdomains activate/delete; domains create/activate/delete
    • notebooks push; projects create/delete; orgs create; bootstrap; feedback delete
    • compute push/delete
  • A y piped on stdin no longer confirms a hosted mutation.
  • The default answer is No for every hosted mutation except projects, branches, orgs and domains create, and bootstrap. Pressing Enter now declines db push, deploys, secrets changes and migration history updates.
  • SUPABASE_YES in supabase/.env, or in a .env, .env.local or .env.<NODE_ENV>[.local] file in the working directory, no longer confirms hosted mutations. When one of those files sets it, a shell export of the same value doesn't count either. Use --yes.
  • db push, db reset against a hosted target (including seeding), config push and functions deploy --prune each ask one combined question instead of several. Deploy and prune are confirmed before anything is deployed, and --prune now runs even when every function is already up to date. secrets unset --all lists the secrets before asking.
  • Declining a hosted mutation fails with HostedMutationDeclinedError ("context canceled"). The command-specific cancelled error codes are removed. Declining db pull or pull aborts before any file is written; there's no longer a files-only path.
  • Removing network bans automatically, while connecting to a hosted database, now needs --yes. Otherwise the command fails and points to supabase network-bans remove.
  • migration down and migration repair --all on the local database no longer prompt.
  • 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 error envelopes gain a target object; db reset's target changes from a string to that object, and config push JSON no longer reports declined_addons or a skipped status. Arrays and generated artifacts 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.
  • Agents never get a confirmation prompt, even with a terminal attached; they need --yes for hosted mutations.

Every leaf command carries a withTargetPolicy annotation with its target
group and hosted-mutation consent default. A tree guard checks the group
against each leaf's flags, database-target default and consent.
…LI-2695)

Add resolveTarget for sync and selectable commands and
DbConfigResolver.resolveConnection. It takes an already-resolved target,
rejects --password unless the target is linked, announces the local and
--db-url targets, and only removes network bans when the caller holds an
approval. The existing resolve method keeps its behaviour.
requireHostedMutationConsent grants on --yes or a shell SUPABASE_YES, asks
only an interactive text-mode terminal, and otherwise fails with
HostedMutationConsentRequiredError; stdin is never read. A local target needs
no consent, and hostedMutationPreapproval lets reads clear a network ban only
under --yes.
planDbSeed returns the seed input and a detail line for the single hosted
consent. The existing resolveDbSeedInput and confirmSeedIntoMatchedRemote stay
until the integration shard removes them.
describeHostedConsent runs the six consent cases against a leaf: JSON and
piped text without --yes, a TTY decline, --yes, a shell SUPABASE_YES, and a
project .env that must not count. A synthetic leaf proves the helper.
CLI-2695)

Encryption, network bans/restrictions, SSL enforcement, Postgres config, SSO and
notebooks push ask for consent (default No) before their first API write, or accept
--yes. Reads never prompt.
withCommandTelemetry logs --remote, --linked, --local and a ref-shaped
--project-ref/--project-id for every hosted or selectable command, so the
per-leaf projectRefSafeFlags list is gone.
Hand-written selector interfaces dropped the --db-url acceptance brand, so a
database-URL leaf could lose --db-url, or a no-URL leaf pass one, without a
compile error.
…branch resolution tree-wide

The pooler entry applies to a ref or branch selector only. A guard test over
the command tree requires every --remote leaf to compose branchResolution.
seed buckets reuses STORAGE_PERMISSIONS and reads all four selector flags
from the seed group's scoped globals.
gen types resolves the target first, so a bare --remote followed by a branch
name reaches the ambiguity check instead of being read as the positional
language, and --remote <ref> works on the rewritten command line. The
remote-selector mutex groups collapse into one list of flags a hosted project
excludes.
Completion asks the same rewrite the runtime uses whether the word after a bare
--remote is its value, and offers --remote= (with no trailing space) instead of
a bare switch.
Moves the declaration into command-internal so seed buckets no longer imports
another command. The drift helper test now also covers --project-ref given as a
boolean.
…f sides

- repair, down, squash and the removed `db remote commit` hint name the
  target's flags so the suggested command reaches the same database
- `db diff` rejects a target selector combined with --from/--to and
  documents that `remote` there always means the linked project
- `db diff --from/--to=<ref|branch>` declares the linked-database
  permissions (login role, pooler config, network bans)
- the IPv6 error after an ad-hoc --remote=<ref|branch> points at the
  pooler instead of suggesting `supabase link`
- the `migration fetch` overwrite prompt names the target
- deprecation warnings show the command path, its operand placeholders and the
  `--remote` replacement last, and never echo operands, other flags, the `--`
  tail or URL userinfo
- `--remote` values are trimmed and matched case-insensitively against
  `postgres(ql)://`; other URLs are rejected, and a project URL suggests its ref
- keyword `--db-url` and boolean-literal `--project-ref` warnings say how to
  reach the same target through `--remote`
- an empty or false `--remote` fails without a help dump and names the values
  that command accepts; `--remote` on `link` and on local-only commands says so
- the default branch renders as the production project and a branch banner names
  its parent project
- the bare `--remote` ambiguity check reports a match, a near miss or a failed
  listing with both readings, and keeps the cause and a login suggestion
- branch lookups tell an expired token to log in and suggest the closest name
- the linked-project hint reads `SUPABASE_PROJECT_ID` before the link file
- `--remote` help rows, descriptions and JSON help follow each command's variant
- conflict errors list only the selectors the command has and suggest `--remote`
… command-target

- empty and false `--remote` pins use the new messages and the command's value hint
- deprecation warning examples show the operand placeholders
- ADR 0033 and command-target describe the SUPABASE_PROJECT_ID hint, the JSON help
  fields and the `link` and local-only `--remote` errors
…li-2695-bc-consistent-local-vs-remote-target-defaults-and-one
…erinfo at the last @

A word with whitespace or '=' is never read as a branch name, so a NAME=value
pair or a keyword DSN no longer reaches the bare-remote ambiguity check or a
branch lookup. --remote=<keyword dsn> is rejected with a pointer to
--remote=<url>, and stripUserinfo drops everything up to the last '@' so a
password containing '@' cannot leak.
A usage error no longer costs a branch listing or prints a Target banner. A
positional language that is also the word after a bare --remote still lets the
branch check rule first.
…nch UUID as unverified

The bare --remote ambiguity message no longer echoes the word, names a missing
or invalid access token, and the branch lookups declare required permissions
that match when they run (a new uuid selector shape covers GET /v1/branches/{id}).
A branch UUID resolved without a linked project is announced as the bare
project, environment unverified, since the lookup carries no default-branch flag.
A positional declared with Param.withHidden still parses but counts for no
operand capacity and no warning placeholder. gen types --remote <word> now
joins to --remote=<word> and its deprecation warnings read
'supabase gen types --remote=<ref>'; a language written before --remote keeps
working. The gen types positional-language guard no longer needs to wait for
the bare-remote ambiguity check.
…or --to

classifyExplicitRef no longer turns it into a branch name, so the value is
never echoed in a branch-not-found or not-linked message. The error points
at --from=<url> or --to=<url>.
classifyExplicitRef returns the rejection --remote gives a value instead of
reading it as a branch name, so a URL with credentials is never echoed. A
project's own https URL names --from=<ref> or --to=<ref>.
…c-consistent-local-vs-remote-target-defaults-and-one

# Conflicts:
#	apps/cli/src/commands/gen/types/SIDE_EFFECTS.md
#	apps/cli/src/shared/output/output.layer.ts
#	apps/cli/src/shared/output/output.layer.unit.test.ts
@Coly010 Coly010 changed the title feat(cli)!: consistent local vs remote target defaults and one hosted-mutation consent rule feat(cli)!: one --remote flag, local target defaults and one hosted-mutation consent rule Oct 19, 2026
expect(text).toContain(`TARGET\n ${target}\n`);
expect(row).toContain(`--remote[=${hint}]`);
expect(row).toContain(`--remote=${hint}`);
expect(row).not.toMatch(new RegExp(`\\]\\s+${hint.replace(/[|[\]]/g, "\\$&")}\\s`));

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants