Skip to Content
DocumentationSelf-host2. Run the web app

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:

ModeEnv varWhen
ES256 (preferred)SUPABASE_JWT_SIGNING_KEY — a private JWKThe project is on asymmetric signing keys
HS256 (legacy)SUPABASE_JWT_SECRET — the shared secretThe 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 up

Sign in with the email and password the setup script printed.

Deploy it

For anything beyond local use, build the container:

pnpm run web:docker:build

The 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.

Last updated on