How data moves from session creation to payment confirmation.
Merchant
│ has_many
▼
CheckoutSession ──belongs_to──▶ Order ──has_one──▶ Invoice ──has_one──▶ Payment
│ │ │ │
│ token │ order_number │ payment_request │ preimage
│ amount_cents │ total_amount_sats │ payment_hash │ amount_sats
│ customer_* │ items (jsonb) │ │ btc_fiat_price
│ payment_method │ metadata (jsonb) │ │ fiat_variance
│ success_url / cancel_url │ │ │ confirmed_at
│ expires_at (15 min) │ │ expires_at │
│ │ │ │
└─ UI session (disposable) └─ Business record └─ Network request └─ Settlement proof
(permanent) (per attempt) (immutable)
Each table captures a distinct stage with a different lifecycle:
| Table | Purpose | Lifecycle |
|---|---|---|
| CheckoutSession | Temporary UI session. Captures customer intent and info before any money is involved. Expires in 15 minutes. | Disposable |
| Order | Permanent business record. The merchant's receipt — tied to items, amounts, and a wallet. Feeds reports, dashboards, and refunds. | Permanent |
| Invoice | Payment-network request. Holds the BOLT-11 string (Lightning) plus confirmation tracking. One Order may need a new Invoice if the first expires. | Per attempt |
| Payment | Proof of settlement. Records the exact BTC/fiat price at confirmation, the preimage, and calculates price variance. | Immutable |
Three ways to start a checkout, one unified flow:
POST /api/v1/checkout_sessions
Server-side integration. Returns checkout_url to redirect customer.
GET /pay/:slug
Zero-code. Share a URL via email, social media, or QR code.
data-key="pk_live_..."
JavaScript widget. Opens checkout overlay from any webpage.
All three converge at the same CheckoutController, entering the 3-step pipeline below.
API, payment link, or embed widget creates a CheckoutSession. Customer is redirected to the checkout page.
WRITE: checkout_sessions
token, amount_cents, currency, merchant_id, product_id, success_url, cancel_url, metadata, expires_at
If the merchant requires customer info collection and it was not pre-filled via API, the customer fills out a form.
WRITE: checkout_sessions
customer_email, customer_name, customer_phone, customer_address
On the first visit to the QR code page, the system creates an Order, links it to the session, converts the fiat amount to sats at the current BTC price, and generates an Invoice with a BOLT-11 payment request.
WRITE: orders
merchant_id, wallet_id, total_amount_cents, total_amount_sats, items, metadata, status = pending
WRITE: checkout_sessions
order_id = new order
WRITE: invoices
payment_request, payment_hash, amount_sats, expires_at
The checkout page displays a QR code. Background jobs begin polling for payment:
PaymentMonitorJob polls every 5 seconds (up to 15 minutes).
The checkout page listens on WebSocket (ActionCable) for instant updates, with HTTP polling every 5 seconds as fallback.
When a background job detects settlement:
WRITE: invoices
status = paid, paid_at
WRITE: orders
status = paid
WRITE: payments
amount_sats, preimage, btc_fiat_price, fiat_value, fiat_variance, confirmed_at
WRITE: checkout_sessions
status = completed
WebSocket broadcasts to checkout page, which redirects the customer to success_url. Webhooks fire asynchronously.
If 15 minutes pass with no payment detected:
WRITE: invoices
status = expired
WRITE: checkout_sessions
status = expired
Customer is redirected to cancel_url.
pending ──▶ completed (paid)
pending ──▶ expired (15 min)
pending ──▶ cancelled
pending ──▶ invoice_generated
──▶ paid
──▶ cancelled
paid ──▶ refunded
pending ──▶ paid (settled)
pending ──▶ expired (TTL)
pending ──▶ cancelled
No status enum.
Created once on confirmation.
Immutable after creation.
Proof: preimage (Lightning)
| # | Trigger | Table | Operation |
|---|---|---|---|
| 1 | API call / payment link / embed | checkout_sessions |
INSERT token, amount, currency, merchant, product, urls, metadata, expires_at |
| 2 | Customer submits info form | checkout_sessions |
UPDATE customer_email, customer_name, customer_phone, customer_address |
| 3 | First visit to QR page | orders |
INSERT merchant, wallet, amounts, items, metadata |
| 5 | First visit to QR page | checkout_sessions |
UPDATE order_id = new order |
| 6 | Invoice generation | orders |
UPDATE total_amount_sats (fresh BTC price) |
| 7 | Invoice generation | invoices |
INSERT payment_request, payment_hash, amount_sats, expires_at |
| 8 | Background job detects payment | invoices |
UPDATE status = paid, paid_at |
| 9 | Background job detects payment | orders |
UPDATE status = paid |
| 10 | Background job detects payment | payments |
INSERT amount_sats, preimage, btc_fiat_price, fiat_value, fiat_variance, confirmed_at |
| 11 | Background job detects payment | checkout_sessions |
UPDATE status = completed |