🔍⌘K

Start typing to search docs.

Stripe Integration

1.0.0

Webhook 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

ScopeDescription
Platform subscriptionsCustomers purchase App Factory plans (Starter, Pro, Enterprise).
Billing lifecycleTrials, upgrades/downgrades, cancellations, invoices.
Usage dataWebhooks hydrate Supabase tables so the dashboard can show subscription state.
Customer appsBy 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 IDNameIntervalMetadata
price_appfactory_starter_monthlyStarterMonthly{"plan":"starter"}
price_appfactory_pro_monthlyProMonthly{"plan":"pro"}
price_appfactory_enterprise_monthlyEnterpriseMonthly{"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

EventPurpose
customer.subscription.createdStart trial/subscription.
customer.subscription.updatedPlan changes, quantity updates, status transitions.
customer.subscription.deletedCancellations.
invoice.payment_succeededTrack successful billing cycle.
invoice.payment_failedTrigger email + dashboard alerts.
checkout.session.completedFinalize 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_events table 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

IssueFix
Signature verification failedEnsure STRIPE_WEBHOOK_SECRET matches the current endpoint. Regenerate after using stripe listen.
No organization mapped to customerRun stripe customers update with metadata[organizationId]=... or check billing_customers table.
Webhook retries piling upCheck dashboard logs (apps/dashboard/.vercel/logs) and Supabase webhook_events for error details.
Test mode data showing in productionConfirm 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.