Skip to content

Architecture

OpenCheckout is a Next.js application that orchestrates the Open Payments protocol. This page explains how the internal components work together to process a payment from session creation to completion.

System Components

┌──────────────────────────────────────────────────────┐
│ OpenCheckout │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Merchant │ │ Customer │ │
│ │ API │ │ Checkout UI │ │
│ │ /api/checkout│ │ /pay/:sessionId │ │
│ └──────┬──────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌──────┴──────────────────┴─────────┐ │
│ │ Checkout Engine │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Session │ │ State Machine │ │ │
│ │ │ Manager │ │ guarded flow │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ └──────┬───────────────────────────┘ │
│ │ │
│ ┌──────┴───────────────────────────┐ │
│ │ Open Payments Orchestrator │ │
│ │ ┌────────┐ ┌───────┐ ┌───────┐ │ │
│ │ │Incoming│ │Quote │ │Outgoing│ │ │
│ │ │Payment │ │ │ │Payment │ │ │
│ │ └────────┘ └───────┘ └───────┘ │ │
│ │ ┌────────┐ ┌─────────────────┐ │ │
│ │ │ Grant │ │ Hash │ │ │
│ │ │ Handler│ │ Verification │ │ │
│ │ └────────┘ └─────────────────┘ │ │
│ └──────┬───────────────────────────┘ │
│ │ │
│ ┌──────┴──────┐ ┌──────────┐ ┌──────────────┐ │
│ │ SQLite DB │ │ Webhook │ │ Open Payments│ │
│ │ (Drizzle) │ │ Delivery │ │ SDK │ │
│ └─────────────┘ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────┘

Checkout Engine

The checkout engine manages the lifecycle of every checkout session. It consists of:

  • Session Manager (src/lib/checkout/sessions.ts) — Creates, retrieves, and updates sessions in the database. Handles pagination, session expiry, and Open Payments reference tracking.
  • State Machine (src/lib/checkout/state-machine.ts) — Enforces preparation, approval, processing, completion, expiry, and cancellation transitions. Terminal states cannot be changed.
  • Idempotency (src/lib/checkout/idempotency.ts) — Stores merchant-scoped hashes of Idempotency-Key headers. Duplicate requests within 24 hours return the cached response and expired records are cleaned opportunistically.

Open Payments Orchestrator

The orchestrator (src/lib/open-payments/) wraps the @interledger/open-payments SDK and handles the full payment protocol flow:

  • Wallet Address (wallet-address.ts) — Resolves wallet address URLs to discover the associated authorization server and resource server.
  • Incoming Payment (incoming-payment.ts) — Requests an incoming payment grant from the merchant’s authorization server, then creates an incoming payment resource on the merchant’s wallet.
  • Quote (quote.ts) — Requests a quote grant from the customer’s authorization server, then creates a quote resource to determine the payment cost.
  • Grant (grant.ts) — Handles the interactive GNAP grant flow for outgoing payments, including nonce generation and grant continuation.
  • Outgoing Payment (outgoing-payment.ts) — Creates an outgoing payment resource on the customer’s wallet to issue the payment instruction.
  • Hash Verification (hash-verification.ts) — Verifies the SHA-256 interaction hash returned by the authorization server during the redirect callback.

Database

OpenCheckout uses SQLite with WAL mode enabled for concurrent read performance. The schema is managed by Drizzle ORM and contains five core tables:

  • merchants — merchant accounts with encrypted private keys
  • api_keys — SHA-256 hashed API keys for authentication
  • checkout_sessions — the core entity with session state, line items, and Open Payments references
  • idempotency_keys — request reservations and cached API responses
  • webhook_events — delivery log for outbound webhook attempts

No separate database server is required. The SQLite file is stored at the path configured by DATABASE_URL.

Webhook Delivery

When a session completes, the webhook engine (src/lib/webhook/deliver.ts) stores an event and POSTs a signed JSON payload to the merchant’s configured URL. Delivery is attempted up to 3 times immediately. The authenticated maintenance job replays undelivered stored events up to the durable attempt limit.

Security Layer

  • API key authentication — all /api/checkout endpoints verify the Authorization: Bearer sk_xxx header against SHA-256 hashed keys in the database.
  • Ed25519 request signing — every Open Payments API call is signed with the merchant’s Ed25519 private key using HTTP Message Signatures.
  • AES-256-GCM encryption — merchant private keys are encrypted at rest with a key derived from the ENCRYPTION_KEY environment variable using scrypt.
  • Hash verification — the interaction hash from the authorization server’s redirect is verified to prevent forged callbacks.
  • URL validation — webhook URLs are validated to prevent Server-Side Request Forgery. Redirect URLs are validated to prevent open redirects.

Payment Flow — Step by Step

1. Session Creation (Merchant API)

POST /api/checkout/sessions
→ Authenticate API key
→ Check idempotency key (if provided)
→ Validate request body (Zod schema)
→ Insert session row (status: "open")
→ Return session with checkout URL

At this point, no Open Payments resources have been created. The session is just a database record waiting for a customer.

2. Wallet Submission (Customer Action)

POST /pay/:sessionId/wallet
→ Validate wallet address URL
→ Resolve wallet address (discover auth/resource servers)
→ STEP 1: Create incoming payment on merchant's wallet
→ Request incoming payment grant from merchant's auth server
→ Create incoming payment resource
→ STEP 2: Create quote on customer's ASE
→ Request quote grant from customer's auth server
→ Create quote with incoming payment URL as receiver
→ STEP 3: Request interactive outgoing payment grant
→ Generate nonce for hash verification
→ Request grant from customer's auth server with interact.finish
→ Store each external reference as it is created
→ Move the session to awaiting_approval
→ Return interact redirect URL to frontend

3. Payment Approval (ASE Interaction)

The customer is redirected to their authorization server’s interact URL. They log in to their account, review the payment details, and approve. The authorization server redirects back to the OpenCheckout grant callback.

4. Grant Callback (Payment Completion)

GET /pay/:sessionId/grant/callback?interact_ref=xxx&hash=xxx
→ Verify interaction hash (defense-in-depth)
→ Continue the grant with the interact_ref
→ Obtain the finalized access token
→ Create outgoing payment with quote ID
→ Atomically claim awaiting_approval → processing
→ Mark session as completed and purge grant continuation secrets
→ Fire webhook (background, non-blocking)
→ Redirect to OpenCheckout success page

5. Success Page

The OpenCheckout success page displays a confirmation with a countdown, then forwards the customer to the merchant’s success_url.

State Machine

open → preparing → awaiting_approval → processing → completed
│ │ │
├────────┴─────────────────┴────────→ expired
└───────────────────────────────────→ canceled

A preparation failure can safely return preparing to open. Once a session reaches a terminal state (completed, expired, or canceled), no further transitions are allowed.

Technology Stack

LayerTechnology
FrameworkNext.js 16 (App Router) with TypeScript
DatabaseSQLite via better-sqlite3 with Drizzle ORM
Payments@interledger/open-payments SDK
UIReact 19 with Tailwind CSS, Lucide icons
DeploymentDocker with multi-stage Node.js build
DocumentationStarlight (Astro)