Customer Billing

Customer billing separates what customers can see from what operators can investigate.

On this page

Customer and operator surfaces

SurfaceWhat it showsNotes
/activityCurrent subscriptions, usage, credits, and recent billing events.Requires billing access and provides the current activity dashboard.
/billing/historyCustomer-owned purchases, payments, invoices, refunds, and subscription changes in one activity container.Uses normalized activity rows and does not expose raw webhook payloads or operator controls.
/api/billing/portalProvider-hosted portal launch for the signed-in subscription customer.Portal and receipt behavior depends on the selected provider and whether the customer has a subscription or one-time purchase.
/billing/customersOperator list of provider customers.Super-admin oriented customer operations.
/billing/customers/[providerCustomerId]Customer ledger detail, subscriptions, invoices, charges, refunds, disputes, entitlements, and webhooks.Provider capability differences are shown in UI.
/api/system/billing/exportCustomer-safe CSV exports of the local billing projection.Provider-specific fields and receipt links appear only where the selected adapter supports them.
Implementation reference3 areas

These Product code locations explain how the documented behavior is implemented. Expand them when you are ready to customize or maintain this area.

Customer-facing billing activity

Signed-in customers see account-owned normalized billing activity, not raw provider payloads.

  • src/app/(dashboard)/billing/page.tsx
  • src/features/billing/server/queries/activity.ts
  • src/features/billing/ui/hub/activity.tsx

Portal access

Portal launch uses the active provider and current customer record through a server service.

  • src/app/api/billing/portal/route.ts
  • src/features/billing/server/services/portal.ts
  • src/features/billing/server/providers/stripe/portal.ts
  • src/features/billing/server/providers/lemonsqueezy/portal.ts
  • src/features/billing/server/providers/dodo/portal.ts
  • src/features/billing/server/providers/polar/portal.ts
  • src/features/billing/server/providers/creem/portal.ts

Operator investigation

Customer detail pages, sync, refunds, and exports are admin-oriented workflows with provider capability differences.

  • src/app/(dashboard)/billing/customers/[providerCustomerId]/page.tsx
  • src/features/billing/server/queries/customer-history.ts
  • src/features/billing/server/services/customer-sync.ts
  • src/features/billing/server/services/admin-refund.ts
  • src/features/billing/server/export/service.ts

Reference paths are relative to the delivered product checkout.

Customer billing support flow

  1. 1

    Start from local history

    Customer and operator pages read projected subscriptions, orders, invoices, charges, refunds, disputes, and entitlements from local tables.

    Relevant Product code
    • src/features/billing/server/queries/current-profile-history.ts
    • src/features/billing/server/queries/customer-history.ts
  2. 2

    Use portal for customer changes

    The portal service resolves the active provider and customer before returning a provider-hosted URL.

    Relevant Product code
    • src/features/billing/server/services/portal.ts
  3. 3

    Sync provider state deliberately

    Provider refresh and refunds stay behind admin services so support actions remain auditable and provider-aware.

    Relevant Product code
    • src/features/billing/server/services/customer-sync.ts
    • src/features/billing/server/services/admin-refund.ts
  4. 4

    Export from local projections

    CSV exports are assembled from local customer and invoice rows after authorization checks.

    Relevant Product code
    • src/features/billing/server/export/request/auth.ts
    • src/features/billing/server/export/service.ts

Portal request schema

Use this example as a starting point, then adapt it to your product's rules and configuration.

Portal requests accept only relative return paths so provider redirects stay inside the app.

ts
const PortalBodySchema = z.object({
  return_path: z
    .string()
    .trim()
    .max(300)
    .refine((value) => value.startsWith("/") && !value.startsWith("//"), {
      message: "return_path must be a relative path",
    })
    .optional(),
  source: z.string().trim().max(80).optional().default("dashboard_billing_hub"),
});
Source reference
  • src/features/billing/server/services/portal.ts

Support workflow

  • Start from the customer detail page when investigating billing support.
  • Use the selected provider's sync or reconciliation path when the adapter supports it.
  • Use local projections as the dashboard source of truth after verified webhooks arrive.
  • Keep refunds behind super-admin checks and follow the selected provider's refund capability.
  • Treat provider disputes and chargebacks as provider-managed unless the selected adapter exposes a supported local operation.