Files
supabase/supabase
kanad 1b01a9c8af feat: table for collecting interfaces feedback (#48420)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Database migration — adds a table for collecting free-form product
feedback submitted from Supabase interfaces (starting with the CLI and
the MCP server), including support for deleting a submission via a
server-issued token.

## What is the current behavior?

There is no destination for feedback submitted from the CLI or MCP
server. The existing `feedback` and `feedback_comments` tables are
scoped to the docs feedback widget, so interface feedback would
otherwise end up as ad-hoc GitHub issues — with no way to revoke
something submitted by accident (e.g. a secret key pasted into the
message).

## What is the new behavior?

Adds `public.interfaces_feedback`:

| Column | Type | Notes |
| --- | --- | --- |
| `id` | `bigint` identity | primary key (not exposed through the API) |
| `created_at` | `timestamptz` | `not null default now()` |
| `feedback` | `text` | `not null`, ≤ 1000 chars — the free-form
feedback |
| `delete_token` | `uuid` | server-generated, `unique not null`;
authorizes deleting the row |
| `user_agent` | `text` | ≤ 255 chars; interface + version, also
identifies the source interface |
| `user_id` | `text` | optional, ≤ 255 chars; unverified,
interface-defined identifier |
| `project_ref` | `text` | optional, ≤ 255 chars |
| `metadata` | `jsonb` | ≤ 8 KB catch-all |

**Submission** happens exclusively through a `SECURITY DEFINER`
function, `submit_interfaces_feedback(...)`, which inserts the row and
returns the server-generated `delete_token` exactly once. There is no
insert grant or policy on the table itself, so clients cannot insert
directly or supply their own token — the function is the only door.
Execute is revoked from `PUBLIC` and granted to `anon` only (both
statements matter: local and hosted databases have different default
function ACLs).

**Deletion** is a hard `DELETE` authorized by presenting the token in an
`x-feedback-token` request header. RLS policies compare the row's
`delete_token` against that header (`current_setting('request.headers',
...)`) — the URL filter is never the security boundary; a request
without the matching header affects zero rows, even with no filter or
someone else's token in the filter. Tokens never expire (the delete
right shouldn't lapse). The header is cast to `uuid` and compared
against the untransformed column, so lookups use the unique index on
`delete_token` even for header-only reads; a malformed token header is
rejected with a `400` (`22P02`), consistent with what a malformed URL
filter value already returns.

**Context gate (defense-in-depth)**: rows submitted with a `project_ref`
and/or `user_id` additionally require the matching
`x-feedback-project-ref` / `x-feedback-user-id` headers — on both reads
and deletes — so a leaked bare token can neither read the submission
text back nor remove the row. A `NULL` column imposes no requirement:
context-free rows keep token-only behavior, and extra headers sent
against them are ignored (this keeps clients that always send their
current context from being locked out of rows submitted without it).
These are client-supplied, unverified values, so the gate is a knowledge
factor rather than an identity check; clients should persist
`{delete_token, project_ref, user_id}` together at submit time and
re-present them byte-exact (`project_ref`/`user_id` are compared as
plain text).

**Reads** are limited to `grant select (feedback, delete_token)` behind
the same token-scoped policy: a token-holder can preview their own
submission text before deleting and confirm the delete matched (`Prefer:
count=exact` → `Content-Range: */1` vs `*/0`). No other columns are
readable by any API role; `delete_token` needs select because PostgREST
requires a WHERE clause on deletes and filter columns require select
privilege.

Verified locally via `supabase db reset` + the local REST API: token
issuance, token-scoped preview and delete, zero-row results for
missing/wrong/malformed tokens (including a victim's token in the filter
without the header), the full context-gate matrix (project+user,
project-only, and context-free rows, incl. lenient extra-header
behavior), denied direct inserts and column reads, length caps enforced
through the function, and no execute for `authenticated`.

## Additional context

Linear tickets: [CLI-1946](https://linear.app/supabase/issue/CLI-1946),
[CLI-1999](https://linear.app/supabase/issue/CLI-1999)

The client-side flows (`supabase feedback add` / `feedback delete` in
the CLI, and the MCP tool) land separately in their respective repos and
will call the RPC / DELETE endpoint described above.

Supersedes #48378 — recreated on a fresh git branch so that the Supabase
preview branch used for testing this table isn't shared with unrelated
work.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for collecting and storing feedback submitted through
interfaces.
* Feedback can include submission source, timestamps, user details,
project references, and additional metadata.
* Added secure feedback submission with controlled access to protect
submitted information.
* Added support for authorized feedback removal using a secure deletion
token.
* Added safeguards to validate feedback content and restrict access to
permitted information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:32:51 -07:00
..