Skip to content

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

Merged
Coly010 merged 332 commits into
nextfrom
columferry/cli-2695-bc-consistent-local-vs-remote-target-defaults-and-one
Oct 21, 2026
Merged

Coly010 merged 332 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)
    true, yes, on, 1, y The linked project, as bare --remote
    false, no, off, 0, n Rejected: leave --remote out instead
    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, or is shaped like a project ref, the command stops before doing anything and shows both readings. --remote given twice with different values fails before anything runs.

  • 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. The deprecated flags still work alongside them and warn: --project-ref points the remote side at that project, and the others change nothing.

  • A hosted database whose direct host is unreachable falls back to its connection pooler: first the pooler URL supabase link saved, then the project's pooler config from the Management API. The linked project used to try only the saved URL.

  • --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 (db diff --from/--to prints one per database, in order), 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, and a failed removal is reported as that failure.
  • pull asks once, up front, on every run except --dry-run. db pull asks when it would update the remote migration history. Declining either 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. 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; run supabase db schema declarative generate --remote first to start from the linked project.
  • --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.
  • db query rejects SQL passed both as an argument and with --file.
  • 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, when it's shaped like a project ref, 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 when it updates the remote migration history; pull, except with --dry-run
    • 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 a working-directory file defines it at all, a shell-exported SUPABASE_YES doesn't count either, since the CLI can't tell the two apart. 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.

…r can match

A nested comment closed by */ directly followed by * re-used the slash as the start of /*, so the
splitter stayed inside a comment PostgreSQL had already closed. /*/ also no longer closes the
comment it opens. Adds an unterminated-input flag for callers that must refuse such input.
…ck waits

--read-only now accepts only statements that start with SELECT, WITH, VALUES, TABLE, SHOW or
EXPLAIN, refuses input that ends inside a comment, quote or dollar quote or contains BEGIN ATOMIC,
and sets a 5s lock_timeout inside the read-only transaction on direct connections. Docs and
SIDE_EFFECTS state what the flag does and does not guarantee.
…tions

- Hosted changes accept only `--yes` or a shell-exported SUPABASE_YES; a value that a cwd
  `.env`, `.env.local` or `.env.$NODE_ENV[.local]` also defines counts as file-sourced.
- The confirmation echo names the real source (`--yes` or `SUPABASE_YES`).
- The consent-required error names the action, target and reason, with suggestions for
  `--yes`, `--read-only` (db query), ignored dotenv files and agent runs.
- Action strings are imperative sentence-case phrases at every call site.
- `canPrompt` refuses when an agent is detected; `notebooks pull` reconcile uses it.
- Global `--yes` help describes the hosted rule.
- A sync or selectable leaf with `consent: "none"` must declare `consentExemptReason`
  (type and tree guard); notebooks pull's create operation is exempt for that leaf only.
- ADR 0033 matches the code: groups, approval sources, agent rule, exemptions.
# Conflicts:
#	apps/cli/src/commands/gen/types/SIDE_EFFECTS.md
…default-local hint

Show "branch" only on positive evidence, name a ref that is not the linked project,
append a --linked hint to the default-local banner, add the port to the db-url banner,
encode every JSON target through a CommandTargetJson schema, share withAnnouncedTarget,
and build the linked ResolvedTarget in resolveTarget. config push reads its JSON target
environment from the live probe and prints its plan lines in plan wording.
Every loader reports a malformed config file through describeConfigParseError, resolveTarget
hints at --linked only for a default-local run on a linked checkout, resolveConfigTarget has
integration coverage for its linked target, and the command target banner and JSON shape live
in one shared doc that the SIDE_EFFECTS files link.
# Conflicts:
#	apps/cli/src/commands/config/push/push.integration.test.ts
#	apps/cli/src/commands/secrets/set/set.integration.test.ts
@Coly010
Coly010 requested a review from a team as a code owner October 15, 2026 16:31

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by a newer AI review

🤖 AI Review

Verified all four findings against the PR code. Confirmed the inaccurate reset plan, failing read-only live-test assertion, and misleading linked-target hint. Refuted the reported consent-bypass example: parseDotEnv expands the same-file reference to "1", so the consent check recognizes it as dotenv-derived. Focused Bun checks verified the parser behavior and SQL rejection; the live suite was not run.

Findings

Severity Location Category Sources Claim
🟡 MINOR apps/cli/src/commands/db/reset/reset.handler.ts:243 ux claude The remote reset confirmation promises to apply local migrations even when migrations are disabled in config.
🟡 MINOR apps/cli/src/commands/db/query/query.live.test.ts:48 test-correctness codex The new read-only live test expects a database error, but its CREATE statement is rejected by the client-side filter before execution.
🟡 MINOR apps/cli/src/command-internal/target/resolve-target.ts:75 target-selection codex The default-local hint can name a different project from the project that --linked selects when SUPABASE_PROJECT_ID overrides the saved link.
Refuted findings (kept for transparency, not posted as review comments)
  • apps/cli/src/command-internal/hosted-consent/hosted-consent.yes.ts:74 (security): Dotenv variable expansion can silently approve hosted mutations because APPROVAL=1 followed by SUPABASE_YES=$APPROVAL allegedly parses as a literal reference while Bun supplies "1".
    Refuted: Running the actual parseDotEnv implementation on the reported input returns SUPABASE_YES="1", not "$APPROVAL". That equals the reported Bun process value, so hosted-consent.yes.ts:74-77 returns no approval. The alleged literal-versus-expanded mismatch does not occur for this example.

Stats

Claude findings: 1 · Codex findings: 3 · Confirmed: 3 · Refuted: 1 · Uncertain: 0


Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

Comment thread apps/cli/src/commands/db/reset/reset.handler.ts Outdated
Comment thread apps/cli/src/commands/db/query/query.live.test.ts Outdated
Comment thread apps/cli/src/command-internal/target/resolve-target.ts Outdated
Group help tags every subcommand row as local or remote, local only or
remote only, and a command's own help gets a TARGET section before USAGE.
The --remote flag row shows the value it accepts. JSON help carries the same
information as a target field.
…bc-consistent-local-vs-remote-target-defaults-and-one
…bc-consistent-local-vs-remote-target-defaults-and-one
@Coly010

Coly010 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for dogfooding it on staging @avallete, that's really reassuring! Went through the notes:

Worth a look: all fixed

  • --yes with a failing ban removal now fails with the removal error itself, and never says it needs --yes
  • loopback aliases (localhost, 127.x, ::1) count as the same host when checking for the local db, so your localhost:54322 URL is local now
  • db query rejects --file together with positional SQL before resolving anything, so a mistyped --remote <word> can't fall through to the linked project
  • PR body updated for pull and declarative sync, and the stale db push JSDoc is fixed (plus a few others with the same wording)

Minor:

  • SUPABASE_YES: went with your suggestion. Any autoloaded cwd dotenv definition makes the env value file-sourced, whatever it is
  • db diff --from lcoal now says Did you mean --from=local? and lists what --from/--to accept. A real branch called locale still resolves
  • gen types --remote typescript is still read as a branch, but when that branch doesn't exist it now suggests --lang=typescript --remote
  • --remote --remote=<linked ref>: I've left this one as a conflict. Treating them as equal would mean reading the link before the duplicate check, for a case nobody really writes on purpose. Happy to revisit if you feel strongly though

Cleanup: done, apart from one bit

  • hasExplicitSelector, the operand-capacity snapshot, the helper's own integration test and the duplicate branchRow/request-log copies are gone, and the e2e is down to the two cases that need the binary
  • warnTargetDeprecations/refFromDbUrl are un-exported and tested through their callers, and the overlapping target unit tests are deduped
  • OFF_WORDS is the one list now (global-flags keeps its exact-case match)
  • parent-ref JSDoc reworded, release-notes prompt reverted
  • I kept the consent coverage test's check that every consent leaf has a describeHostedConsent test, since that's what stops a new hosted mutation shipping without one. The lowercase-action source scan is gone

Comment thread apps/cli/src/command-internal/db-config.layer.ts Outdated
@Coly010
Coly010 added this pull request to the merge queue Oct 21, 2026
Merged via the queue into next with commit 1fa6164 Oct 21, 2026
48 checks passed
@Coly010
Coly010 deleted the columferry/cli-2695-bc-consistent-local-vs-remote-target-defaults-and-one branch October 21, 2026 11:11
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.

3 participants