NonDev Apps

Day 05 of 07

Add Stripe billing and your first paid plan

Goal: Today you add a $15/month subscription using Stripe Checkout. When a free user hits their limit, they see an upgrade button. After payment, the webhook grants them unlimited access. By the end of today your product can earn real money.

What to do

01

Create your product and price in Stripe

Set up the $15/month subscription product in the Stripe dashboard.

  • Log in to stripe.com.
  • Make sure you are in Test mode — look for an orange "Test mode" badge in the top right. If you see "Live mode", toggle to Test mode.
  • Click Products in the left sidebar.
  • Click Add Product.
  • Product name: Copy Grader Pro
  • Description: Unlimited landing page grades, full history, and shareable result links.
  • Click Add a price.
  • Price: $15.00 USD.
  • Billing period: Monthly.
  • Click Save.
  • You will see your product with a Price ID that looks like: price_1AbcDef12345. Copy this Price ID — you will need it as an environment variable.
02

Add Stripe keys to your environment files

Add the Stripe publishable key, secret key, price ID, and webhook secret to your project.

  • In Stripe, click Developers → API Keys in the left sidebar.
  • Copy the Publishable Key — starts with pk_test_. This is safe to use in the browser.
  • Click "Reveal" next to the Secret Key — starts with sk_test_. This must only be used on the server.
  • Never share your Secret Key with anyone or put it in client-side code.
  • Add these four lines to your .env.local file:
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key
  • STRIPE_SECRET_KEY=sk_test_your_key
  • STRIPE_PRICE_ID=price_your_price_id
  • STRIPE_WEBHOOK_SECRET=whsec_placeholder (you will replace this in step 5)
  • Install Stripe: stop npm run dev, run npm install stripe, restart npm run dev.
03

Create the Stripe checkout API route

Build the server-side endpoint that creates a Stripe Checkout session and returns the URL.

  • Open Claude Code and paste: "Create a Next.js API route at app/api/checkout/route.ts that: (1) Gets the authenticated Supabase user using the server client. (2) If no user, returns 401. (3) Creates a Stripe client using process.env.STRIPE_SECRET_KEY. (4) Creates a Stripe Checkout Session with: mode: 'subscription', line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }], success_url: '[YOUR VERCEL URL]/?upgraded=true', cancel_url: '[YOUR VERCEL URL]/?cancelled=true', metadata: { userId: user.id }. (5) Returns JSON { url: session.url }."
  • Replace [YOUR VERCEL URL] with your actual Vercel URL in both the success and cancel URLs.
  • The metadata: { userId: user.id } line is critical — this is how the webhook on Day 5 step 4 knows which user paid.
  • The success_url sends the user back to your app after payment with ?upgraded=true in the URL — you will use this to show a success message.
  • Test the route exists by visiting localhost:3000/api/checkout in your browser — you should see a 401 error if not signed in (that is correct behaviour).
04

Create the Stripe webhook to grant pro access after payment

Build the webhook endpoint that listens for completed payments and updates the user's account.

  • Open Claude Code and paste: "Create a Next.js API route at app/api/webhook/route.ts that: (1) Reads the raw request body as text (not JSON). (2) Gets the Stripe signature from the request headers: request.headers.get('stripe-signature'). (3) Constructs a Stripe event using stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET). (4) If the event type is 'checkout.session.completed': gets the userId from event.data.object.metadata.userId, then uses the Supabase service_role client to update the usage table row where user_id = userId, setting is_pro = true. (5) Returns a 200 response with JSON { received: true }. (6) Add export const dynamic = 'force-dynamic' at the top of the file. Do NOT use bodyParser or JSON.parse on the request body — Stripe signature verification requires the raw body."
  • The raw body requirement is important — if you parse the JSON before verifying the signature, the signature check will always fail.
  • The export const dynamic = 'force-dynamic' prevents Next.js from caching this route, which could cause webhooks to be missed.
  • You will add the real STRIPE_WEBHOOK_SECRET in the next step once you have set up the webhook endpoint in Stripe.
05

Register the webhook in Stripe and get the signing secret

Connect Stripe to your webhook endpoint so it knows where to send payment events.

  • For local testing, install the Stripe CLI: go to stripe.com/docs/stripe-cli and follow the instructions for your OS.
  • Once installed, run in a new terminal tab: stripe login (follow the browser prompt).
  • Then run: stripe listen --forward-to localhost:3000/api/webhook
  • The CLI will output a webhook signing secret starting with whsec_. Copy it.
  • Replace the placeholder in .env.local: STRIPE_WEBHOOK_SECRET=whsec_the_real_secret_from_cli
  • Restart npm run dev.
  • For the production webhook: go to Stripe → Developers → Webhooks → Add Endpoint.
  • Endpoint URL: https://your-project.vercel.app/api/webhook
  • Select event: checkout.session.completed
  • Click Add Endpoint.
  • Stripe shows a Signing Secret — copy it. This is a DIFFERENT secret from the CLI one.
  • Add it to Vercel environment variables as STRIPE_WEBHOOK_SECRET (the production value).
06

Wire up the upgrade button and test the full checkout flow

Connect the upgrade prompt to Stripe and test the full payment-to-access flow.

  • Open Claude Code and paste: "Update app/page.tsx to: (1) Change the Upgrade button to call /api/checkout via a POST request, then redirect to the returned URL: const res = await fetch('/api/checkout', { method: 'POST' }); const { url } = await res.json(); window.location.href = url. (2) When the page loads with ?upgraded=true in the URL, show a green success banner: 'Welcome to Copy Grader Pro — unlimited grades are now active.' and remove the ?upgraded=true from the URL using window.history.replaceState. (3) Update the grade counter display: if is_pro is true, show 'Pro — unlimited grades' badge in green instead of the usage counter."
  • Now test the full flow locally:
  • — Sign in, grade 3 pages, see the upgrade prompt.
  • — Click Upgrade — you should be redirected to a Stripe Checkout page.
  • — On the Stripe Checkout page, enter the test card: 4242 4242 4242 4242, any future date, any CVC.
  • — Click Pay $15.00.
  • — You should be redirected back to your app with ?upgraded=true.
  • — The green success banner should appear.
  • — Grade a 4th page — it should work without the upgrade prompt.
  • — Check Supabase → Table Editor → usage → the is_pro column for your user should now be true.
  • If the Stripe CLI is running (stripe listen), watch its output to confirm the checkout.session.completed event was received and forwarded.
07

Add Stripe keys to Vercel and deploy

Make the full billing flow work on your live URL.

  • Go to Vercel → Settings → Environment Variables.
  • Add: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (pk_test_ for now), STRIPE_SECRET_KEY (sk_test_), STRIPE_PRICE_ID, and STRIPE_WEBHOOK_SECRET (the production one from the Stripe dashboard — NOT the CLI one).
  • Commit and push: "Day 5: Stripe billing complete".
  • After deployment, test the full checkout flow on the live Vercel URL using the test card.
  • Confirm: upgrade button → Stripe Checkout → payment → redirect → success banner → is_pro = true in Supabase → unlimited grading.
  • Do not switch to live Stripe keys yet — wait until Day 7 when you are ready to launch.

Expected result

Your live product has a working $15/month subscription. Test users can hit the free limit, click Upgrade, pay with the test card 4242 4242 4242 4242, and immediately access unlimited grading. is_pro is stored in Supabase.

Key takeaway

  • You now have a business, not just a tool. The Stripe webhook is the most important line in the entire codebase — it is the moment a payment becomes access. Always test the full loop yourself (including the fake payment) before telling anyone else about the product.
Day 5 — Add Stripe billing and your first paid plan | 7-Day SaaS Build Challenge | NonDev Apps