Launch Playbook7 min read

Add Stripe Subscriptions to a Fabricate App

Launch Playbook · Bill customers every month.

A one-time payment is a sale. A subscription is a business. This playbook gates a feature behind a paid monthly plan, hands customers a self-serve portal to cancel or swap cards, and wires the webhook that keeps your database honest. About 45 minutes end-to-end.

By Fabricate TeamUpdated April 29, 2026

Key Takeaways

  • Subscriptions are a Product + recurring Price in Stripe, plus one webhook in Fabricate.
  • Stripe's hosted Customer Portal handles cancel, upgrade, downgrade, and update-card. Don't build it yourself.
  • Always store subscription status in your D1 database — never trust client claims.
  • Test invoice.payment_failed before going live. It happens to ~5% of paying customers.
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 (Clerk 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 Free

Step 2 · Wire the subscription flow (5 min)

Drop this prompt into Fabricate, replacing the Price ID with yours:

Prompt to send to Fabricate
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 Clerk 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.

Fabricate scaffolds the subscription checkout endpoint, migrates the users table, and adds the success page. Usually 4–8 credits.

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:

Prompt to wire the webhook
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.

Fabricate generates the verified-signature handler, the four event handlers, and the D1 update queries.

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.

Prompt to add the portal button
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.

2–3 credits. Customers click → land on Stripe-hosted portal → cancel/upgrade themselves → Stripe fires customer.subscription.updated → your webhook keeps your DB in sync.

Ready to Build?

Start building your full-stack application with Fabricate. Free tier available — no credit card required.

Start Building Free

Step 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

Frequently Asked Questions

Why do I need a webhook? Can I just check Stripe on every request?

You can, but it's slow (each request waits on a Stripe API call) and brittle (Stripe outage = your app outage). The webhook updates your D1 once per state change, and your app reads from D1 — fast and resilient.

What if the webhook is down when Stripe fires it?

Stripe retries failed webhooks with exponential backoff for up to 3 days. As long as your endpoint comes back online and returns 200, Stripe will catch you up. You can also replay any event manually from the Stripe dashboard.

Should I use Stripe's built-in trial or my own?

Use Stripe's. It handles `trialing` → `active` automatically and your webhook gets the right events. Rolling your own trial means writing your own state machine — usually a mistake for a first version.

Can I run subscriptions and one-time products on the same app?

Yes. The same Stripe customer can have one-time charges and active subscriptions. Just make sure your checkout endpoint passes the right `mode` ('subscription' or 'payment') based on which Price/Product was selected.

How do I move from test mode to live?

Same as the one-time playbook: replace pk_test_/sk_test_ with pk_live_/sk_live_ in Fabricate Secrets. Then re-create your Product, Price, webhook endpoint, and Customer Portal config in live mode (test and live are isolated). Plan ~15 minutes for the live config.