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 ofIdempotency-Keyheaders. 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 keysapi_keys— SHA-256 hashed API keys for authenticationcheckout_sessions— the core entity with session state, line items, and Open Payments referencesidempotency_keys— request reservations and cached API responseswebhook_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/checkoutendpoints verify theAuthorization: Bearer sk_xxxheader 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_KEYenvironment 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 URLAt 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 frontend3. 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 page5. 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 └───────────────────────────────────→ canceledA 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
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) with TypeScript |
| Database | SQLite via better-sqlite3 with Drizzle ORM |
| Payments | @interledger/open-payments SDK |
| UI | React 19 with Tailwind CSS, Lucide icons |
| Deployment | Docker with multi-stage Node.js build |
| Documentation | Starlight (Astro) |