Skip to content

Checkout moved back in-app, standalone service removed

Reverts ADR 0008. The standalone checkout stack — a SvelteKit app at checkout/ deployed to Cloudflare Workers plus a separate _internal/apps/checkout Hono microservice with its own Neon Postgres DB — is removed. Checkout is now an in-app dialog in the TanStack SPA (frontend/apps/web/src/components/courses/CheckoutDialog.tsx) calling the existing Django apps.payments API directly.

Context

What actually got built diverged from ADR 0008's plan and never fully worked end to end:

  • The implementation shipped as a SvelteKit app at repo-root checkout/ (not the Hono+HTMX+Alpine app at frontend/apps/checkout/ the ADR described).
  • It talked to a second, independent payment backend_internal/apps/checkout (Hono, own Neon DB, own products/orders/payments/subscriptions tables, own Asaas integration) — duplicating apps.payments (Django, Asaas gateway, Paywall/Payment models) almost feature-for-feature.
  • The two systems only talked to each other one-way, via a Trigger.dev task (enroll-customer.ts) POSTing to ${DJANGO_API_URL}/webhook/enrollment/ after a completed order, to create the Membership that actually grants access. That endpoint was never implemented on the Django side — only webhook/asaas/ and webhook/access-group/ exist. So a real payment completed through this flow never enrolled the buyer; access-granting was silently broken.
  • apps.payments (Django) already has a complete, working checkout surface — PaywallInfoView, CheckoutView (Pix/Boleto/Card via AsaasGateway, FakeGateway fallback for dev), PaymentConfirmView, FakePayView, AsaasWebhookView — and Orval had already generated typed hooks for all of it (useApiV1CheckoutCreate2, useApiV1PaymentsRetrieve, useApiV1PaymentsFakePayCreate, useApiV1PaywallsRetrieve). The SPA just wasn't using them — getCheckoutRedirectUrl (checkout.functions.ts) instead redirected the browser out to the external checkout app.

Decision

  • Delete checkout/ (SvelteKit app) and _internal/apps/checkout (Hono microservice + Neon schema) entirely.
  • Build the checkout UI as an in-app dialog (CheckoutDialog.tsx) in frontend/apps/web, using the already-generated apps.payments Orval hooks — no new backend surface needed.
  • frontend/apps/web/src/routes/courses/$courseId.tsx and .../$courseId/$lessonId.tsx's paywall CTAs open the dialog instead of redirecting to pay.<domain>.com.

Consequences

  • Positive: One checkout implementation, one payment ledger (apps.payments.models.Payment), one gateway integration (apps.payments.gateways.AsaasGateway). No more silently-broken cross-service enrollment webhook.
  • Positive: No second Neon Postgres project, no Trigger.dev deploy, no separate Cloudflare Workers route/DNS for pay.<domain>.com to maintain.
  • Negative: Checkout UI now ships inside the main SPA bundle again — the bundle-size motivation from ADR 0008 is back, unaddressed. Accepted: correctness (enrollment actually working) matters more than a few KB, and the dialog is small (no client-side card tokenization was ever added in the standalone app either, so there's no PCI capability lost).
  • Negative: The card payment form still posts raw card fields to the backend (card_data on CheckoutRequest), same PCI-DSS gap ADR 0008 already flagged and never actually fixed. Unresolved — should be replaced with Asaas's client-side tokenization before real card volume goes through this path.
  • frontend/apps/web/src/components/courses/CheckoutDialog.tsx — the in-app checkout UI.
  • backend/apps/payments/views.py, backend/apps/payments/gateways/base.py — the (only, now) checkout backend.
  • ADR 0008 — the decision this reverts.

Strum — Documentação.