Skip to content

feat: eval the API keys guide in supabase.com/docs - #212

Merged
czenko merged 21 commits into
mainfrom
docs/api-key-eval
Sep 1, 2026
Merged

feat: eval the API keys guide in supabase.com/docs#212
czenko merged 21 commits into
mainfrom
docs/api-key-eval

Conversation

@czenko

@czenko czenko commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes DOCS-1301

Problem

We point agents at the API keys guide. Nobody has measured whether reading it is enough to keep the secret key out of a browser bundle.

No existing eval answers that. resolve-sdk-001-legacy-key-migration is Node scripts, server-side throughout. build-functions-004/005/006 are Edge Function auth, where the secret key is legitimately server-side. None of them builds a client bundle, so none can catch a secret key shipped to a browser.

Solution

  • Adds build-docs-003-api-keys-guide. The guide is the subject under test. The prompt is a product request plus the guide's url, and never says key, publishable, secret, service role, server, expose, or environment variable.
  • Seeds a Vite app with two screens. Sign-up is public and correct with a publishable key. The roster lists everyone who signed up with their email, which lives in auth.users and no publishable key reaches. The roster is the one place where the convenient answer ships the whole table to the browser.
  • Scores nine checks. Six read the built bundle and client source, two probe the running stack, one confirms the guide was retrieved.
  • Adds ctx.stackStatus() to the local-stack scoring context, which reports the stack's url plus its publishable, secret, and legacy anon keys. The scorer needs all four, and getClient() only carries the publishable key.

Checks that exist to stop a false green

  • roster returns every signed-up email pairs with the bundle checks, which a run that built nothing passes trivially.
  • client source contains a signUp call pairs with client bundle carries a publishable or anon key, which passes for a key that ships and is never called.
  • no client role can read another user's email catches the design that dodges the elevated call entirely by copying addresses into a table anyone can read. Everything else scored green before this check existed.

Where the email check came from

Not from the page under test. The page frames publishable-key safety as an RLS checklist, and the shortcut of copying addresses into a readable table satisfies that checklist while leaking. It came from api/securing-your-api, which treats grants and RLS as separate layers.

What this eval does not score

Two checks were tried and removed, because both measured the platform rather than the page. Keeping either means a CLI upgrade reports as a docs change.

  • Which key format the client uses. The build injects a placeholder over VITE_SUPABASE_ANON_KEY, so a client wired through that name never carries a real key into the bundle and the format is not observable.
  • Which key format the server uses. The Edge Function runtime decides which keys a function is handed, and the pinned CLI hands over legacy ones only. A function reading them follows the runtime, not the guide.

Known limitations

  • The roster probe seeds two users and requires both addresses in one response. An agent that paginates to fewer than two rows per page would fail on a design choice, not a defect. auth.admin.listUsers() defaults to 50 per page and PostgREST to 1000 rows, so this is a narrow edge, but it would surface as an unexplained red.
  • no secret-bearing env var is client-exposed passes by design. It reads every .env outside supabase/ and fails on a secret in any of them. An agent that keeps the secret in the function env passes, and that is the intended outcome.
  • client source contains a signUp call is a literal match. Dead code satisfies it, and a call reached only from outside client source does not. Proving the screen works needs a driven DOM the scorer does not have.
  • Authorization is out of scope. The roster probe calls as a signed-in user, so a roster gated on sign-in counts as working. Restricting the endpoint to staff needs a role in the seed and is what build-rls-003-org-roles-permissions measures.
  • suite: regression, not benchmark. It is a regression detector for the guide, and it reports no failure against the published page.

Baseline

claude-code-sonnet-5 with and without skills, 3 runs each, CI run 33549257417: 9/9 on all six runs. Results are committed to this branch.

No check fails against the published guide, which is the floor a regression detector needs. A later docs change that breaks key placement shows up as a drop from this.

The scorer was also pointed at a revised draft of the guide, and that came back 9/9 on all six runs too, so the draft introduces no regression.

Manual testing

  1. Free the Supabase ports. Run docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E '5432[12]->' and stop whatever holds them.
  2. Run pnpm eval:dry -- --eval build-docs-003-api-keys-guide. It plans 18 experiments.
  3. Run pnpm typecheck. Clean.
  4. Read README.md in the eval folder. Confirm the persona rules and the check rationale match the scorer.
  5. The scorer was checked against thirteen throw-away solutions with no agent in the loop. A correct one passes every check except the transcript check, which a no-agent run always fails, and each bad one fails the checks it was built to fail.

@czenko
czenko requested a review from a team August 17, 2026 23:37
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
evals Ready Ready Preview Sep 1, 2026 7:38pm UTC

Request Review

@czenko
czenko force-pushed the docs/api-key-eval branch from 82f8870 to c64044c Compare August 24, 2026 16:59
@czenko
czenko changed the base branch from main to core/share-docs-guide-read-check August 24, 2026 17:00
@czenko
czenko force-pushed the core/share-docs-guide-read-check branch from 53a5412 to 8c98b7c Compare August 24, 2026 17:09
@czenko
czenko force-pushed the docs/api-key-eval branch 2 times, most recently from cf1c7a9 to 89c0ffa Compare August 24, 2026 17:12
@czenko czenko added the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 24, 2026
Comment thread evals/build-docs-003-api-keys-guide/PROMPT.md Outdated
@czenko czenko removed the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 25, 2026
@czenko
czenko force-pushed the docs/api-key-eval branch from 8b6cb00 to 78ee2ac Compare August 25, 2026 17:20
@czenko
czenko changed the base branch from core/share-docs-guide-read-check to main August 25, 2026 17:20
czenko and others added 3 commits August 25, 2026 10:25
Adds `ctx.stackStatus()` to the local-stack scoring context, returning the
stack's url plus its publishable, secret, and legacy anon keys. `getClient()`
only carries the publishable key, so a scorer that needs the others had to
parse `supabase status` a second time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `build-docs-003-api-keys-guide`. The guide is the subject under test: the
prompt is a product request plus the guide's url, and never says key,
publishable, secret, service role, server, expose, or environment variable.

Seeds a Vite app whose roster screen needs an address that no publishable key
reaches, so the convenient answer ships the whole table to the browser. Scores
nine checks across the built bundle, the client source, the running stack, and
whether the guide was retrieved at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mattrossman

Copy link
Copy Markdown
Collaborator

(Merging main in so that it can use the new 3-run averaging strategy)

Comment thread evals/build-docs-003-api-keys-guide/access.ts Outdated
@mattrossman mattrossman added the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 26, 2026
Comment thread evals/build-docs-003-api-keys-guide/EVAL.ts
Comment thread evals/build-docs-003-api-keys-guide/bundle.ts
@mattrossman mattrossman removed the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 26, 2026

@mattrossman mattrossman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good overall and tested a couple runs on my end.

I'd just call attention to this potential flakiness I experienced a couple times.

…in one place

The roster probe called the endpoint with the publishable key as its bearer
token, so a roster gated on being signed in returned 401 and failed a check
about whether the roster works. It now sends the fixture user's access token,
which a roster open to anyone answers too.

`checkBundle` and the access probes return keyed check records, so `EVAL.ts`
lists all nine checks in one array instead of spreading them in from two
files. A seed that fails now reports both access checks as unproven rather
than replacing them with a single failure, keeping the check count stable.

`no secret-bearing env var is client-exposed` reads Vite's inlined prefixes off
`vite.config.ts`, so a renamed `envPrefix` stays in range of the check.
@czenko czenko added the run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes label Aug 31, 2026
czenko and others added 4 commits August 31, 2026 11:29
The bundle check accepted the legacy anon key alongside the publishable key,
so a run that reached for the deprecated format scored clean. `client uses a
publishable key, not the legacy anon key` now makes that a separate claim,
leaving the existing check as the control on whether the client is wired up.

The complaints behind it are FDBKIN-19189, FDBKIN-32569, FDBKIN-6545, and
DOCS-313, all users who could not tell which key format to reach for.

The server side stays unscored. The Edge Function runtime injects
SUPABASE_SERVICE_ROLE_KEY, so a correct implementation still holds a legacy
credential there, and failing that would measure a platform default.
The Edge Function runtime fills SUPABASE_SERVICE_ROLE_KEY and
SUPABASE_ANON_KEY with legacy keys, so keeping the secret off the client is
not the same as using the new key format on the server. `server reads no
legacy key variable` scores that separately.

The guide says to use the publishable and secret keys, so a server holding a
deprecated credential is a gap in the guide rather than a reasonable answer.
Scored as a source scan because both formats resolve to the same Postgres
role, leaving the source as the only place the difference shows.

Measured on the pinned CLI: the runtime injects no new-format key, so a
function has to supply its own. Reading the secret key from its own name in
`supabase/functions/.env` works and passes every other check. README records
the working implementation and the CLI the behaviour depends on.

Splits `walk` and `readText` into `files.ts`, now that the bundle scan and the
server scan both need them.
czenko and others added 2 commits August 31, 2026 16:21
The eval inherited the sandbox default of 2.67.1, whose Edge Function
runtime injects only legacy keys. A key-format eval cannot fairly score the
server side on a runtime with no new-format key to offer, so pin 2.109.1 the
way the other key evals do. Its runtime injects SUPABASE_SECRET_KEYS.

README now states the passing shapes for that runtime.

@barryroodt barryroodt 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.

Looking good, I left a few inline comments on scorer cases that can still pass when they should fail.

Comment thread evals/build-docs-003-api-keys-guide/bundle.ts
Comment thread evals/build-docs-003-api-keys-guide/server.ts Outdated
Comment thread evals/build-docs-003-api-keys-guide/bundle.ts Outdated
Comment thread evals/build-docs-003-api-keys-guide/bundle.ts
czenko and others added 2 commits September 1, 2026 10:51
The client env scan skipped `export VITE_ADMIN=sb_secret_...`, because the
`export ` prefix broke the name match, and skipped a secret behind a prefix
declared through a variable, because only a literal `envPrefix` parsed. It now
reads every `.env` outside `supabase/` and fails on a secret in any of them,
parsing neither names nor prefixes. A secret in the client's env is exposed
whichever name holds it.

The server check matched variable names only, so a legacy key bound to an
alias passed. It now also decodes JWTs under `supabase/functions` and fails on
an `anon` or `service_role` role. Renamed to `no legacy key reaches the
server`, which is what it now proves.

`client source calls signUp` overstated a literal match, since dead code
satisfies it. Renamed to `client source contains a signUp call`. Proving the
screen works needs a driven DOM the scorer does not have, so the claim is
narrowed rather than strengthened.

Credential detection moves to keys.ts, now that the bundle scan and the server
scan both need it.
czenko and others added 2 commits September 1, 2026 12:23
Two checks measured the platform rather than the page, so a CLI change would
report as a docs change.

`client uses a publishable key, not the legacy anon key` could not observe its
own condition. The build injects a placeholder over VITE_SUPABASE_ANON_KEY, so
a client wired through that name carries no real key into the bundle.

`no legacy key reaches the server` scored which keys the Edge Function runtime
hands a function. The pinned CLI hands over legacy ones only, so a function
reading them follows the runtime, not the guide.

Nine checks remain. The positive controls stay: without them a run that built
nothing scores full marks.
@czenko

czenko commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Update: I added a couple more checks since Matt's review and have since reverted them since they proved to not reflect on the document but on the functionality of the CLI.

@czenko
czenko merged commit f0738d6 into main Sep 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-evals-changed Add to a PR to refresh only the benchmark evals that have had changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants