Checkout as a standalone Hono SSR app on the edge
Status: Superseded by ADR 0013. The standalone checkout app and its
_internal/apps/checkoutmicroservice were removed; checkout is now handled in-app by the TanStack SPA callingapps.paymentsdirectly. Kept here for history — see 0013 for why.
The checkout page was embedded inside the main TanStack Start SPA (frontend/apps/web/src/routes/courses/$courseId/checkout.tsx). We decided to extract it into a separate, lightweight Edge-deployed app at frontend/apps/checkout/ that runs on Cloudflare Workers using Hono SSR with HTMX and Alpine.js.
Context
The checkout page had grown organically inside the main SPA. At ~520 lines it was a self-contained page, but being inside the SPA meant:
- It carried the full React 19 + TanStack Router + TanStack Query bundle (~120KB+) just to render a form and a QR code.
- Any change to the checkout page required building and deploying the entire SPA.
- The SPA runs on Cloudflare Workers via Nitro, but shared the Worker with all other routes — no isolation for a page that processes payments.
- Credit card data was being handled in a React component without client-side tokenization (documented PCI-DSS gap).
Decision
We will extract the checkout into a standalone app with the following architecture:
- Framework: Hono v4 — an ultralight (~12KB) TypeScript framework built for Cloudflare Workers, with first-class JSX SSR support.
- Deploy target: Cloudflare Workers on the
pay.<root-domain>subdomain. - Client interactivity: HTMX 2 for async HTML fragment swapping (create checkout, poll payment status), Alpine.js 3 for client-side reactivity (credit card form formatting, visual card preview).
- Styling: Tailwind CSS v4 (already in the monorepo).
- Auth: JWT token passed via cookie (preferred) or query param (fallback) from the main SPA — the Worker validates it and forwards it to Django API calls.
- API: The Worker acts as an SSR proxy, calling the Django backend's REST API internally and returning HTML fragments to the browser.
Key design properties
| Property | Decision |
|---|---|
| App size | ~30KB JS total (HTMX + Alpine CDN), zero framework JS bundle |
| Time-to-interactive | Instant — HTML is server-rendered on the edge |
| Auth model | JWT forwarded via Authorization: Bearer to Django |
| Payment methods | Pix (QR Code), Boleto, Credit Card via Asaas |
| Polling | HTMX hx-trigger="every 3s" for payment status |
| Dev mode | Vite dev server at localhost:3001 |
| Production | wrangler deploy → pay.acme.com/* |
Considered Options
Option 1: Keep inside TanStack Start SPA
- Pro: Same stack, existing auth flow, no new deploy target.
- Con: Heavy bundle for a form page, no isolation, shared deployment risk.
Option 2: Separate TanStack Start app
- Pro: Same React stack, isolated deploy.
- Con: Still carries React 19 + Router + Query (~120KB+) for a ~520-line page.
Option 3: Django Templates + HTMX + Alpine (SSR from Django)
- Pro: Already in the project for public pages, auth via session cookie, zero new infra.
- Con: Not on the edge — rendered in the Django region, not globally distributed.
- Trade-off: Acceptable for a Brazilian-market app (Asaas is Brazil-only), but edge deployment gives faster first paint globally.
Option 4: Hono SSR + HTMX + Alpine (chosen)
- Pro: ~12KB framework, edge-native, JSX SSR, same monorepo, deploys independently.
- Con: New tech in the monorepo, JWT auth forwarding needed between apps.
Consequences
- Positive: The main SPA bundle shrinks by ~520 lines (checkout page removed). The checkout app can be deployed independently, scaled independently, and maintained without touching the main app.
- Positive: The edge-deployed SSR gives the fastest possible first paint for the payment page.
- Positive: HTMX + Alpine replaces React for this page — simpler mental model, less JS.
- Negative: Auth token must be forwarded from SPA to checkout app (cookie or query param). Adds a small handshake complexity.
- Negative: Credit card expiry field still lacks proper
nameattribute and combined-field parsing (documented, deferred to next iteration). - Negative: New deploy target (
pay.acme.com) needs DNS and Cloudflare Workers route configuration.
Related
frontend/apps/checkout/— the app workspace.apps/payments/views.py— Django backend endpoints.apps/payments/gateways/base.py— Payment gateway abstraction.