Demo App
Understand how the Next.js demo proves provider portability through shared routes and provider-specific server configuration.
Demo App
This repository includes a Next.js App Router demo in apps/demo-nextjs.
Its purpose is to provide a demo of OpenBilling objective: switching between Stripe and Dodo should not require rewriting app-level billing routes.
About
- The homepage redirects to
/pricing/demo - Provider switching is centralized in
src/lib/billing.ts - The route handlers stay provider-neutral
- Success and cancellation pages are shared across providers
- Webhook handling stays on the shared
@openbilling/corecontract
Route surface
The demo exposes three billing routes:
POST /api/checkout
Expected request body:
{
"customerEmail": "demo@example.com"
}Behavior:
- Validates that
customerEmailis present - Builds provider-specific checkout input in
src/lib/billing.ts - Derives
successUrlandcancelUrlfrom the request origin - Returns:
{
"id": "checkout_123",
"url": "https://checkout.example.com/session",
"provider": "stripe"
}POST /api/portal
Expected request body:
{
"customerId": "cus_123"
}Behavior:
- Validates that
customerIdis present - Creates a provider portal link
- Uses
${origin}/pricing/demoas the return URL - Returns:
{
"url": "https://billing.example.com/portal",
"provider": "dodo"
}POST /api/webhook
Behavior:
- Reads the raw request body as text
- Passes all headers through to
verifyWebhook - Logs the normalized event
- Returns:
{
"received": true
}The webhook route does not branch on provider-specific payload types.
Provider switching
The demo keeps provider switching in one helper:
export function getBillingProvider(): BillingProvider {
switch (getConfiguredProviderName()) {
case Provider.Stripe:
return createStripeProvider({
apiKey: requireEnv('STRIPE_API_KEY'),
webhookSecret: requireEnv('STRIPE_WEBHOOK_SECRET'),
});
case Provider.Dodo:
return createDodoProvider({
apiKey: requireEnv('DODO_API_KEY'),
webhookSecret: requireEnv('DODO_WEBHOOK_SECRET'),
baseUrl: 'https://test.dodopayments.com',
});
}
}Checkout input is also built in one place so the route handlers do not need to know whether the active provider wants priceId or productId.
Demo environment variables
Shared:
BILLING_PROVIDER=stripeStripe:
STRIPE_API_KEY=...
STRIPE_WEBHOOK_SECRET=...
STRIPE_PRICE_ID=...Dodo:
DODO_API_KEY=...
DODO_WEBHOOK_SECRET=...
DODO_PRODUCT_ID=...The demo uses Stripe priceId and Dodo productId values behind the server-side helper rather than exposing those differences in route-level API design.
Run the demo
pnpm install
pnpm build
pnpm devThen open the app and use the shared pricing flow at /pricing/demo.
Checkout flow behavior
The current demo flow is subscription-focused:
- The pricing form collects only an email address
- The checkout route builds the provider-specific catalog input
- The success page confirms the hosted flow returned successfully
- The cancel page confirms that nothing was provisioned
Important detail:
- The success page is not the source of truth for billing state
- The webhook route is the source of truth for normalized billing events
Demo-only caveats
- The Dodo demo adapter uses
https://test.dodopayments.com - The UI contains aspirational marketing copy & components that is not the supported SDK surface and is meant for a visual aid