Skip to content
Loadout

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.