How to Integrate Stripe Subscriptions in Bubble.io — The Right Way
Billing is workspace-level, not user-level. One Stripe Customer per workspace. Webhooks, not redirect URLs. This guide covers every step of a production-grade Stripe integration in Bubble — from checkout to cancellation.
The Four Principles of Bubble + Stripe Done Correctly
One Customer Per Workspace
Create one Stripe Customer for the workspace — not for each user. The workspace owner is the billing contact. Store stripe_customer_id on the Workspace record.
Webhooks Drive State
Never trust the checkout success redirect URL to activate a subscription. Trust only webhooks. A webhook from Stripe is the authoritative source of truth for billing status.
Validate Every Signature
Every webhook endpoint must verify the Stripe-Signature header before processing. Unvalidated endpoints can be forged by anyone to fake subscription activations.
Plans in the Database
Store every plan detail in a Plan data type — price, limits, features. Never hardcode plan logic in conditions. When you reprice, update the database record, not 40 workflow conditions.
Metadata Links Everything
Pass workspace_id in Stripe metadata on every object: Customer, Subscription, Checkout Session. This is how your webhook finds the correct Bubble workspace to update.
Preserve on Cancellation
Never delete workspace data when a subscription is cancelled. Set status to Cancelled, make the app read-only, keep all data. Recovering customers find their data intact — deletion is irreversible and destroys trust.
The Checkout Flow — Four Steps
email = Workspace owner’s email
name = Workspace name
metadata[workspace_id] = Workspace’s Unique ID
→ Save response.id to Workspace’s stripe_customer_id
mode = “subscription”
customer = Workspace’s stripe_customer_id
line_items[0].price = Selected Plan’s stripe_price_id
subscription_data.metadata[workspace_id] = Workspace Unique ID
success_url = /billing/success?session_id={CHECKOUT_SESSION_ID}
cancel_url = /billing
| Stripe Event | What to Do in Bubble |
|---|---|
| checkout.session.completed | Find Workspace by metadata.workspace_id → set stripe_sub_id, subscription_status = Active, plan record |
| customer.subscription.updated | Update plan and subscription_status — handles upgrades, downgrades, renewals |
| invoice.payment_failed | Set status = Past Due → email owner with update-card link |
| customer.subscription.deleted | Set status = Cancelled → make read-only, preserve all data |
| customer.subscription.trial_will_end | Send trial-ending email 3 days before expiry with direct checkout link |
| invoice.payment_succeeded | Ensure status = Active (corrects any Past Due state) |
Show “Invite” button when:
Workspace’s seats_used < Workspace’s plan’s seat_limit
// Workflow: server-side guard (Step 1 of every sensitive workflow)
Only when:
Workspace’s seats_used < Workspace’s plan’s seat_limit
AND Workspace’s subscription_status = Active
The Three Billing Mistakes That Cause Revenue Leakage
-
✕
Trusting the success redirect URL: Users can manipulate URL parameters. Only a validated webhook should activate a subscription. The redirect is for UX, not for business logic.
-
✕
Not validating webhook signatures: Any server can POST to your Bubble endpoint. Validate the Stripe-Signature header on every event or you are open to billing fraud.
-
✕
Billing per user instead of per workspace: Creates orphaned Stripe customers, broken billing states, and an impossible data model when users belong to multiple workspaces.
Need Help with Your Stripe Integration?
We build Stripe billing into Bubble SaaS apps every week. Let’s make yours bulletproof.
