new row violates row-level security policy (Supabase)
Your insert or update is blocked by RLS. Add a policy for that exact write, without turning RLS off.
Supabase Edge Functions check for a Supabase JWT by default, and Stripe doesn't send one, so the webhook gets 401 before your code runs. Turn verify_jwt off for that one function, then verify Stripe's signature yourself with constructEventAsync.

You point a Stripe webhook at your Supabase Edge Function, and every delivery fails with a 401. The response body says Missing authorization header, and your function's logs show nothing, because your code never ran. Supabase's troubleshooting guide lists that exact body, {"code":401,"message":"Missing authorization header"}, and says it comes from the platform's built-in check that "runs before your code. When it fails, your function never executes" (Supabase: Edge Function 401 error response). A webhook can't pass that check, so you turn it off for that one function and verify Stripe's signature yourself instead.
1. Turn off JWT verification for the webhook function only (Verified locally). In supabase/config.toml, use your function's folder name:
[functions.stripe-webhook]
verify_jwt = false
Supabase's function configuration docs use this exact block: "If you want to use Edge Functions without Authorization checks (commonly used for Stripe webhooks), you can configure this in your config.toml" (Supabase: Function configuration). Leave every other function on the default. In our test, the same function and the same signed test event got 401 Missing authorization header with verify_jwt = true, and 200 after we set it to false and restarted the local stack (Verified).
2. Redeploy so the hosted function picks it up (Docs-only: we didn't deploy to a hosted project). Deploy with the CLI after the config.toml change, or pass the flag:
supabase functions deploy stripe-webhook --no-verify-jwt
The CLI reference describes --no-verify-jwt as "Disable JWT verification for the Function" (Supabase CLI: functions deploy), and the config reference notes that "the --no-verify-jwt flag overrides this configuration" (Supabase: CLI config). Supabase's troubleshooting guide also lists a Dashboard route: open the function's details and toggle off JWT verification (Supabase: Edge Function 401 error response).
3. Verify the Stripe signature in the function (Verified locally). This step is not optional. With the platform check off, your signature check is the only thing between the internet and your handler. This is Supabase's Stripe webhook example, which we ran as written:
import Stripe from 'npm:stripe@^22'
import { withSupabase } from 'npm:@supabase/server@^1'
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string)
// Deno needs the Web Crypto provider for signature checks
const cryptoProvider = Stripe.createSubtleCryptoProvider()
export default {
fetch: withSupabase({ auth: 'none' }, async (req) => {
const signature = req.headers.get('Stripe-Signature')
const body = await req.text() // the raw body, not req.json()
let event
try {
event = await stripe.webhooks.constructEventAsync(
body,
signature!,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
undefined,
cryptoProvider
)
} catch (err) {
return new Response(err.message, { status: 400 })
}
// only now act on event.type
return Response.json({ ok: true })
}),
}
Source: Supabase: Handling Stripe Webhooks. Supabase's auth guide gives the same pattern for any external webhook: "Use auth: 'none' to skip the wrapper's credential check, then verify the provider's signature inside the handler. Keep verify_jwt = false" (Supabase: Securing Edge Functions). Stripe's side: verify "using the JSON payload, the Stripe-Signature header, and the whsec_ webhook signing secret", and "Stripe requires the raw body of the request to perform signature verification" (Stripe: Receive events in your webhook endpoint).
Use constructEventAsync, not constructEvent. Supabase's guide warns that "Deno has no synchronous Node crypto", and that the synchronous call "throws 'SubtleCryptoProvider cannot be used in a synchronous context' on this runtime" (Supabase: Securing Edge Functions). We got that exact error from the synchronous call with the Web Crypto provider (Verified).
In our test, with verify_jwt = false and this code, a correctly signed event got 200, and a missing header, a changed body and a signature made with a different secret each got 400, logged as a failed signature check (Verified). Offline with the Stripe library, re-formatting the JSON before checking it failed, and so did a correctly signed event whose timestamp was 10 minutes old, with "Timestamp outside the tolerance zone" (Verified). Stripe: "Our libraries have a default tolerance of 5 minutes", and "Don't use a tolerance value of 0", which "disables the recency check entirely" (Stripe: Receive events in your webhook endpoint).
4. Keep the signing secret in function secrets, not in code (Verified locally with supabase/functions/.env; production steps Docs-only):
supabase secrets set STRIPE_WEBHOOK_SIGNING_SECRET=whsec_...
supabase secrets set STRIPE_API_KEY=sk_...
Locally, "Edge Functions read secrets from supabase/functions/.env", and that file belongs in .gitignore. For production, supabase secrets set sets secrets "without a .env file" (Supabase: Environment variables). Use the whsec_ value for this exact endpoint. Stripe: "Stripe generates a unique secret key for each endpoint. If you use the same endpoint for both test and live API keys, the secret is different for each one" (Stripe: Receive events in your webhook endpoint). A wrong secret gives a 400 "No signatures found" error, which has its own page: No signatures found matching the expected signature for payload.
verify_jwt is enabled (the default), the platform inspects the Authorization header of every request before your function runs… If the header is missing, malformed, or signed with a different key, the platform returns a 401 error, and your code never executes" (Supabase: Authorization headers).Stripe-Signature header instead (Stripe: Receive events in your webhook endpoint).verify_jwt without the signature check. Supabase: "Be careful when using this flag, as it will allow anyone to invoke your Edge Function without a valid JWT" (Supabase: Function configuration). In our test, a function with no signature check returned 200 to a forged checkout.session.completed event typed by hand (Verified).verify_jwt = false plus the provider's signature check. And never put the service-role or secret key in client code or VITE_ / NEXT_PUBLIC_ variables. It bypasses RLS and belongs only in server-side secrets (check what your build ships).req.text(). In our test, re-serialized JSON failed the check (Verified).whsec_ secret or your Stripe secret key into the function's code or the repo. Use function secrets.My Stripe webhook to a Supabase Edge Function returns 401 "Missing authorization header".
1. In supabase/config.toml, add verify_jwt = false under [functions.<my webhook function name>]
for that function ONLY. Don't change any other function.
2. Make sure the function verifies the Stripe signature before doing anything:
read the raw body with req.text(), get the Stripe-Signature header, and call
await stripe.webhooks.constructEventAsync(body, signature, secret, undefined,
Stripe.createSubtleCryptoProvider()). Return 400 if it throws.
3. Read the whsec_ signing secret and the Stripe key from Deno.env.get(), never
hard-code them. Tell me the supabase secrets set commands to run.
4. Do not use the service_role or secret key in client code, and do not add any
Supabase key to the webhook URL. Show me the diff first and wait for my OK.
A webhook has no user accounts, so here the two "accounts" are the real sender and a stranger:
stripe trigger payment_intent.succeeded with the Stripe CLI (Stripe: Test your handler). The delivery should get a 2xx, and your function logs should show the event. (Docs-only: we signed test events ourselves instead.)curl -i -X POST https://<project-ref>.supabase.co/functions/v1/stripe-webhook -H 'content-type: application/json' -d '{"type":"checkout.session.completed"}'. Expect 400 and a signature error in the logs, and no change in your database. A 200 here means the signature check is missing.verify_jwt is off only for the webhook.No signatures found matching the expected signature (Stripe) · CORS error calling a Supabase Edge Function · Secret keys in frontend env vars · Rotating secrets after a host exposure · Is the publishable key safe? · Our Supabase RLS guide
Did the 401 clear, and did a stranger's unsigned request get a 400? Tell us what your webhook does after the check (orders, credits, subscriptions). We'll add the patterns we see most.
This guide is educational and based on the vendor's public docs as of the review date. Platforms change: check the linked sources. Not a security audit. Disclaimer.
From BuiltSafely · builtsafely.com · https://builtsafely.com/fixes/stripe-webhook-401-edge-function/
The BuiltSafely weekly
Platform changes that affect the security of AI-built apps, checked against official docs. Free. Unsubscribe any time.