82  82. Step 8: Set Up Stripe

As of Oct 22, 2026 · Official docs: Stripe · Stripe sandboxes · Stripe API keys · Stripe subscriptions · Stripe customer portal

Stripe will handle subscriptions and billing: the app’s monthly “Premium” plan. While you develop, you work in a Stripe sandbox (Stripe’s test environment; older pages call it “test mode”). Stripe is free to use in a sandbox.

As you go, copy each value into the Task App Setup Worksheet (see Classes 10–12: Start here). Its Commands sheet builds the exact lines you paste in this chapter, with your own keys in them.

82.1 Create Stripe Account

  1. Go to https://dashboard.stripe.com.
  2. Sign up for a new account or log in.

82.2 The Stripe Sandbox

A new Stripe account starts in a sandbox. In a sandbox, real cards are never charged and no real money moves. Instead, you pay with test card numbers (see Step 12) to make sure that your code works. Do every Stripe step in this chapter in the same sandbox: keys, products and webhooks from one sandbox don’t work in another.

82.3 Get API Keys

The keys are on the dashboard’s Home page, and on the API keys page. A sandbox has two. Copy both into the worksheet:

  • The publishable key (starts with pk_test_) is meant for code that runs in a browser. This app never calls Stripe from the browser, so it doesn’t use this key; you won’t paste it anywhere.

  • The secret key (starts with sk_test_) can do anything with your Stripe account. It must stay on servers. Store it as a Supabase secret, so the Edge Functions can use it: copy the worksheet’s STRIPE_SECRET_KEY line from the Commands sheet, paste it into VS Code’s terminal (in the task-app folder), and press Enter. The line looks like this:

    npx supabase secrets set STRIPE_SECRET_KEY=sk_test_...

    Type these lines yourself rather than giving the keys to Claude, so the secret keys never go into Claude’s chat.

If you have separate development and production projects, repeat this setup for each one (we do not in this tutorial).

82.3.1 Create a Product

  1. In the Stripe Dashboard, go to Product catalog (it may be under More).

  2. Click + Add product.

  3. Fill in:

    • Name: TaskMaster Premium
    • Optional description and image.
  4. Under the price, choose:

    • Recurring
    • Billing period: Monthly
    • Amount: e.g., $10
  5. Click Add product to save the product and its price.

  6. Open the product, click the price, and copy its Price ID (it starts with price_) into the worksheet.

  7. Store the Price ID as a Supabase secret named STRIPE_PRICE_ID. The create-stripe-session Edge Function reads it from there when it creates a checkout page. Copy the worksheet’s STRIPE_PRICE_ID line, paste it into VS Code’s terminal (in the task-app folder), and press Enter:

    npx supabase secrets set STRIPE_PRICE_ID=price_...
Not in .env.local

Earlier versions of this page put the Price ID (and the publishable key) in .env.local as NEXT_PUBLIC_STRIPE_PRICE_ID. The app’s code never reads those variables, so checkout fails. The Price ID must be the Supabase secret STRIPE_PRICE_ID.

82.3.2 Get Webhook Signing Secret

You’ll use a webhook to let Stripe notify your app about subscription changes. A webhook is a web address that Stripe calls to tell your app that something happened, such as “this customer paid”. Stripe calls the setup an event destination.

The address is your stripe-webhook Edge Function. The worksheet builds it for you from your Supabase project ref (the row “Stripe webhook URL”):

https://YOUR-PROJECT-REF.supabase.co/functions/v1/stripe-webhook

You deploy that function in Step 11. You can create the webhook now, with the real address, or wait until the function is deployed, as Classes 10–12 do ((H) Deploy the Edge Functions and the Stripe webhook). If you create it now, Stripe can’t deliver events to it until the function is deployed; that’s fine, because you don’t try a payment until Step 12.

  1. Open the Webhooks tab in Stripe’s Workbench (in the same sandbox) and click Create an event destination.

  2. Choose:

    • Events from: Your account.
    • Payload style: Snapshot. Stripe recommends “Thin”, but this app’s code only understands Snapshot events.
  3. Select these events:

    • checkout.session.completed
    • customer.subscription.deleted

    (The code handles only these two. The tutorial repository’s README also lists customer.subscription.updated; adding it does no harm.)

  4. Click Continue and choose Webhook endpoint as the destination type.

  5. Click Continue and enter the Endpoint URL: your Stripe webhook URL from the worksheet.

  6. Create the destination. On its settings page, find the signing secret (it starts with whsec_), click Reveal secret, and copy it into the worksheet.

  7. Store it as a Supabase secret: copy the worksheet’s STRIPE_WEBHOOK_SECRET line, paste it into VS Code’s terminal (in the task-app folder), and press Enter. The line looks like this:

    npx supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

For each event, Stripe’s Workbench shows whether it was delivered. The app’s stripe-webhook function answers “OK” even when it rejects an event (for example, because the signing secret in STRIPE_WEBHOOK_SECRET doesn’t match this destination’s secret). So Stripe may show an event as delivered even though the app ignored it. If an upgrade doesn’t show up in the app, look at the stripe-webhook logs in the Supabase dashboard under Edge Functions: the function writes the reason there.

82.3.3 Store the Secret Key in Supabase Vault

When someone signs up for the app, the database itself creates a Stripe customer for them. (Migration 7_init_stripe_integration.sql, which you run in Step 9, sets this up.) To talk to Stripe, the database needs your Stripe secret key. It reads the key from Supabase Vault, an encrypted store for secrets inside your database, under the name stripe.

  1. In the Supabase dashboard, open SQL Editor.

  2. Paste the Vault line from the worksheet’s Commands sheet, which has your own secret key in place of sk_test_..., and click Run:

    select vault.create_secret('sk_test_...', 'stripe');

    This is SQL, the language of databases, not a terminal command. It calls the function create_secret in Supabase’s vault, with two values: your secret key and the name to store it under. (The tutorial repository’s README does the same with an insert into vault.secrets ... command.)

Do this before anyone signs up for the app. Without the key in Vault, creating a new user’s profile fails, so sign-up fails.

82.3.4 Set Up the Customer Portal

The app’s subscription button sends premium users to Stripe’s customer portal, a Stripe page where they can update their payment method or cancel. In a sandbox, the portal doesn’t work until you save its settings once.

  1. Open the customer portal settings in the Stripe Dashboard (in the same sandbox).
  2. Look over the options (letting customers cancel is on by default). For class demos, set cancellations to happen immediately.
  3. Click Save.

By default, a cancelled subscription stays active until the end of the billing period. Only then does Stripe send customer.subscription.deleted, and the app moves the user back to the free plan. With cancellations set to happen immediately, a cancelled subscription switches the app back to the free plan right away, so you can see the whole cycle in one sitting.

Practice Questions

Easy

Question 1 · True or false

While you work in the Stripe sandbox, real credit cards are charged.

False. In the sandbox, real cards are never charged. You test your code with fake test credit card numbers instead.

Question 2 · Short answer

In your own words, what is a webhook, and what does task-app use one for?

A webhook is a web address (URL) that you give to Stripe so that Stripe can notify your app automatically when something happens: Stripe sends a message to that address. task-app uses it so that Stripe can tell the app about subscription changes, such as a completed checkout or a cancelled subscription.

Question 3 · Multiple choice

Where does the page store the Price ID of your subscription price?

  1. In .env.local, as NEXT_PUBLIC_STRIPE_PRICE_ID
  2. As a Supabase secret named STRIPE_PRICE_ID
  3. In the Google Cloud Console
  4. Nowhere: Stripe picks the price automatically

b. The create-stripe-session Edge Function reads the Price ID from the Supabase secret STRIPE_PRICE_ID when it creates a checkout page. The app’s code never reads a NEXT_PUBLIC_STRIPE_PRICE_ID variable (a), so a Price ID stored there is ignored and checkout fails. Google (c) has nothing to do with payments, and Stripe can’t guess which of your prices to charge (d).

Question 4 · Matching

Match each Stripe value with its description.

Value Description
1. Publishable key A. Looks like price_12345...; identifies the price a customer subscribes to (for example, $10 per month)
2. Secret key B. Looks like whsec_...; lets your code check that a webhook message really came from Stripe
3. Price ID C. Safe to use in code that runs in the browser
4. Webhook signing secret D. Can do anything with your Stripe account through Stripe’s API; must stay on a server

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

Medium

Question 5 · Write the command

Stripe shows you the signing secret whsec_9f8e7d. Write the command that stores it as a Supabase secret named STRIPE_WEBHOOK_SECRET.

npx supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_9f8e7d

Remember that in this tutorial every Supabase CLI command starts with npx supabase.

Question 6 · Multiple choice

You want a “Pro Plan” that costs $10 every month. Which price settings match?

  1. One-off, $10
  2. Recurring, billing period Monthly, $10
  3. Recurring, billing period Yearly, $10
  4. One-off, $120

b. A subscription needs a recurring price, and monthly billing at $10 matches the plan. One-off prices (a, d) charge only once, and a yearly billing period (c) charges once a year.

Question 7 · Matching

Match each webhook event with when Stripe sends it.

Event When Stripe sends it
1. checkout.session.completed A. A customer’s subscription ends (for example, it is cancelled)
2. customer.subscription.updated B. A customer finishes paying on Stripe’s checkout page
3. customer.subscription.deleted C. Something about a subscription changes (for example, its plan or status)

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

Task-app’s webhook code acts only on 1 and 3: it upgrades the user to premium after a checkout, and moves them back to free when the subscription ends.

Question 8 · Write the path

Sam’s Supabase project ref is abcdefghijklmnop. What endpoint URL does he enter for the webhook’s event destination in Stripe?

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

It is the address of the stripe-webhook Edge Function on his Supabase project. The worksheet builds it for him from his project ref.

Question 9 · Multiple choice

When you create the event destination, Stripe offers two payload styles and recommends “Thin”. Which one do you choose for this app, and why?

  1. Thin, because Stripe recommends it
  2. Snapshot, because the app’s webhook code only understands Snapshot events
  3. Either one; the app handles both
  4. Neither; the payload style only matters in live mode

b. The stripe-webhook function reads the event’s full details (Snapshot style). Stripe’s recommendation (a) is for new code written for Thin events, which this app’s code is not. The app does not handle both (c), and the choice matters in the sandbox too (d).

Question 10 · Short answer

When a user clicks to upgrade to the Pro Plan, how does Stripe know what to charge them? What role does the Price ID play?

When the app starts a Stripe checkout, it tells Stripe which price the customer is buying by sending the Price ID (for example, price_12345...). That ID points to the price you created (recurring, monthly, $10), so Stripe knows how much to charge and how often.

Hard

Question 11 · Scenario

While testing, Maria completes Stripe’s checkout page successfully, but task-app still shows her on the free plan. In Stripe’s Workbench, the checkout.session.completed event shows as delivered. Explain what could be going on and where she should look.

task-app finds out about a completed payment through the webhook. “Delivered” only means that the stripe-webhook function answered; it answers “OK” even when it rejects an event. A likely cause is a signing secret mismatch: the secret stored in STRIPE_WEBHOOK_SECRET isn’t this destination’s whsec_... secret (for example, she created a second destination, or copied the secret wrong), so the function can’t verify the message and ignores it. Other things to check: the endpoint URL points to her own project’s stripe-webhook function, and the payload style is Snapshot. She should look at the stripe-webhook logs in the Supabase dashboard under Edge Functions, which say why the event was rejected. To fix a wrong secret, she sets STRIPE_WEBHOOK_SECRET again with the destination’s signing secret.

Question 12 · Short answer

Why does Stripe give each webhook a signing secret? What could happen if the app accepted webhook messages without checking it?

A webhook address is a public URL, so anyone could send messages to it. Stripe signs every message using the signing secret, and your function uses the same secret to check that the message really came from Stripe and wasn’t changed. Without that check, an attacker could send a fake “checkout completed” message and get the paid plan for free.

Question 13 · Long answer

The page mentions repeating this setup for each environment if you have separate development and production setups. Suppose task-app goes live and starts charging real customers. What has to be set up again, and why?

Real payments happen in Stripe’s live mode, which is completely separate from the sandbox. You need:

  • the live secret key,
  • the product and its price created again for live use (with a new Price ID),
  • a live webhook endpoint that points to your function, with its own signing secret.

Then you update the Supabase secrets (STRIPE_SECRET_KEY, STRIPE_PRICE_ID and STRIPE_WEBHOOK_SECRET) with these live values. Sandbox keys and IDs can’t be used for real payments.

Question 14 · Scenario

You finished Steps 8 to 12, but every new sign-up fails with a database error. You notice that you skipped the section “Store the Secret Key in Supabase Vault”. Why does a missing Stripe key break sign-up, of all things, and how do you fix it?

When a user signs up, the database creates their profile, and a trigger then creates a Stripe customer for them. To reach Stripe, the database reads the secret key from Supabase Vault under the name stripe. With no key there, the trigger fails, so the profile isn’t created and the sign-up fails. Fix: in the Supabase SQL Editor, run select vault.create_secret('sk_test_...', 'stripe'); with your secret key, then sign up again.

Question 15 · Short answer

A premium user clicks the app’s subscription button and gets an error instead of Stripe’s customer portal. Checkout worked fine earlier. What did you most likely skip?

Saving the customer portal settings in the Stripe Dashboard. Checkout and the portal are separate Stripe features: checkout works without any portal settings, but in a sandbox Stripe can’t open the portal until you have saved its settings once.

Question 16 · Scenario

During a class demo, Sam cancels his Premium subscription in Stripe’s customer portal, but task-app still shows him on the Premium plan. Nothing is broken. What explains it, and what portal setting changes this?

By default, a cancelled subscription stays active until the end of the billing period, and only then does Stripe send customer.subscription.deleted, the event that moves the user back to the free plan. So the app correctly keeps Sam on Premium for now. In the customer portal settings, setting cancellations to happen immediately makes Stripe end the subscription right away, so the app switches him back to the free plan at once.

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