Architecture
Preserve the route, domain, contract, registry, and adapter boundary that keeps the product modular.
The central invariant
Product behavior should remain stable when infrastructure changes:
Page or API route
↓
Domain operation
↓
Stable provider contract
↓
Provider registry
↓
Vendor adapter
↓
Cloudflare binding or third-party APIRoutes own HTTP concerns, validation, session checks, and response shape. Domain modules own business rules and persistence. Vendor adapters own external protocols.
Runtime
TanStack Start provides file routing, SSR, server handlers, and bundling. Vite compiles the client and Worker server. @cloudflare/vite-plugin packages the Cloudflare deployment.
The production Worker can bind D1, R2, KV, Cloudflare Email, and Workers AI in one deployment.
Domain-first routes
A route should ask for a business action:
await sendEmail({
to: user.email,
template: 'verifyEmail',
context: { name: user.name, url },
})It should not construct an SES request, parse a Stripe webhook, or address an R2 object directly.
Persistence boundary
Drizzle owns the D1 schema. Domain modules expose operations such as createApiKey, saveUserFile, getActiveEntitlement, or joinWaitlist. React components and API routes do not duplicate SQL.
External object bytes require explicit lifecycle handling. For example, account deletion removes R2 objects before relational cascades remove D1 metadata.
Provider boundary
Each provider-backed module has four parts:
- an application-facing TypeScript interface;
- one or more small adapters;
- a registry mapping configuration to an adapter;
- domain operations that hide the registry from call sites.
See Add a provider for the extension workflow.
Acceptance boundary
An adapter is not complete because it compiles. The corresponding user workflow must work: verify an email, complete checkout, manage billing, upload and retrieve a file, revoke a key, or send an administrator notification.