Skip to content

feat(cli): add feedback add and delete commands for quick CLI feedback - #5988

Open
kanadgupta wants to merge 29 commits into
developfrom
kanad-claude-2026-07-22/feedback-command
Open

feat(cli): add feedback add and delete commands for quick CLI feedback#5988
kanadgupta wants to merge 29 commits into
developfrom
kanad-claude-2026-07-22/feedback-command

Conversation

@kanadgupta

@kanadgupta kanadgupta commented Jul 29, 2026

Copy link
Copy Markdown
Member

What

Adds a TS-only supabase feedback command family (from the original brainstorm) to the legacy shell so users — and agents — can send quick, low-friction feedback to the Supabase team without filing a GitHub issue, and revoke a submission later (e.g. an accidentally pasted secret):

supabase feedback add "when I run multiple stacks in parallel I get port conflicts"
# → Thanks for the feedback!
# → To delete this feedback later, run: supabase feedback delete <token>

supabase feedback delete 123e4567-e89b-12d3-a456-426614174000

Part of CLI-1946; the delete command is CLI-2188. Scope evolved in this thread: feedback add (no btw alias) plus a token-based delete path, rather than full user-scoped CRUD.

How feedback add works

  • Message resolution: positional args → piped stdin (non-TTY) → interactive prompt (TTY, text mode) → error. Messages starting with a dash use the -- sentinel.
  • Transport: submits through the SECURITY DEFINER RPC submit_interfaces_feedback (feat: table for collecting interfaces feedback supabase#48420) via supabase-js — the table has no insert grant, so the RPC is the only door and the delete token is always server-generated. The committed key is a publishable (anon) key, safe to ship in the binary. 10s timeout.
  • Delete token: the RPC returns a uuid delete_token exactly once. Text mode prints it with a "to delete this later" hint; json/stream-json carry it as delete_token in the result payload. The CLI never persists it.
  • Submission context: CLI version, user agent, OS/arch, agent detection (is_agent/agent_name via @vercel/detect-agent, to support the activation analysis in AI-961), and the linked project ref. metadata.source: "cli" distinguishes CLI rows from the future MCP path. The access token is never sent; user_id is never sent.
  • Project ref resolution: SUPABASE_PROJECT_ID<workdir>/supabase/.temp/project-ref (the file supabase link writes) → omitted. Reads the file directly (not via LegacyProjectRefResolver, whose prompt path needs the platform API) so feedback works logged-out; a broken ref file degrades to "unlinked".
  • Environments: the feedback backend follows the resolved profile the same way the Management API URL does (staging profiles → staging project). Production intentionally reuses the staging project until a dedicated one is provisioned (tracked in CLI-1998).

How feedback delete <token> works

  • Validation: the token must be a UUID (checked client-side to avoid PostgREST's cryptic uuid-cast error) and is lowercased before sending.
  • Preview first: a token-scoped read shows the feedback text before anything is deleted, so the user can verify what the token unlocks. Zero rows → a friendly not-found error covering all three indistinguishable causes (wrong token, already deleted, project-ref context mismatch).
  • Confirmation: interactive text mode prompts (Permanently delete this feedback? [y/N]); --yes/SUPABASE_YES skips it. Machine modes (json/stream-json) fail loudly without --yes rather than deleting silently — same contract as logout.
  • Deletion: a hard DELETE with Prefer: count=exact; the CLI verifies Content-Range reports exactly one row. Authorization is the x-feedback-token request header matched by RLS — the delete_token=eq. URL filter only satisfies PostgREST's filterless-delete rejection.
  • Context gate: rows submitted from a linked project also require the matching x-feedback-project-ref header. The delete command resolves the ref as --project-refSUPABASE_PROJECT_ID → linked-ref file and always sends whatever resolves (extra context against a context-free row is ignored server-side).
  • Machine modes return the deleted text in the result payload: { "feedback": "...", "message": "Feedback deleted." }.

Privacy note for reviewers

The feedback message, the delete token, and the --project-ref value go only to the feedback backend — never to PostHog. Message and token are positional arguments, which extractChangedFlagNames structurally excludes from the flags telemetry property; --project-ref is recorded by name only with its value redacted. Regression tests assert none of them appear in captured analytics events.

Reviewer-relevant context

  • The shared service was reshaped from FeedbackSubmitter (insert-only) into FeedbackClient (submit/preview/delete) in src/shared/feedback/feedback-client.{service,layer}.ts, and the profile→environment mapping and cli-config layer wiring were hoisted to the feedback family root (feedback.layers.ts, feedback-project-ref.ts) now that two commands share them.
  • src/shared/feedback/database.types.ts is generated (supabase gen types) and excluded from formatting/knip.
  • The e2e golden path is one combined add → delete round trip against the staging project (pinned --profile supabase-staging), which also cleans up its own row each run.
  • postgrest-js silently retries idempotent GETs (the preview) up to 3× with backoff on network errors; mutations and the RPC don't retry. It settles fine — noted because supabase-js exposes no way to disable it.
  • The merge from develop picked up the CLI-1970 docs restructure: the feedback commands are recorded in docs/go-cli-divergences.md (TS-only section) and registered in legacy-docs-spec.tables.ts (other-commands tag) instead of the old porting-status tracker.
  • Heads-up on LegacyCliConfig.projectId: it is a bare SUPABASE_PROJECT_ID env passthrough — it does not read config.toml or the linked-project file, so it is None in a linked project unless that env var is set. An earlier revision of this branch used it directly as "the linked project ref", which meant project_ref was always null in practice. The AGENTS.md row that described it as resolving project-id from config.toml is corrected here, since that phrasing is what made the field look project-aware.
  • services.integration.test.ts now uses an isolated temp workdir instead of process.cwd(), fixing machine-dependent behavior when the developer has local supabase start state.

🤖 Generated with Claude Code

kanadgupta and others added 10 commits July 28, 2026 08:17
The vendored effect clone in .repos/ drowns out workspace results in
editor-wide search.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LegacyCliConfig.projectId is a bare SUPABASE_PROJECT_ID env passthrough, so
the feedback submission's project_ref was null in a linked project unless
that env var happened to be set. Fall back to <workdir>/supabase/.temp/
project-ref, the file supabase link writes, mirroring the soft-load half of
LegacyProjectRefResolver.resolveOptional. The file is read directly rather
than through the resolver so the command keeps working unauthenticated; a
broken ref file degrades to unlinked instead of failing the submission.

The previous integration test injected projectId straight into the config
mock, so it only proved the handler forwarded the field and never exercised
resolution -- despite being named for the workdir-linked scenario that did
not work. Replace it with coverage that seeds the real file, plus env
precedence, unlinked, and unreadable-file cases.

Also correct the AGENTS.md row claiming LegacyCliConfig reads project-id
from config.toml, which is what made this field look project-aware.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanadgupta
kanadgupta marked this pull request as ready for review July 29, 2026 05:25
@kanadgupta
kanadgupta requested a review from a team as a code owner July 29, 2026 05:25
@kanadgupta
kanadgupta requested review from gregnr and mattrossman July 29, 2026 05:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 656f13a667

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/feedback.e2e.test.ts Outdated
Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@d8a427eb17f081911ac3246cae480174b44ba5ab

Preview package for commit d8a427e.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 830e565f2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae5d202bd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/legacy/commands/feedback/feedback.handler.ts Outdated
Comment thread apps/cli/src/shared/feedback/feedback-submitter.layer.ts Outdated
Comment thread apps/cli/src/legacy/commands/feedback/add/add.handler.ts Outdated
…alias

Restructures the TS-only feedback command from a single `supabase feedback`
command (with a `btw` alias) into a `feedback` group with an `add`
subcommand, following the nested-subcommand layout. Telemetry now records
`command: "feedback add"`; behavior is otherwise unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanadgupta kanadgupta changed the title feat(cli): add feedback command for quick CLI feedback submission feat(cli): add feedback add command for quick CLI feedback submission Aug 13, 2026
…07-22/feedback-command

# Conflicts:
#	apps/cli/src/legacy/commands/functions/delete/delete.integration.test.ts
#	apps/cli/src/legacy/commands/functions/download/download.integration.test.ts
#	apps/cli/src/legacy/telemetry/legacy-command-instrumentation.unit.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ca265f84d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/cli/src/shared/feedback/feedback-submitter.layer.ts Outdated
@kanadgupta
kanadgupta marked this pull request as draft August 13, 2026 18:09
- `feedback add` now calls the SECURITY DEFINER `submit_interfaces_feedback`
  RPC (the upstream table no longer allows direct inserts) and surfaces the
  server-issued delete token exactly once in every output format
- new `feedback delete <token>` previews the feedback text, confirms
  (`--yes`/`SUPABASE_YES` to skip; machine modes fail loudly without it),
  and hard-deletes via the token-gated RLS policy (`x-feedback-token` +
  optional `x-feedback-project-ref` context header)
- reshape the shared `FeedbackSubmitter` service into `FeedbackClient`
  (submit/preview/delete) and hoist the shared feedback layers and
  project-ref resolver to the command family root

CLI-1946, CLI-2188

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kanadgupta kanadgupta changed the title feat(cli): add feedback add command for quick CLI feedback submission feat(cli): add feedback add and delete commands for quick CLI feedback Aug 14, 2026
kanadgupta and others added 11 commits August 13, 2026 18:17
…eedback-submission-e37650

# Conflicts:
#	apps/cli/docs/go-cli-porting-status.md
## Summary

Attach the gotrue user UUID to `supabase feedback add` submissions via
the `submit_interfaces_feedback` RPC's optional `user_id` parameter,
mirroring the project-ref convention: best-effort and never failing the
submission.

The id is sourced from the persisted telemetry identity (distinct_id in
~/.supabase/telemetry.json, stamped at login) — a synchronous in-memory
read, so the command keeps its zero-auth posture and works logged-out
(user_id omitted). Submit-side attribution is gated on telemetry
consent: opted-out users submit anonymously. No deviceId fallback —
user_id is semantically a gotrue UUID, not an anonymous device id.

Because the `interfaces_feedback` RLS policies require a matching
`x-feedback-user-id` header to read or delete a row that was submitted
with a `user_id`, `feedback delete` now presents the persisted id on
both the preview and the delete requests. Unlike submission, the header
is **not** consent-gated — it is functional auth context, and gating it
would strand rows submitted before a consent opt-out. Logged-out runs
omit the header, which still matches all anonymous rows.

Stacked on #5988 (base: `kanad-claude-2026-07-22/feedback-command`);
this is a clean reapplication of the change from #5998 onto the
RPC-based `FeedbackClient`.

## Linked issue

closes [CLI-2008](https://linear.app/supabase/issue/CLI-2008)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-conflicts-develop-2fe0dd

# Conflicts:
#	apps/cli/AGENTS.md
Applies the accepted Codex review findings on the feedback family:

- Abort in-flight feedback requests on fiber interruption: the client's
  run helper now threads Effect.tryPromise's interruption signal into
  every postgrest call via AbortSignal.any with the 10s timeout, so
  Ctrl-C can no longer let a submit commit after cancellation.
- Gate the interactive prompt on stdin.isTTY in addition to
  output.interactive, so whitespace-only piped stdin with a TTY stdout
  fails with the documented empty-message error instead of opening a
  prompt against exhausted stdin.
- Honor the --agent yes|no override in the submission payload via
  legacyResolveAgentMode (hoisted from db query to legacy/shared);
  --agent no also suppresses the detected agent_name.
- Refresh ~/.supabase/telemetry.json on every feedback add/delete run
  via the standard Effect.ensuring(telemetryState.flush) finalizer.
- Honor -o json on both commands (machine payload via encodeGoJson,
  stdout payload-only); values outside feedback's pretty|json enum are
  rejected pre-run like db query's restricted set. yaml/toml stay
  unsupported: the struct-spec encoders reproduce Go field names and no
  Go struct exists for this TS-only command.
- Route feedback HTTP through the legacy transport: a composed fetch
  wires --debug request logging and --dns-resolver https DoH resolution
  into the supabase-js client.
- Move the real-backend add→delete round trip to the gated live tier
  (add.live.test.ts) and keep a hermetic e2e for subcommand routing via
  the no-network empty-message path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +21 to +28
const FEEDBACK_STAGING: FeedbackEnvironment = {
url: "https://imrwaufzgcaczqmpnxyr.supabase.co",
key: "sb_publishable_puOyAlqG5J_XfBMTDM2Ckw_L5mieFdb",
};

// No dedicated production feedback project exists yet (CLI-1946): production
// intentionally reuses the staging values until one is provisioned.
const FEEDBACK_PRODUCTION: FeedbackEnvironment = { ...FEEDBACK_STAGING };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

note to self: update these values once CLI-1999 and CLI-1998 are complete

@kanadgupta
kanadgupta marked this pull request as ready for review August 19, 2026 00:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8a427eb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
// The logger's write is synchronous (a gated stderr write); running it
// at this plain-fetch boundary keeps the wrapper a `typeof fetch`.
Effect.runSync(logger.http(method, url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Redact deletion tokens from debug URLs

When feedback delete <token> --debug runs, both the preview and DELETE requests encode the capability in the URL as delete_token=eq.<token>, and this call writes that entire URL to stderr. This exposes the token—and therefore authority to read and delete potentially sensitive feedback—in terminal recordings, CI logs, or debug output shared with support; redact this query parameter only for logging while sending the original URL to the transport.

AGENTS.md reference: apps/cli/AGENTS.md:L318-L318

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds like an actual risk.

Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.

Comment on lines +39 to +40
const deleted = await runSupabaseLive(
[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clean up live feedback in a finally block

If the add succeeds but JSON parsing, an assertion, or the subsequent delete invocation fails, execution never completes this cleanup and the persistent staging row remains. Fresh evidence in the current revision is that deletion is still only a sequential test step after assertions rather than guaranteed teardown; retain the returned token and perform an exact best-effort delete in finally so failing live runs do not pollute staging.

AGENTS.md reference: AGENTS.md:L170-L170

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.

@kanadgupta
kanadgupta marked this pull request as draft August 19, 2026 00:20
@kanadgupta
kanadgupta marked this pull request as ready for review August 19, 2026 00:21

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8a427eb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

*/
export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch {
const { dnsResolver, logger } = options;
const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve headers in DoH feedback requests

When --dns-resolver https is used, this wrapper delegates supabase-js requests to legacyDohFetch, but supabase-js supplies init.headers as a Headers instance and legacy-http-dns.ts rebuilds it with { ...init?.headers, Host: ... }; spreading Headers yields no entries. The resulting submit, preview, and delete requests lose apikey, content type, and the x-feedback-* capability headers, so feedback operations fail in the exact environments that need the DoH fallback. Fresh evidence beyond the earlier transport-wiring comment is this incompatibility between the newly wired supabase-js transport and the existing DoH header reconstruction; clone with new Headers(init.headers) and set Host instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client

@avallete avallete left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing to fix before merge (the open Codex P1 on debug URLs, commented on that thread), one doc fix (PR description still says user_id is never sent), and a couple of non-blocking notes inline.

the PR description says "The access token is never sent; user_id is never sent", but the current code sends the consent-gated gotrue UUID as user_id (add.handler.ts:91), and SIDE_EFFECTS.md documents that correctly. Since the description is what privacy sign-off reads, could you update that bullet to match?

const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
// The logger's write is synchronous (a gated stderr write); running it
// at this plain-fetch boundary keeps the wrapper a `typeof fetch`.
Effect.runSync(logger.http(method, url));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds like an actual risk.

Maybe strip or redact the query string in legacyFeedbackFetch before logger.http(...) the Management API debug logger never has this problem because its credentials ride in headers.

*/
export function legacyFeedbackFetch(options: LegacyFeedbackFetchOptions): typeof globalThis.fetch {
const { dnsResolver, logger } = options;
const dohFetch = legacyDohFetch({ dnsResolver, innerFetch: options.innerFetch });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the ...init?.headers spread in legacyDohFetch preserves apikey/x-feedback-token as-is. Not an issue for this client

Comment on lines +39 to +40
const deleted = await runSupabaseLive(
[

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Valid but minor, a leaked row only happens on assertion failure between the add and the delete, staging-only, and the message self-identifies as test data. Fine to wrap the delete in a finally for hygiene, but I wouldn't block on it.

const config = {
message: Argument.string("message").pipe(
Argument.withDescription(
"Freeform feedback. Bare words are joined with spaces. 1000 character limit.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The char limit is documented here but only enforced server-side, so an over-limit message surfaces as a raw PostgREST error classified externalNetwork, so a user mistake gets counted as a backend failure in the actionability KPIs. A client-side length check that fails with an invalidInput classified error before any request would be nice. Non-blocking if you'd rather do it as a follow-up.

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