Fix
Supabase Edge Function secrets
Answer
Supabase Edge Functions read secrets with Deno.env.get. Locally they load supabase/functions/.env when the stack starts. Production secrets are set in the Dashboard or with supabase secrets set, and a deployed function reads a new secret without a redeploy. A custom secret name cannot start with SUPABASE_.
- Next.js 16.4.1
- React 19.3
- @supabase/ssr 0.12.7
- @supabase/supabase-js current, installed alongside @supabase/ssr
Last verified 2026-10-15
Agents still put an Edge Function secret in the Next.js .env, name a custom secret with a SUPABASE_ prefix, or expect the project-root .env to reach the function. The Next.js + Supabase pack pins Next.js 16.4.1, React 19.3, @supabase/ssr 0.12.7, and @supabase/supabase-js (current, installed alongside @supabase/ssr). It covers Auth, RLS, and the publishable key in the Next.js app. It does not set Edge Function secrets. RLS is /fixes/supabase-rls-no-policy-is-deny-all. getClaims() is /fixes/supabase-ssr-getsession-vs-getclaims.
Local and production load secrets differently. Locally, Edge Functions read supabase/functions/.env, loaded on supabase start. A .env.example is not enough, because the runtime reads the values. A root .env is what config.toml reads through env(). The function still needs the value in supabase/functions/.env. A file such as .env.local is read only when you pass it to supabase functions serve with --env-file. If Deno.env.get comes back empty, restart the stack or serve with that flag, and confirm you edited the file that runtime reads. Add supabase/functions/.env to .gitignore.
Production secrets are set in the Dashboard or with the CLI. supabase secrets set NAME=value sets one secret. supabase secrets set --env-file .env pushes every name in that file. supabase secrets list shows the names on the remote project. Creating or deleting a production secret requires the Owner or Administrator role. Developers can view secrets and cannot change them. A deployed function reads a new secret immediately, with no redeploy. A secret name cannot start with SUPABASE_. That prefix is reserved, and the Dashboard and the Management API reject it.
Read a secret with Deno.env.get(name). Do not print the value. Return whether it is set. Inside an Edge Function, Supabase injects the newer SUPABASE_URL, SUPABASE_DB_URL, SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS, and SUPABASE_JWKS. It still injects the legacy SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY. SUPABASE_PUBLISHABLE_KEYS and SUPABASE_SECRET_KEYS are JSON dictionaries. Parse them and pass the default entry to createClient. SUPABASE_SECRET_KEYS bypasses Row Level Security, so use it in the function and never in a browser. NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY are the Next.js app names. They do not replace supabase/functions/.env. Checked against https://supabase.com/docs/guides/functions/secrets.
Last verified 2026-10-15. doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed.
Pro
Next.js 16 + Supabase Pro rules, $1+
$1+ · pay what you want
RLS policy recipes, getClaims, Stripe webhooks, and Cache Components. Deeper than the free Next.js + Supabase pack.
Each pack ships as AGENTS.md, CLAUDE.md, Cursor .mdc, and Copilot instructions, with 12 months of free updates.
From the Next.js 16 + Supabase Pro rules
Enable RLS on every table in `public`. A table with no policy denies that command for `anon` and `authenticated`. A table with RLS off is readable and writable through the Data API. The free pack already says this. Write the policy, do not stop at `enable row level security`.
Name the role with `to authenticated`. Compare `(select auth.uid())` to `user_id`. The `select` wrapper is the form in the Supabase RLS guide. `auth.uid()` is null when the JWT is missing, and `null = user_id` is never true.
`select` and `delete` use `using`. `insert` uses `with check`, so a user cannot insert a row for someone else. `update` uses both: `using` picks the row, `with check` locks the row after the write, including `user_id`.
The secret key, and the legacy `service_role` key, bypass RLS. Use one only on the server, after you have checked the caller yourself. A browser client never gets that key. Supabase also says a secret key does not bypass RLS when the request still carries a user access token.Next.js 16 + Supabase Pro rules, $1+Next.js + Supabase pack — Next.js 16.4 and Supabase SSR, with the APIs that actually work.
doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed
Sources in the pack
Docs
Questions
- Where do Supabase Edge Functions read secrets?
- Supabase Edge Functions read secrets with Deno.env.get. Locally they load supabase/functions/.env when the stack starts. Production secrets are set in the Dashboard or with supabase secrets set, and a deployed function reads a new secret without a redeploy. A custom secret name cannot start with SUPABASE_.
- Why is Deno.env.get empty in a local Edge Function?
- The function reads supabase/functions/.env, loaded on supabase start. A root .env or a .env.example does not fill that runtime. Restart the stack, or serve with --env-file supabase/functions/.env.
- Can a custom Edge Function secret start with SUPABASE_?
- No. That prefix is reserved for variables Supabase injects, and the Dashboard and the Management API reject it. The function already receives the newer SUPABASE_URL, SUPABASE_DB_URL, SUPABASE_PUBLISHABLE_KEYS, and SUPABASE_SECRET_KEYS, plus the legacy SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY.
- When was this Supabase Edge Function secrets rule last checked?
- Last verified 2026-10-15. doc-verified; TypeScript snippets pass tsc --strict (TypeScript 5, @types/react 19) with zod and the Supabase client stubbed.