OpenBilling

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/core contract

Route surface

The demo exposes three billing routes:

POST /api/checkout

Expected request body:

{
  "customerEmail": "demo@example.com"
}

Behavior:

  • Validates that customerEmail is present
  • Builds provider-specific checkout input in src/lib/billing.ts
  • Derives successUrl and cancelUrl from 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 customerId is present
  • Creates a provider portal link
  • Uses ${origin}/pricing/demo as 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=stripe

Stripe:

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 dev

Then 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

On this page