85  85. Step 11: Deploy Supabase Edge Functions

As of Oct 22, 2026 · Official docs: Supabase Edge Functions · Supabase: Edge Function secrets · Stripe webhooks

Edge Functions are small back-end programs that run on Supabase’s servers. This app has three: one for AI labels, one for Stripe payments, and one for Stripe’s webhook.

Location in project: supabase/functions/, one folder per function. The folder’s name is the function’s name, and it is the name you use when you deploy it. The folder _shared is not a function: it holds code that the functions share, and the CLI includes it when it deploys a function that uses it.

85.1 The Three Functions

85.1.1 1. create-task-with-ai

Used to:

  • Check which signed-in user is calling (from the user’s JWT, a signed “ID badge” that the app sends along).
  • Save the new task in the tasks table.
  • Ask OpenAI for a label (using the secret OPENAI_API_KEY) and save the label with the task.

85.1.2 2. create-stripe-session

Used when the user clicks the subscription button in the app:

  • For a free user, it creates a Stripe Checkout session (Stripe’s payment page) for the price in the secret STRIPE_PRICE_ID.
  • For a premium user, it returns a link to Stripe’s customer portal, where the user can manage or cancel the subscription.

It uses the secrets STRIPE_SECRET_KEY and STRIPE_PRICE_ID.

85.1.3 3. stripe-webhook

Used to:

  • Receive event notifications from Stripe.
  • Verify each event’s signature using STRIPE_WEBHOOK_SECRET.
  • Update the user’s row in the profiles table: checkout.session.completed sets subscription_plan to premium, and customer.subscription.deleted sets it back to free. The code ignores every other event.

This function is called directly by Stripe, so it doesn’t use user JWTs.

85.2 The Keys and Secrets the Functions Read

The functions read their keys from environment variables on Supabase’s servers, never from files on your computer. There are two kinds:

Name What it holds Who puts it there
OPENAI_API_KEY Your OpenAI key You, with npx supabase secrets set ((E) OpenAI API key in Classes 10–12)
STRIPE_SECRET_KEY, STRIPE_PRICE_ID Your Stripe secret key and price ID You, the same way (Step F)
STRIPE_WEBHOOK_SECRET The webhook’s signing secret You, at the end of this page
SUPABASE_URL Your project’s address Supabase, automatically
SUPABASE_PUBLISHABLE_KEYS, SUPABASE_SECRET_KEYS Your project’s publishable (sb_publishable_...) and secret (sb_secret_...) keys Supabase, automatically

The shared file supabase/functions/_shared/supabaseKeys.ts reads the last two. create-task-with-ai uses the publishable key together with the user’s login token, so the database’s security rules (RLS) still apply. create-stripe-session and stripe-webhook use the secret key, which skips those rules, so they can read and update any user’s profile. That is fine because the secret key never leaves Supabase’s servers.

A Supabase project can have several keys of each kind, so SUPABASE_PUBLISHABLE_KEYS and SUPABASE_SECRET_KEYS each hold a small JSON object of named keys, such as {"default": "sb_secret_..."}. A new project has one key of each kind, named default, and supabaseKeys.ts picks that one. If a variable has no key, the function stops with the error “No Supabase key found in …”, which points you to Project Settings > API Keys in the dashboard.

You may see older tutorials use the “anon” and “service_role” keys (the dashboard’s Legacy tab). This app’s code doesn’t read them any more: it uses only the new publishable and secret keys.

85.3 Deploy the Functions

Work in the project root, the task-app folder (it contains the supabase/ folder). Claude deploys the three functions by running these commands in the terminal for you, one at a time, after asking your permission. (To run one yourself, type it in VS Code’s terminal and press Enter.)

npx supabase functions deploy create-task-with-ai
npx supabase functions deploy create-stripe-session
npx supabase functions deploy stripe-webhook --no-verify-jwt
  • npx supabase runs the Supabase CLI from the project’s own packages (see Step 5a).
  • functions deploy uploads one function to Supabase’s servers, where it runs. The argument after it is the function’s name: the name of its folder in supabase/functions. The names use hyphens: the app calls the functions at URLs built from these exact names.
  • --no-verify-jwt (only for stripe-webhook) turns off JWT verification for that function, so that Stripe can call it without a Supabase login. The section after next explains why.

Each function’s URL has this form:

https://[your-project-ref].supabase.co/functions/v1/create-task-with-ai

85.4 Verify Deployment

  1. In the Supabase dashboard, open Edge Functions.
  2. Confirm you see:
    • create-task-with-ai
    • create-stripe-session
    • stripe-webhook
  3. Click a function to see its details. Its Logs tab shows runtime errors and the messages the code prints.

85.5 JWT verification for stripe-webhook

stripe-webhook must accept calls from Stripe, which don’t carry a Supabase JWT (a user’s login token). The --no-verify-jwt option above turns JWT verification off for that function only.

To check it, open Edge Functions > stripe-webhook > Details in the dashboard. The switch Verify JWT with legacy secret should be off. If it is on, turn it off and save, or deploy the function again with --no-verify-jwt.

The project’s supabase/config.toml file already contains these lines, so every deploy of stripe-webhook keeps verification off, even if someone forgets the option:

[functions.stripe-webhook]
verify_jwt = false

Why is this safe? Stripe doesn’t send JWTs. Instead, it signs every event, and the function checks that signature with STRIPE_WEBHOOK_SECRET.

If one of the other functions answers “Invalid JWT” (401)

The other two functions keep JWT verification on. If creating a task or upgrading fails, open that function’s logs in the dashboard (Edge Functions) and show Claude what they say. For an “Invalid JWT” (401) error, one known fix, which Claude can make with you, is to turn off Verify JWT with legacy secret for that function too (Edge Functions > the function > Details). This is safe here: both functions check the user’s login themselves before they do anything.

85.6 Create the Stripe Webhook

A webhook is a web address that Stripe calls to tell your app that something happened, such as “this customer paid”. Now that stripe-webhook is deployed, give Stripe its address.

  1. Open dashboard.stripe.com/webhooks (in the same sandbox as the rest of your Stripe setup) and click Create an event destination:
    • Events from: Your account.

    • Payload style: Snapshot. Stripe suggests “Thin”, but this app’s code only understands Snapshot events.

    • Events: checkout.session.completed and customer.subscription.deleted (the two the code handles; the README also lists customer.subscription.updated, which does no harm).

    • Destination type: Webhook endpoint.

    • Endpoint URL: your Stripe webhook URL from the worksheet:

      https://[your-project-ref].supabase.co/functions/v1/stripe-webhook
  2. On the destination’s page, click Reveal next to the signing secret (whsec_...) and copy it into the worksheet.
  3. Copy the worksheet’s STRIPE_WEBHOOK_SECRET line (an npx supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_... command), paste it into VS Code’s terminal (in the task-app folder), and press Enter. You type this one yourself, so the secret never goes into Claude’s chat.

If you already created an event destination with a temporary URL when you set up Stripe (Step 8), you can edit that one instead: change its Endpoint URL to the address above, and check its payload style and events. Its signing secret must be the one stored in STRIPE_WEBHOOK_SECRET.

You test the webhook in Step 12 by buying a subscription with a test card. Afterwards:

  • The destination’s page in Stripe lists the events Stripe sent.
  • The stripe-webhook logs in Supabase show Received event: checkout.session.completed.
  • In the Supabase Table Editor, your row in profiles has subscription_plan set to premium.

Stripe can also send test events (with stripe trigger in the Stripe CLI), but those events create a new test customer who isn’t linked to any of your app’s users, so they don’t change your database.

Stripe may show an event as delivered even when the function rejected it: the function always answers “OK”. If an upgrade doesn’t appear in the app, read the stripe-webhook logs in Supabase.

Practice Questions

Easy

Question 1 · Short answer

What is a Supabase Edge Function, and in which folder of the project is the code for the three functions kept?

An Edge Function is a small piece of back-end code that runs on Supabase’s servers - not in the user’s browser and not on your laptop - whenever something calls its URL. Functions like these are called serverless: you write the function and Supabase takes care of running it, so you never set up or manage a server yourself. The code lives in supabase/functions/, with each function in its own subfolder.

Question 2 · Matching

Match each function with its job. One job on the right does not belong to any of the three functions.

Function Job
1. The AI task-creation function A. Receives event notifications from Stripe and updates the subscription status in the database
2. The Stripe session function B. Asks OpenAI to suggest a label for a task and saves the task
3. stripe-webhook C. Stores the images that users upload
D. Sends the user to a Stripe checkout page, or to Stripe’s customer portal if they already subscribe

1 → B, 2 → D, 3 → A.

C is not an edge function’s job: images are stored in the Supabase Storage bucket from the previous step.

Question 3 · True or false

After you deploy a function, you can keep editing its file in supabase/functions/ on your laptop, and the live function on Supabase updates automatically.

False. Deploying uploads a copy of your code to Supabase. The live function changes only when you deploy again (for example with npx supabase functions deploy stripe-webhook). Until then, Supabase keeps running the old copy.

Question 4 · Multiple choice

A function you deployed is returning errors. Where does this page tell you to look for the error messages?

  1. The function’s Logs tab under Edge Functions in the Supabase dashboard
  2. The supabase/migrations/ folder on your computer
  3. The Google Cloud Console
  4. The .env.local file

a. The function runs on Supabase’s servers, so its runtime errors are recorded in its logs there. The migrations folder (b) holds database changes, the Google Cloud Console (c) holds the Google sign-in settings, and .env.local (d) holds settings for the Next.js app - none of them record what happens when the function runs.

Question 5 · Multiple choice

When you create the Stripe event destination, which payload style do you choose, and why?

  1. Thin, because Stripe suggests it
  2. Snapshot, because this app’s code only understands Snapshot events
  3. Either one: the code handles both
  4. Neither: payload style is only for live mode

b. Stripe suggests “Thin” (a), but stripe-webhook reads the full event that a Snapshot payload contains, for example the customer of a completed checkout. With Thin events, the code wouldn’t find what it needs (c). Payload style applies to sandbox destinations too (d).

Medium

Question 6 · Write the command

Write the command that deploys only the stripe-webhook function. In which folder should you be when you run it?

npx supabase functions deploy stripe-webhook --no-verify-jwt

Run it from the project root, the task-app folder, which contains the supabase/ folder. (In class, Claude runs it for you.) --no-verify-jwt keeps JWT verification off for this function, so Stripe can call it. This tutorial runs the Supabase CLI through npx; if you installed the CLI another way, leave out npx.

Question 7 · Write the URL

Your Supabase project ref is abcdefghijklmnopqrst. What is the URL of your deployed stripe-webhook function? (This is the URL you give to Stripe.)

https://abcdefghijklmnopqrst.supabase.co/functions/v1/stripe-webhook

The pattern is https://[your-project-ref].supabase.co/functions/v1/ followed by the function’s name.

Question 8 · Matching

Match each secret or credential with what it is used for.

Secret or credential Used for
1. OPENAI_API_KEY A. Proving which signed-in user is calling the AI task-creation function
2. STRIPE_SECRET_KEY B. Checking that an incoming webhook event really came from Stripe
3. STRIPE_WEBHOOK_SECRET C. Letting the AI task-creation function ask OpenAI for a label
4. The user’s JWT D. Letting the Stripe session function create checkout sessions

1 → C, 2 → D, 3 → B, 4 → A.

A JWT (JSON Web Token) is a signed “ID badge” that the app sends along with a request to show which signed-in user is making it.

Question 9 · Multiple choice

A user who already has a subscription clicks “Manage Subscription”. According to the page, what does the Stripe session function do?

  1. Creates a second subscription for the same user
  2. Returns a link to Stripe’s customer portal, where the user can manage the existing subscription
  3. Deletes the user’s account
  4. Calls OpenAI to recommend a subscription plan

b. For a new subscriber, the function creates a Stripe Checkout session (the payment page). For someone who already subscribes, it returns a customer portal link instead, so they can manage what they already have. It doesn’t create duplicates (a) or delete accounts (c), and calling OpenAI (d) is the AI task-creation function’s job.

Question 10 · Matching

Match each environment variable that the deployed functions read with who puts it there.

Variable Who puts it there
1. OPENAI_API_KEY A. Supabase, automatically, for every function
2. SUPABASE_SECRET_KEYS B. You, with npx supabase secrets set, after creating the webhook in Stripe
3. STRIPE_WEBHOOK_SECRET C. You, with npx supabase secrets set, after creating your OpenAI key

1 → C, 2 → A, 3 → B.

Supabase gives every function SUPABASE_URL, SUPABASE_PUBLISHABLE_KEYS and SUPABASE_SECRET_KEYS on its own. The keys of other services (OpenAI, Stripe) you store yourself, as secrets.

Question 11 · True or false

If you deploy stripe-webhook and forget the --no-verify-jwt option, JWT verification is turned on for it, and Stripe’s calls are refused.

False, for this project. Its supabase/config.toml file already contains verify_jwt = false under [functions.stripe-webhook], and the CLI reads that setting on every deploy. The option is a second safeguard. You can always check the switch Verify JWT with legacy secret under Edge Functions > stripe-webhook > Details: it should be off.

Hard

Question 12 · Long answer

Explain why JWT verification must be turned off for stripe-webhook, and why doing so does not let just anyone fake a payment.

With JWT verification on, Supabase lets a call through to a function only if it carries a valid JWT. That makes sense for the other two functions, which the app calls on behalf of a signed-in user. But stripe-webhook is called directly by Stripe’s servers, and Stripe doesn’t send a JWT - so Supabase would turn away every Stripe event before your code even ran.

It’s still safe because the function does its own check: Stripe signs every event, and the function verifies that signature using STRIPE_WEBHOOK_SECRET, which only you and Stripe know. A fake “payment succeeded” message sent by anyone else won’t carry a valid signature, so the function rejects it.

Question 13 · Scenario

You deployed all three functions and pointed Stripe at the stripe-webhook URL. Now Stripe reports that every delivery to your endpoint fails with HTTP status 401 (Unauthorized), and the function’s logs show none of the messages your code would print. What did you most likely forget, and how do you fix it?

You forgot to turn off JWT verification for stripe-webhook, for example by deploying it without --no-verify-jwt. Stripe’s requests carry no JWT, so Supabase answers “401 Unauthorized” before your function’s code runs. Fix: deploy it again with npx supabase functions deploy stripe-webhook --no-verify-jwt, or switch off Verify JWT with legacy secret under Edge Functions > stripe-webhook > Details in the Supabase dashboard. Also check that supabase/config.toml still has verify_jwt = false under [functions.stripe-webhook].

Question 14 · Scenario

When you set up Stripe, you created an event destination with a temporary URL (https://mybogusurl.com/). You deploy the functions but never change that URL. Then you buy a subscription with a test card. What happens, and how do you fix it?

The payment itself succeeds on Stripe’s checkout page, but Stripe sends its notification to the bogus URL, so your stripe-webhook function never hears about it. The database is never updated, and the app doesn’t treat the user as a subscriber. Fix: in the webhook settings in the Stripe Dashboard, change the destination’s endpoint URL to https://[your-project-ref].supabase.co/functions/v1/stripe-webhook and save (or create a new destination with that URL, as in “Create the Stripe Webhook”, and store its signing secret with npx supabase secrets set STRIPE_WEBHOOK_SECRET=...).

Question 15 · Ordering

Put these steps in the order this page does them:

  • A. Create the webhook in Stripe, with the deployed stripe-webhook URL as its endpoint
  • B. Deploy the three functions with the Supabase CLI
  • C. Check that JWT verification is off for stripe-webhook
  • D. Check in the Supabase dashboard that all three functions appear, and look at their logs

B → D → C → A.

Deploying (B) has to come first: until a function is deployed, there is nothing to check (D and C) and no URL to give Stripe (A). After A comes one more step: storing the webhook’s signing secret as STRIPE_WEBHOOK_SECRET.

Question 16 · Scenario

Sam creates a task, but no AI label appears. The create-task-with-ai logs in Supabase show an “Invalid JWT” (401) error, and none of the messages the function’s code prints. What is one known fix, and why is it safe for this function?

Supabase refused the call before the function’s code ran, because its JWT check didn’t accept the login token. One known fix: turn off Verify JWT with legacy secret for that function (Edge Functions > create-task-with-ai > Details). It is safe because the function checks the user’s login itself: it asks Supabase who the user is, and stops with “No user found” if nobody is signed in.

Question 17 · Long answer

create-task-with-ai uses the Supabase publishable key, while stripe-webhook uses the secret key. Explain why each function uses the key it does.

create-task-with-ai acts on behalf of the signed-in user: it uses the publishable key together with the user’s login token, so the database’s RLS rules apply, and the user can only create tasks of their own. stripe-webhook is called by Stripe, with no user signed in, and it must change the plan in whichever user’s profile the payment belongs to. RLS would block that, so it uses the secret key, which skips RLS. That is acceptable because the function runs only on Supabase’s servers, where the key stays, and it first checks Stripe’s signature.

© 2025-2026 Y. Rosenthal. All rights reserved.