Skip to content

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

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

Coly010 wants to merge 108 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

Database and storage commands that can run locally didn't agree on what they target by default. Some went to the local stack, some to the linked project, and gen types opened a picker. Hosted mutations also each had their own idea of when to ask, what the default answer was, and whether y on stdin or SUPABASE_YES in supabase/.env counted. This makes both consistent.

Targets

  • Every command declares its target group with withTargetPolicy (local-only, account, hosted, sync, selectable). A tree test fails if a leaf doesn't.
  • Commands that can run locally default to the local stack. --linked, --project-ref or --db-url pick something else, or set the new workspace target = "remote" in supabase/config.toml (or SUPABASE_TARGET=remote) to make the linked project the default.
  • --local, --linked and --db-url are mutually exclusive everywhere, --project-ref implies --linked, and --password only applies to a linked target.
  • 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 --linked 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 now 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 with the Management API's read_only mode on --linked), and skips consent. It's meant for unattended reads against hosted databases. Functions with side effects called from a SELECT still run, and the docs say so.

The design and its enforcement are in ADR 0033, which also replaces ADR 0032's seed consent section and updates ADR 0004's safety model. A companion PR to develop, #7186, adds the target key early and warns on commands whose default flips here.

Linked issue

Closes CLI-2695

🤖 Generated with Claude Code

BREAKING CHANGE: Target defaults and hosted-mutation consent are now consistent across the CLI.

  • db dump, migration list, migration repair, inspect db *, inspect report, gen types and storage ls/cp/mv/rm now use the workspace target, which defaults to the local stack, instead of the linked project. To keep the old behaviour, pass --linked, set target = "remote" in supabase/config.toml, or export SUPABASE_TARGET=remote. SUPABASE_PROJECT_ID on its own no longer selects a hosted project for these commands.
  • gen types with no selector generates from the local database instead of opening a project picker or failing. --project-id is deprecated in favour of --project-ref.
  • db schema declarative generate no longer asks which target to use. It uses the workspace target, or --linked/--db-url.
  • --local=false and --linked=false now mean "not set", so db reset --linked=false resets the local database. --local, --linked and --db-url are mutually exclusive on every command. --project-ref implies --linked and is rejected with --local or --db-url. --password is rejected unless the target is the linked project, before any local stack is started.
  • Commands that target the linked project open the project picker in an interactive terminal when the checkout is unlinked. Database and storage commands used to fail.
  • 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 --linked (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 linked 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.
…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.

🤖 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 on lines +243 to +245
useSchemaFiles
? "apply the declarative schema files"
: `apply local migrations${resolvedVersion === "" ? "" : ` up to version ${resolvedVersion}`}`,

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.

🟡 MINOR · ux · source: claude

The remote reset confirmation promises to apply local migrations even when migrations are disabled in config.

Evidence: reset.handler.ts:243-245 includes the migrations plan line whenever useSchemaFiles is false, whereas lines 270-279 apply migrations only when toml.migrationsEnabled is true. db-config.toml-read.ts:718 obtains that boolean from config.db.migrations.enabled.

Suggested fix: Include the migrations plan line only when toml.migrationsEnabled is true; retain the separate declarative-schema plan line.

Comment on lines +48 to +54
`create table ${table} (id int)`,
"--db-url",
project.dbUrl,
"--read-only",
]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("read-only transaction");

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.

🟡 MINOR · test-correctness · source: codex

The new read-only live test expects a database error, but its CREATE statement is rejected by the client-side filter before execution.

Evidence: query.live.test.ts:48 submits CREATE TABLE with --read-only, and line 54 expects "read-only transaction". query.read-only.ts:77-87 classifies CREATE as a violation; query.handler.ts:364-371 returns the statement-filter error before reaching execution at lines 384-391.

Suggested fix: Test database enforcement with an allowlisted statement that attempts a write, such as SELECT INTO a uniquely named table, and clean up that table if enforcement regresses.

Comment on lines +75 to +83
const linkedRef = yield* readProjectRefFile(fs, path, workdir).pipe(
Effect.map(Option.filter((ref) => PROJECT_REF_PATTERN.test(ref))),
Effect.orElseSucceed(() => Option.none<string>()),
);
if (Option.isNone(linkedRef)) return;
const cell = yield* Effect.serviceOption(CommandTarget);
if (Option.isNone(cell)) return;
const cache = yield* readCachedLinkedProject(fs, path, workdir);
yield* cell.value.hintLocal(linkedAlternativeHint({ ref: linkedRef.value, cache }));

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.

🟡 MINOR · target-selection · source: codex

The default-local hint can name a different project from the project that --linked selects when SUPABASE_PROJECT_ID overrides the saved link.

Evidence: resolve-target.ts:75-83 builds the hint exclusively from readProjectRefFile. target-identity.ts:76 renders "default; pass --linked for" that ref. In contrast, project-ref.layer.ts:115-122 prioritizes cliSettings.projectId over the saved ref, and command-settings.layer.ts:148-149 loads the environment project ID.

Suggested fix: Resolve the hint's project using the same environment-before-file precedence as linked selection, without prompting or making network requests.

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.

1 participant