~/projects/billing-dashboard > claude plan

Customer Billing Dashboard

Implementation plan generated by Claude Code — 14-day sprint, full-stack billing UI with Stripe integration

Project Overview

Building a customer-facing billing dashboard that allows users to manage every aspect of their subscription lifecycle. The dashboard integrates directly with Stripe as the source of truth for billing state and exposes a clean, responsive UI for self-service account management.

Tech Stack

User Stories

  1. As a customer, I can view my current subscription plan, billing cycle, and next invoice date
  2. As a customer, I can browse and filter my invoice history and download PDF receipts
  3. As a customer, I can upgrade or downgrade my plan and preview proration charges before confirming
  4. As a customer, I can add, remove, and set a default payment method using Stripe Elements
  5. As a customer, I can view my API usage, storage consumption, and seat utilization with daily/weekly/monthly charts
  6. As a customer, I receive email notifications for upcoming invoices, failed payments, and plan changes

Architecture Plan

File structure follows Next.js App Router conventions with co-located components and a clean separation between API routes, UI components, and shared utilities.

src/
  app/
    (dashboard)/
      billing/
        page.tsx                  # Main billing overview
        invoices/
          page.tsx                # Invoice history list
          [id]/page.tsx           # Invoice detail + PDF download
        subscription/
          page.tsx                # Current plan + upgrade/downgrade
        payment-methods/
          page.tsx                # Saved cards, add new
        usage/
          page.tsx                # Usage charts + quotas
    api/
      billing/
        invoices/route.ts         # GET invoices from Stripe
        subscription/route.ts     # CRUD subscription
        payment/route.ts          # Payment method management
        webhook/route.ts          # Stripe webhook handler
  components/
    billing/
      InvoiceTable.tsx            # Sortable, filterable invoice list
      PlanCard.tsx                # Current plan display + CTA
      UsageChart.tsx              # Recharts usage visualization
      PaymentMethodCard.tsx       # Card display with delete/default
      BillingAlert.tsx            # Overdue, expiring, limit warnings
  lib/
    stripe.ts                     # Stripe client singleton
    billing-utils.ts              # Format currency, dates, plan names
  types/
    billing.ts                    # TypeScript interfaces

Implementation Phases

Phase 1: Foundation (Days 1–3)

  • Initialize Stripe SDK with singleton pattern and environment-based key management
  • Build webhook handler with signature verification and event routing
  • Design and migrate database schema: customers, subscriptions, invoices, usage_records
  • Implement auth middleware that scopes all billing routes to the authenticated customer ID
  • Set up error boundaries, Suspense loading states, and toast notification system

Phase 2: Core Views (Days 4–7)

  • Billing overview dashboard: current plan card, next invoice preview, payment status badge, and quick actions
  • Invoice history with server-side pagination, date range filtering, status filtering (paid/open/void), and search
  • PDF invoice generation using @react-pdf/renderer with branded template
  • Subscription management: plan comparison grid, upgrade/downgrade flow with Stripe proration preview
  • Payment method CRUD with Stripe Elements integration for PCI-compliant card collection

Phase 3: Usage & Analytics (Days 8–10)

  • Build usage tracking pipeline: API call counting, storage metering, seat tracking with per-minute granularity
  • Usage dashboard with Recharts: line charts (daily/weekly/monthly), area charts for quota visualization
  • Quota alert system: 80% threshold warning, 95% critical, 100% hard limit with grace period
  • Usage-based billing calculation display showing projected overage charges before cycle end

Phase 4: Polish (Days 11–14)

  • Email notification templates: upcoming invoice (3 days before), payment failed (immediate + retry schedule), plan changed confirmation
  • Stripe Billing Portal deep link for edge-case self-service (tax ID, billing address)
  • Accessibility audit targeting WCAG 2.1 AA: keyboard navigation, screen reader labels, focus management
  • End-to-end test suite with Playwright covering upgrade flow, payment method add/remove, and webhook processing
  • Rate limiting on mutation endpoints: 10 req/min for plan changes, 30 req/min for payment method operations

Key Technical Decisions

1
Server Components for invoice list

Reduces client bundle size significantly. Invoice data fetches happen on the server, and the component streams to the client as ready. No client-side Stripe SDK needed for read operations.

2
Stripe Checkout for upgrades

Don't rebuild the payment flow. Use Stripe's hosted checkout with a return URL for plan upgrades. This offloads PCI compliance, 3DS handling, and payment method validation entirely to Stripe.

3
Webhook-first architecture

All billing state changes flow through Stripe webhooks rather than API polling. The local database is a read cache of Stripe's state. Webhook handler processes: invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, payment_method.attached, payment_method.detached.

4
Optimistic UI for payment methods

When a user adds a payment method via Stripe Elements, show it immediately in the UI before the webhook confirms. This provides instant feedback. Reconcile on webhook delivery and roll back if the payment method was rejected.

5
ISR for plan pages

Pricing and plan comparison pages use Incremental Static Regeneration with a 1-hour revalidation window. Plan details change infrequently, so this avoids unnecessary Stripe API calls while keeping content fresh enough.

Data Model

Prisma schema with PostgreSQL. The local database mirrors Stripe state received through webhooks and adds application-specific fields like usage tracking.

model Customer {
  id                String         @id @default(uuid())
  email             String         @unique
  name              String?
  stripeCustomerId  String         @unique @map("stripe_customer_id")
  createdAt         DateTime       @default(now())
  updatedAt         DateTime       @updatedAt

  subscriptions     Subscription[]
  invoices          Invoice[]
  usageRecords      UsageRecord[]

  @@map("customers")
}

model Subscription {
  id                    String    @id @default(uuid())
  stripeSubscriptionId  String    @unique @map("stripe_subscription_id")
  customerId            String    @map("customer_id")
  status                String    // active, past_due, canceled, trialing
  planId                String    @map("plan_id")
  planName              String    @map("plan_name")
  currentPeriodStart    DateTime  @map("current_period_start")
  currentPeriodEnd      DateTime  @map("current_period_end")
  cancelAtPeriodEnd     Boolean   @default(false) @map("cancel_at_period_end")
  createdAt             DateTime  @default(now())
  updatedAt             DateTime  @updatedAt

  customer              Customer  @relation(fields: [customerId], references: [id])

  @@map("subscriptions")
}

model Invoice {
  id                String    @id @default(uuid())
  stripeInvoiceId   String    @unique @map("stripe_invoice_id")
  customerId        String    @map("customer_id")
  amountDue         Int       @map("amount_due")       // in cents
  amountPaid        Int       @map("amount_paid")      // in cents
  currency          String    @default("usd")
  status            String    // draft, open, paid, void, uncollectible
  invoiceUrl        String?   @map("invoice_url")
  pdfUrl            String?   @map("pdf_url")
  periodStart       DateTime  @map("period_start")
  periodEnd         DateTime  @map("period_end")
  createdAt         DateTime  @default(now())

  customer          Customer  @relation(fields: [customerId], references: [id])

  @@map("invoices")
}

model UsageRecord {
  id          String    @id @default(uuid())
  customerId  String    @map("customer_id")
  metric      String    // api_calls, storage_bytes, seats
  quantity    BigInt
  timestamp   DateTime  @default(now())

  customer    Customer  @relation(fields: [customerId], references: [id])

  @@index([customerId, metric, timestamp])
  @@map("usage_records")
}

API Endpoints

Method Path Description Auth
GET /api/billing/invoices List invoices with pagination, date range, and status filters Yes
GET /api/billing/invoices/:id Get invoice detail including line items Yes
GET /api/billing/invoices/:id/pdf Generate and download invoice PDF Yes
GET /api/billing/subscription Get current subscription details and available plans Yes
POST /api/billing/subscription/change Upgrade/downgrade plan with proration preview Yes
POST /api/billing/subscription/cancel Cancel subscription at period end Yes
GET /api/billing/payment-methods List saved payment methods Yes
POST /api/billing/payment-methods Attach a new payment method (Stripe token) Yes
DELETE /api/billing/payment-methods/:id Detach a payment method Yes
POST /api/billing/payment-methods/:id/default Set payment method as default Yes
GET /api/billing/usage Get usage data with metric and date range params Yes
POST /api/billing/webhook Stripe webhook receiver (signature verified) Stripe Sig

Security Considerations

Risks & Mitigations

Stripe API rate limits during bulk invoice fetches
→ Implement cursor-based pagination with a 100-invoice page size and cache responses in Redis with a 5-minute TTL. Prefetch the next page in the background while the user views current results.
Proration calculation complexity
→ Never calculate proration manually. Use Stripe's upcoming_invoice preview endpoint to get exact proration amounts. Display the Stripe-calculated amount to the user before confirming the plan change.
Webhook delivery failures
→ Implement idempotent webhook handlers using the event ID as a deduplication key. Store processed event IDs in a set with 72-hour expiry. Set up a dead letter queue for events that fail processing after 3 retries.
Currency formatting edge cases
→ Use Intl.NumberFormat with the currency code from Stripe (supports all 135+ currencies). Handle zero-decimal currencies (JPY, KRW) by checking Stripe's currency metadata rather than assuming cents.
Stale subscription state after checkout
→ After Stripe Checkout redirect, poll the subscription endpoint for up to 30 seconds with exponential backoff until the webhook has processed. Show a "confirming your plan change" loading state during this window.