2. Run the web app
Paste the script’s WEB block into apps/web/.env.local. Everything in it is
filled in except the JWT signing key, which only the dashboard can give you.
Choose the JWT signer
The web app mints short-lived Supabase JWTs for headless clients (that is how the agent authenticates without holding your password). Supabase accepts those tokens only if they are signed the way your project verifies — and which way that is depends on the project’s vintage, not on your preference.
The setup script already told you which one you need, by reading your project’s public JWKS. Set exactly one:
| Mode | Env var | When |
|---|---|---|
| ES256 (preferred) | SUPABASE_JWT_SIGNING_KEY — a private JWK | The project is on asymmetric signing keys |
| HS256 (legacy) | SUPABASE_JWT_SECRET — the shared secret | The project still uses the legacy symmetric secret |
ES256
Supabase keeps only the public half of a signing key, so you mint the keypair and hand it the public side:
supabase gen signing-key --algorithm ES256 # prints a private JWK, including its `kid`Then Project Settings → JWT Keys → Create standby key → “Import an existing key” and paste the private JWK.
Two traps here. “Import an existing key” is a choice inside that flow — miss it and you get a key whose private half Supabase keeps, which cannot sign anything. And leave the key as standby; do not rotate. A standby key’s public half is published immediately and PostgREST accepts its signatures. Rotating only changes which key Supabase signs user sessions with — no gain here, and it moves every signed-in user onto your key.
Put the same private JWK — one line of JSON, kid included — in
SUPABASE_JWT_SIGNING_KEY.
HS256
Copy Project Settings → API → JWT Settings → JWT Secret into
SUPABASE_JWT_SECRET.
Setting neither is refused loudly at mint time, rather than producing a token no verifier will accept.
Start it
pnpm install
pnpm web # next dev, first free port from 3000 upSign in with the email and password the setup script printed.
Deploy it
For anything beyond local use, build the container:
pnpm run web:docker:buildThe image carries no project settings: pass the WEB block as runtime env
vars (docker run --env-file apps/web/.env.local …). The server hands
SUPABASE_URL and SUPABASE_PUBLISHABLE_OR_ANON_KEY to the browser with each
page, so the same image serves any Supabase project. Deploy it anywhere that
runs a container and can reach your Supabase project.
Put it behind HTTPS and restrict who can reach it. The app is the thing that mints Supabase JWTs; treat it as sensitive infrastructure, not a static site.
Reference: docs/features/pat-auth.md
covers the token-exchange design, signer modes, and the security posture in
full.