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 atfrontend/apps/checkout/the ADR described). - It talked to a second, independent payment backend —
_internal/apps/checkout(Hono, own Neon DB, ownproducts/orders/payments/subscriptionstables, own Asaas integration) — duplicatingapps.payments(Django, Asaas gateway,Paywall/Paymentmodels) 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 theMembershipthat actually grants access. That endpoint was never implemented on the Django side — onlywebhook/asaas/andwebhook/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 viaAsaasGateway,FakeGatewayfallback 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) infrontend/apps/web, using the already-generatedapps.paymentsOrval hooks — no new backend surface needed. frontend/apps/web/src/routes/courses/$courseId.tsxand.../$courseId/$lessonId.tsx's paywall CTAs open the dialog instead of redirecting topay.<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>.comto 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_dataonCheckoutRequest), 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.
Related
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.