Skip to content

Checkout as a standalone Hono SSR app on the edge

Status: Superseded by ADR 0013. The standalone checkout app and its _internal/apps/checkout microservice were removed; checkout is now handled in-app by the TanStack SPA calling apps.payments directly. 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

PropertyDecision
App size~30KB JS total (HTMX + Alpine CDN), zero framework JS bundle
Time-to-interactiveInstant — HTML is server-rendered on the edge
Auth modelJWT forwarded via Authorization: Bearer to Django
Payment methodsPix (QR Code), Boleto, Credit Card via Asaas
PollingHTMX hx-trigger="every 3s" for payment status
Dev modeVite dev server at localhost:3001
Productionwrangler deploypay.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 name attribute and combined-field parsing (documented, deferred to next iteration).
  • Negative: New deploy target (pay.acme.com) needs DNS and Cloudflare Workers route configuration.
  • frontend/apps/checkout/ — the app workspace.
  • apps/payments/views.py — Django backend endpoints.
  • apps/payments/gateways/base.py — Payment gateway abstraction.

Strum — Documentação.