Stripe Integration
1.0.0Webhook topology, price configuration, and billing flows.
Stripe Billing & Webhook Guide
This guide explains how Stripe powers App Factory’s platform subscriptions, how webhooks are processed, and how generated apps interface with platform billing. It replaces the per-app webhook instructions used in the legacy multi-bundle workflow.
Last reviewed: October 16, 2025
1. What Stripe Covers
| Scope | Description |
|---|---|
| Platform subscriptions | Customers purchase App Factory plans (Starter, Pro, Enterprise). |
| Billing lifecycle | Trials, upgrades/downgrades, cancellations, invoices. |
| Usage data | Webhooks hydrate Supabase tables so the dashboard can show subscription state. |
| Customer apps | By default, generated apps do not get Stripe keys. Customers can plug in their own provider later. |
2. Products & Prices
Create or verify the following in the Stripe Dashboard:
| Price ID | Name | Interval | Metadata |
|---|---|---|---|
price_appfactory_starter_monthly | Starter | Monthly | {"plan":"starter"} |
price_appfactory_pro_monthly | Pro | Monthly | {"plan":"pro"} |
price_appfactory_enterprise_monthly | Enterprise | Monthly | {"plan":"enterprise"} |
Store the IDs in Doppler:
doppler secrets set STRIPE_PRICE_STARTER=price_appfactory_starter_monthly
doppler secrets set STRIPE_PRICE_PRO=price_appfactory_pro_monthly
doppler secrets set STRIPE_PRICE_ENTERPRISE=price_appfactory_enterprise_monthly
Shared secrets:
doppler secrets set STRIPE_SECRET_KEY=sk_test_...
doppler secrets set STRIPE_PUBLISHABLE_KEY=pk_test_...
doppler secrets set STRIPE_WEBHOOK_SECRET=whsec_...
The Forge CLI writes these values into generated .env.*.generated files for dashboard and landing apps.
3. Checkout & Customer Portal
API routes under apps/dashboard/src/app/api/billing use common/lib/payments/src/stripe-billing.ts:
const billing = new StripeBilling({
secretKey: process.env.STRIPE_SECRET_KEY!,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
priceIds: {
starter: process.env.STRIPE_PRICE_STARTER!,
pro: process.env.STRIPE_PRICE_PRO!,
enterprise: process.env.STRIPE_PRICE_ENTERPRISE!,
},
successUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/billing/success`,
cancelUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/billing`,
});
export async function POST(request: NextRequest) {
const payload = await request.json();
const session = await billing.createCheckoutSession({
orgId: payload.organizationId,
planId: payload.planId,
trialDays: payload.trialDays ?? 14,
});
return NextResponse.json({ url: session.url });
}
4. Webhook Processing
Endpoint: POST /api/billing/webhook (dashboard app).
Events We Listen For
| Event | Purpose |
|---|---|
customer.subscription.created | Start trial/subscription. |
customer.subscription.updated | Plan changes, quantity updates, status transitions. |
customer.subscription.deleted | Cancellations. |
invoice.payment_succeeded | Track successful billing cycle. |
invoice.payment_failed | Trigger email + dashboard alerts. |
checkout.session.completed | Finalize onboarding immediately after checkout. |
Handler Flow
export async function POST(req: NextRequest) {
const signature = req.headers.get('stripe-signature');
const body = await req.text();
const event = billing.parseWebhook(body, signature ?? '');
await applyStripeEvent(event); // writes to Supabase
return NextResponse.json({ received: true });
}
applyStripeEvent lives in common/lib/payments/src/webhook-handler.ts and performs:
- Idempotency check (
webhook_eventstable in Supabase). - Mapping of Stripe customer → organization ID (stored in
billing_customers). - Upserts subscription state into
org_subscriptions. - Emits audit/log events for dashboard notifications.
5. Local Development
# Start dashboard (port 3001 by default)
pnpm --filter apps/dashboard dev
# Listen for Stripe events and forward to dashboard
stripe listen --forward-to localhost:3001/api/billing/webhook
# Trigger events
stripe trigger checkout.session.completed
stripe trigger invoice.payment_succeeded
- Store the CLI-provided webhook signing secret as
STRIPE_WEBHOOK_SECRET. - Use test cards (e.g.,
4242 4242 4242 4242) during checkout. - Playwright subscription tests (
tests/subscription/*) leverage these flows in mock mode.
6. Generated Customer Apps & Stripe
- Generated apps are Stripe-agnostic by default.
- To give a customer Stripe access, update their entry in
ops/apps.yaml:
apps:
my-customer-app:
features:
payments: true
secrets:
- STRIPE_PUBLISHABLE_KEY_CUSTOMER
- STRIPE_SECRET_KEY_CUSTOMER
- STRIPE_WEBHOOK_SECRET_CUSTOMER
- Forge CLI will scaffold placeholder endpoints and remind the operator to populate Doppler secrets.
- Customers can also integrate their own billing solution (instructions land in app README).
7. Monitoring & Alerts
- Stripe Dashboard configured with email alerts for failed payments.
- Webhook handler writes to Supabase
billing_events; Grafana/Metabase dashboards use these tables. - PagerDuty/Slack integration hooks into Supabase triggers to notify the team about repeated payment failures or delinquent invoices.
8. Troubleshooting
| Issue | Fix |
|---|---|
Signature verification failed | Ensure STRIPE_WEBHOOK_SECRET matches the current endpoint. Regenerate after using stripe listen. |
No organization mapped to customer | Run stripe customers update with metadata[organizationId]=... or check billing_customers table. |
Webhook retries piling up | Check dashboard logs (apps/dashboard/.vercel/logs) and Supabase webhook_events for error details. |
Test mode data showing in production | Confirm you toggled to Live mode in Stripe; keys are environment-specific. |
Keep this guide current as billing flows evolve. If you add new events or change webhook destinations, update the event table and handler examples here.