Supabase Edge Functions
Purpose
Guide the authoring, deployment, and debugging of Supabase Edge Functions (Deno runtime), covering Deno-specific module constraints, non-interactive terminal auth, LLM provider REST API integration, error extraction from the client, and cache invalidation when bad data is persisted by a function.
When to use
When writing a new Supabase Edge Function or debugging a deployment, error, or data issue that originates from one. Apply before first deployment to avoid the class of Deno module failures and auth hangs that are non-obvious coming from a Node.js background. Also apply when integrating LLM providers (Gemini, OpenAI, Anthropic) via REST API.
Inputs expected
- Edge function code or error message
- Deployment environment (local dev, CI, PowerShell)
- LLM provider in use (if applicable)
- Client-side caching strategy (if applicable)
Guiding principles
- Don't use npm SDK packages in Deno — use the provider's REST API directly. npm-style imports (
npm:@google/generative-ai@0.21.0) fail to resolve in Deno edge functions. Call the LLM provider's REST API viafetch— this is more stable, has no dependency on Deno's npm compatibility layer, and works across all providers with a single pattern. FunctionsHttpErrorhides the real error — extract it fromerror.context. Whensupabase.functions.invokethrowsFunctionsHttpError, the.messageis always "non-2xx status code". The actual error body from the edge function is inerror.context— callerror.context.clone().json()to read it.supabase loginhangs in non-interactive terminals — useSUPABASE_ACCESS_TOKENinstead. In PowerShell and CI environments,supabase loginblocks waiting for user input. SetSUPABASE_ACCESS_TOKENas an environment variable before runningsupabase functions deployto authenticate without interaction.- Check LLM model names and quota settings when calls return 404 or
limit: 0.gemini-1.5-flashis deprecated on the v1beta REST API and returns 404. Usegemini-2.0-flashorgemini-flash-lite-latest. A quota error withlimit: 0indicates the Google Cloud project has no free tier enabled — this is usually an org policy restriction, not exhausted quota. - Client-side caches will serve bad data even after the edge function is fixed — delete the bad row. When an edge function writes incorrect data to a database cache table, the client will keep reading the bad row because stale checks typically only trigger when no row exists for the expected key. Delete the affected row directly in the Supabase SQL Editor to force a fresh fetch.
- New-format Supabase PATs (
sbp_v0_…) are rejected by older CLI versions. PATs issued in 2026 start withsbp_v0_, and the Supabase CLI (v2.101.0) rejects them with "Invalid access token format". There is no CLI workaround — set Edge Function secrets directly via the Supabase dashboard (Edge Functions → function → Secrets) instead. - On Windows,
supabase loginstores credentials in Windows Credential Manager, not~/.supabase/credentials. The token is saved underLegacyGeneric:target=Supabase CLI:supabaseand can conflict with theSUPABASE_ACCESS_TOKENenvironment variable or carry restricted permissions for management API operations. Remove it withcmdkey /delete:"Supabase CLI:supabase"if it causes auth conflicts. - Updating a secret does not refresh already-warm function instances.
supabase secrets set NAME=... --project-ref <ref>leaves warm instances reading the stale value — redeploy the function (supabase functions deploy <fn>) so new instances boot with the new secret. - The functions gateway rejects non-JWT bearer tokens before your code runs. Any bearer that is not a JWT fails at the gateway with
UNAUTHORIZED_INVALID_JWT_FORMAT. A function that must accept a non-JWT shared secret (a system or cron caller) has to setverify_jwt = falseinsupabase/config.tomland do its own authorization inside. Keepverify_jwt = truefor functions only ever called with a real user JWT (e.g. a signed-in user submitting feedback); flip it to false only when a trusted non-JWT caller is involved. - Never pass
--no-verify-jwttosupabase functions deploy. It turns OFF the gateway JWT check for that function (a silent security regression); deploy WITHOUT the flag to keep the defaultverify_jwt=true, which the CLI takes fromconfig.tomlwhere only the intentionally-public functions are listed asfalse. Verify the live state empirically: an unauthenticated POST returns the gateway body{"code":"UNAUTHORIZED_NO_AUTH_HEADER"}(401) whenverify_jwt=true, versus the function's own JSON when it is off. - Schedule server-side invocation with
pg_cron+pg_net+ a Vault secret. Apg_cronjob calls adispatch_*()SQL function that fires due rows at the edge function viapg_net, authenticated with a Vault secret (vault.create_secret) that must equal the function's env-var secret. Because that bearer is not a JWT, the target function needsverify_jwt = false. Verify delivery by checkingnet._http_responsefor a200. - For account deletion, verify the JWT then use the service role — and de-identify rather than hard-delete. In the function, verify the caller JWT, then call
auth.admin.deleteUser(service role), which cascadespublic.usersvia its FK. To retain non-identifying activity, first snapshot coarse stats to an archive table keyed by the user's UUID; anevent_logwith no FK toauth.userssurvives keyed by UUID, so retained activity stays linkable without PII.
Process
- Write the edge function using
fetchfor external API calls. Do not import npm SDK packages — use the provider's REST API endpoint directly. Structure the function to return a consistent JSON response with explicit status codes. - Authenticate for deployment without interactive login. Set
SUPABASE_ACCESS_TOKENin the environment before deploying:$env:SUPABASE_ACCESS_TOKEN = "sbp_..."(PowerShell) orexport SUPABASE_ACCESS_TOKEN="sbp_..."(bash). Then runsupabase functions deploy <function-name>. - Test the deployed function and extract errors properly. Call via
supabase.functions.invoke. On error, readerror.context.clone().json()— noterror.message— to see the actual response from the function. - Verify LLM model availability and quota. If calls return 404, check the model name against the provider's current API. If quota shows
limit: 0, check GCP → APIs & Services → Quotas for org-level policy restrictions, not just per-project limits. - Invalidate bad cached data by deleting the affected rows. If incorrect data was written to a cache table, delete the row via the Supabase SQL Editor. Do not rely on the client's stale check — it only triggers on absence, not on bad data.
Output format
- Deployment checklist — auth method confirmed, function deployed, test call successful
- Error diagnosis — root cause identified from
error.context, noterror.message - LLM integration notes — model name confirmed, quota verified, REST API endpoint used
- Cache state — any bad rows deleted, fresh fetch confirmed
Quality checklist
- No npm SDK packages imported — all external calls use
fetchagainst REST APIs - Deployment uses
SUPABASE_ACCESS_TOKEN, not interactivesupabase login - Error handling reads
error.context.clone().json()onFunctionsHttpError - LLM model name verified against current provider API (not deprecated
gemini-1.5-flash) - Any bad cached rows deleted directly before verifying the fix
- New-format
sbp_v0_PATs set via the dashboard if the CLI rejects them - On Windows, no stale
supabase logintoken in Credential Manager conflicting withSUPABASE_ACCESS_TOKEN -
verify_jwt = falseset only for functions with non-JWT callers (cron/system), which authorize internally - Deployed without
--no-verify-jwt; unauthenticated POST to a protected function returnsUNAUTHORIZED_NO_AUTH_HEADER(401) - Scheduled functions use
pg_cron+pg_netwith a Vault secret matching the function's env-var secret - Account-deletion functions verify the JWT, use the service role, and de-identify retained activity by UUID
Avoid
- Importing npm SDK packages in Deno (
npm:@google/generative-aiand similar) — they fail silently or throw at import time - Reading
error.messagefromFunctionsHttpError— it is always generic; the real error is inerror.context - Running
supabase loginin PowerShell or CI — it hangs waiting for input; useSUPABASE_ACCESS_TOKEN - Using deprecated Gemini model names (
gemini-1.5-flash) — they return 404 on the v1beta API - Assuming a quota error with
limit: 0means exhausted quota — it usually means the GCP project has no free tier enabled - Fighting the CLI to accept a new-format
sbp_v0_PAT — older CLI versions reject it; set secrets via the Supabase dashboard instead - Overlooking Windows Credential Manager when
supabase loginauth misbehaves on Windows — the stored token can conflict withSUPABASE_ACCESS_TOKEN; clear it withcmdkey /delete - Leaving
verify_jwt = trueon a function called by a non-JWT system/cron caller — the gateway rejects it before your code runs; setverify_jwt = falseand authorize inside - Deploying with
--no-verify-jwt— it silently disables the gateway check; letconfig.tomldecide and prove the live 401 - Hard-deleting a user's activity on account deletion when you need to retain aggregates — snapshot coarse stats keyed by UUID and de-identify instead
Example usage
Edge function calling Gemini REST API returns 404.
supabase loginhangs in PowerShell before deployment. Client keeps showing wrong data after the function was fixed.
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.