Payment System Architecture

Architecture and design decisions for the payment system

System Architecture

The payment system follows a layered architecture with clear separation of concerns:

┌─────────────────────────────────────────────────────────────┐
│                    API Routes (Next.js)                     │
│  Thin layer: auth, params extraction, delegate to service  │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│            Service Layer (@zooly/srv-stripe-payment)          │
│  Business logic: payment completion, payouts, refunds        │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│              Access Layer (@zooly/app-db)                    │
│  Database operations: CRUD, transactions, queries             │
└───────────────────────┬─────────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────────┐
│              Database Schema (Drizzle ORM)                   │
│  Tables: stripe_payments, pay_out, payment_share_tracking  │
└─────────────────────────────────────────────────────────────┘

High-Level Flow Diagram

Payment Intent Creation Flow

Payment Completion Flow

Payment completion uses a dual-path approach. The client endpoint is the primary path; the webhook is the fallback. Both call the same idempotent completePayment() function with FOR UPDATE row locking.

Revenue Distribution Flow

Payout Processing Flow

Refund Flow

Key Design Decisions

1. Account-Centric Architecture

  • All payments keyed by accountId (not userId)
  • Enables multi-user accounts and better separation of concerns
  • System accounts (zooly_acc, stripe_acc) for platform/fee tracking

2. Atomic Operations

  • Critical paths wrapped in database transactions
  • Prevents inconsistencies from partial failures
  • Uses FOR UPDATE locks where needed for concurrency

3. Dual-Path Completion with Idempotency

  • Client endpoint (POST /complete) is the primary completion path — queries Stripe API to verify charge status
  • Webhook (charge.succeeded) is the fallback — covers cases where the client never calls the endpoint
  • Both paths call the same completePayment() function with FOR UPDATE row locking
  • markStripePaymentCompleted returns null if already completed — only one caller succeeds
  • Safe to process duplicate calls from either path

4. Two-Phase Payout Commit

  • Phase 1: Create PENDING PayOut + close tracking (DB)
  • Phase 2: Create Stripe transfer
  • Phase 3: Update PayOut to CHARGE (or CANCELED if transfer fails)
  • Prevents double-payout if DB write fails after successful Stripe transfer

5. Product as Source of Truth for Pricing

  • Price breakdown (Stripe fee, platform fee, talent gross share) lives in the product table
  • Product is created first; the payment system fetches pricing from the product via getProductByPayForId
  • Agent/ambassador/host partner shares calculated at completion time
  • Ensures stakeholder relationships are looked up fresh

6. System Accounts for Fees

  • Replaced v1's virtual user IDs with real account records
  • zooly_acc: Platform fee tracking (auto-closed)
  • stripe_acc: Stripe fee tracking (auto-closed)
  • Makes payeeAccountId NOT NULL across all tracking records

7. Advance Payout Offset

  • Advance payouts create PayOut with advanceAmountMinorUnit > 0
  • Future earnings automatically offset against advance balance
  • Refunds of paid-out tracking increment advance balance

8. Product-Agnostic Design

  • Payment system doesn't hard-code product schemas beyond routing
  • Linked via payFor enum + payForId foreign key (e.g. IP term id, merch session id, offer id for OFFER)
  • Product details live in domain-specific tables; getProductByPayForId() returns a normalized PaymentProduct

File Structure

packages/
├── db/src/
│   ├── schema/
│   │   ├── paymentEnums.ts          # All payment pgEnums
│   │   ├── stripePayments.ts        # Master transaction record
│   │   ├── payOut.ts                # Completed payouts
│   │   ├── paymentShareTracking.ts  # Revenue distribution
│   │   ├── paymentSystemLogs.ts    # Audit trail
│   │   ├── accountPayoutRoutes.ts   # Payout method config
│   │   ├── ppu.ts         # Proof of purchase
│   │   └── accountAgents.ts         # Agent relationships
│   └── access/
│       ├── stripePayments.ts        # Payment CRUD
│       ├── payOut.ts                # Payout CRUD
│       ├── paymentShareTracking.ts  # Tracking CRUD
│       └── ...                      # Other access functions
└── srv-stripe-payment/src/
    ├── stripe.ts                    # Stripe client & operations
    ├── getProductByPayForId.ts      # Product resolution and pricing (inc. OFFER / escrow amounts)
    ├── insertShareTrackingForProduct.ts  # Batch tracking + advance offset (shared completion + offer release)
    ├── completePayment.ts           # Payment completion (escrow branch for OFFER)
    ├── releaseOfferEscrowForOffer.ts     # OFFER: create tracking after delivery accepted
    ├── refund.ts                    # Refund processing
    ├── advancePayout.ts            # Advance payout
    ├── payoutDaemon.ts             # Daily cron daemon
    └── doPayout.ts                  # Per-account payout logic

apps/zooly-app/app/api/payments/
├── create-intent/route.ts          # Create payment intent
├── complete/route.ts                # Payment completion (primary path, verifies with Stripe)
├── webhook/stripe/route.ts          # Stripe webhook handler
├── process-payouts/route.ts         # Payout cron endpoint
├── refund/route.ts                  # Refund endpoint
└── admin/
    ├── do-payout/route.ts           # Manual payout
    └── advance-payout/route.ts      # Advance payout

Environment Variables

VariablePurposeRequired
STRIPE_SECRET_KEYStripe API authentication (sk_xxx)Yes
STRIPE_PUBLISHABLE_KEYStripe.js frontend (pk_xxx)Yes
STRIPE_WEBHOOK_SECRETWebhook signature verification (whsec_xxx)Yes
CRON_SECRETBearer token for cron endpoint authYes
DATABASE_URLPostgreSQL connection stringYes

Next Steps