Table of Contents
What you'll ship
A "Subscribe" button on your app that gates a feature behind an active monthly plan, a webhook that keeps your `users.subscriptionStatus` in sync with Stripe, and a "Manage subscription" button that drops customers into Stripe-hosted Billing Portal where they can cancel, swap cards, or upgrade themselves.
You'll keep zero billing UI in your own app. Stripe runs all of it.
Before you start
You need the one-time Stripe checkout already working — see the previous playbook, "Add Stripe to a Fabricate App in 30 Minutes". The same STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY apply.
You also need a Fabricate Pro project (D1 database + secrets management).
Note
If your app has user accounts already (built-in auth on Fabricate), great — you'll tie subscriptions to user IDs. If not, the prompt in Step 2 adds the auth piece for you.
Step 1 · Create the recurring Price (5 min)
In the Stripe dashboard (test mode), go to Products → Add product. Give it a name ("Pro plan"), set the price ($19/month), and pick "Recurring → Monthly". Save.
Copy the Price ID — it starts with price_… — from the product detail page. You'll paste this into Fabricate in the next step.
Tip
If you want annual billing too, add a second Price on the same product (e.g. $190/year). Same prompt in Step 2 handles both — just ask Fabricate to show monthly/annual toggle.
Ready to Build?
Start building your full-stack application with Fabricate. Free tier available — no credit card required.
Start Building FreeStep 2 · Wire the subscription flow (5 min)
Drop this prompt into Fabricate, replacing the Price ID with yours:
Add a Subscribe button to /pricing that opens a Stripe Checkout session in subscription mode for price ID price_1ABC2def. Tie the subscription to the signed-in user. Add a `subscriptionStatus` column to the users table (active | trialing | past_due | canceled | none) and a `stripeSubscriptionId` column. After successful checkout, redirect to /thank-you-subscriber.
Step 3 · The webhook (10 min)
The webhook is the part new founders skip and regret. Without it, a customer cancelling on Stripe still has access in your app forever — because your app never finds out.
Send Fabricate this prompt:
Add a Stripe webhook endpoint at /api/stripe/webhook. Verify the signature using STRIPE_WEBHOOK_SECRET. Handle these events: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed. On each, look up the user by stripeCustomerId and update users.subscriptionStatus + stripeSubscriptionId in D1. Return 200 on success, 400 on invalid signature.
Register the webhook with Stripe
In the Stripe dashboard → Developers → Webhooks → Add endpoint. URL: `https://your-app.fabricate.build/api/stripe/webhook`. Select the four events listed above. Save.
Stripe will show a Signing secret (starts with whsec_…). Copy it into the Fabricate Secrets panel as `STRIPE_WEBHOOK_SECRET`. Save.
Step 4 · Customer Portal (5 min)
Don't build cancel / change-card / change-plan UI yourself. Stripe ships all of it as the Billing Portal.
In the Stripe dashboard → Settings → Billing → Customer portal, switch it on and pick which actions are allowed (Cancel, Update payment method, Switch plan). Save.
Add a "Manage subscription" button to the user account page that calls a server endpoint to create a Stripe Billing Portal session for the signed-in user, then redirects to portal.url. Only show the button if subscriptionStatus is active or trialing.
Ready to Build?
Start building your full-stack application with Fabricate. Free tier available — no credit card required.
Start Building FreeStep 5 · Test the lifecycle (10 min)
Run all four scenarios in test mode:
1. Subscribe — card 4242 4242 4242 4242 → check D1 user has subscriptionStatus="active".
2. Cancel via portal — confirm Stripe fires customer.subscription.deleted → D1 status="canceled".
3. Failed renewal — in Stripe dashboard, find the test subscription → "Update next invoice" → use card 4000 0000 0000 0341 (declines on renewal). Stripe fires invoice.payment_failed → D1 status="past_due".
4. Upgrade — change to a more expensive Price via portal → customer.subscription.updated → your D1 reflects the new plan.
Important
If any of these don't update the D1 row, your webhook isn't firing. Check Stripe dashboard → Developers → Webhooks → your endpoint → recent attempts. 99% of webhook failures are signature-mismatch (wrong STRIPE_WEBHOOK_SECRET).
Gotchas worth knowing
A short list of the things that bite founders the week after launch:
• Grace period. When a renewal fails, Stripe retries for ~3 weeks before cancelling. During that time the user's status is `past_due` — keep their access by default. Cutting it off immediately tanks involuntary-churn recovery.
• Trials. If you offer a 7-day trial via Stripe, the subscription starts as `trialing`. Treat trialing the same as active in your app — the webhook will fire `customer.subscription.updated` when the trial converts or expires.
• Tax. Enable Stripe Tax in the dashboard to handle EU VAT and US sales tax automatically. One toggle, no code on your side.
• Refunds. Refunds happen in the Stripe dashboard, not your app. Your webhook gets `charge.refunded` if you want to log it — usually unnecessary for SaaS.
• Idempotency. Stripe occasionally retries webhook deliveries. Your handler should be idempotent — Fabricate's generated code uses subscription_id + event_id as a dedup key in D1.
What's next
You're billing customers monthly. Two natural follow-ons:
• Gate features by plan — prompt Fabricate: "In the dashboard, only show <FeatureX /> when users.plan === 'pro'." Reads from the same D1 column the webhook updates.
• Send a dunning email when payment fails — prompt: "On invoice.payment_failed, send the user an email via Resend with a Customer Portal link to update their card." One credit, recovers ~30% of failed renewals.
Ready to Build?
Start building your full-stack application with Fabricate. Free tier available — no credit card required.
Start Building Free