Essay
How to Build Durable Web Apps
March 12, 2026
Build web apps from durable primitives, with clear boundaries, strong contracts, and operationally safe defaults.
Principles
Build durable primitives
A strong product is a small set of primitives that compose safely.
Apply it by:
- making each primitive single-purpose
- keeping inputs, outputs, and failure modes explicit
- designing defaults so the safe path is the easy path
- exposing enough state to inspect and debug behavior
Example: “Create order,” “capture payment,” and “send receipt” are separate primitives; the UI composes them instead of hiding all three inside one page component.
Design API-first boundaries
If a capability matters, it must exist behind a stable application interface, not only inside the UI.
Apply it by:
- putting business logic behind explicit application APIs
- keeping the UI as a client of those APIs, not the source of truth
- making critical workflows scriptable and automatable
- using clear operation names, permissions, and error responses
Example: If support staff need to refund an order, there should be a POST /refunds operation — not a hidden admin button that contains the only refund logic.
Use contracts for both types and runtime validation
Static types catch internal mistakes. Runtime validation protects trust boundaries. Durable systems use one contract model for both.
Apply it by:
- defining contracts once and reusing them across layers
- validating requests, responses, events, and job payloads at runtime
- making illegal states hard or impossible to represent
- inferring persisted row types from the
Drizzle schemainstead of hand-maintaining parallel types
Example: The same CreateOrder contract types the form payload in code and validates the incoming HTTP request at runtime before any write happens.
Rules
Keep one source of truth
Canonical state should live in one place. Caches, indexes, and read models are derived views.
Apply it by:
- rebuilding derived views instead of letting them become competing truth sources
- deleting duplicate ownership when you find it
- keeping the core relational model normalized, constrained, and easy to reason about
Example: Product availability lives in Postgres; Redis and search indexes are rebuilt from it, not edited separately.
Enforce strict boundaries
Separate presentation, application orchestration, domain logic, persistence, and external integrations.
Apply it by:
- keeping handlers and components thin
- putting use-case coordination in the application layer
- keeping domain rules out of transport and ORM code
- isolating third-party integration code behind explicit adapters
Example: The route handler authenticates and parses input, the application service places the order, and the payment provider code lives behind a payment adapter.
Commit core writes synchronously; push side effects async
A user action that changes important state should commit transactionally before success is reported. Side effects should happen after.
Apply it by:
- finishing the core write before reporting success
- pushing email, indexing, analytics, webhooks, and cache invalidation to jobs or events
- defaulting to strong consistency for core workflows
Example: Mark the order as paid in the transaction first; send the receipt email and update analytics in background jobs after commit.
Make retries safe
Any operation that may run more than once must be safe to run more than once.
Apply it by:
- using idempotency keys for important mutations
- designing jobs and callbacks to tolerate retries
- recording enough state to detect duplicate work
Example: If a payment webhook is delivered twice, the second delivery should detect the payment was already recorded and do nothing.
Make important writes observable and auditable
If a write matters, you should be able to see it and explain it later.
Apply it by:
- recording who acted, what changed, and when it changed
- recording why it changed when relevant
- emitting structured logs, traces, metrics, and audit records for critical paths
Example: For a subscription cancellation, record who triggered it, the previous and new status, the timestamp, and the request or job that caused it.
Test real boundaries
Focus tests on what must not break at the seams of the system.
Apply it by:
- writing unit tests around domain rules
- writing integration tests around database and external boundaries
- writing a small number of end-to-end tests for critical paths
- testing authorization, contracts, and idempotency explicitly
- preferring real boundaries over mock-heavy tests
Example: Test that an unauthorized user gets 403, an invalid payload gets 400, and a valid request writes the expected row and audit record.
Defaults
Architecture
- Modular monolith first — Default to one deployable app with internal module boundaries.
- Postgres is the primary source of truth — Canonical state lives there; everything else is derived.
- Handlers are thin; application code owns workflows — Transport parses/auths, application code orchestrates and transacts.
- Core writes are synchronous; side effects are async — Commit important state first, then fan out.
- Server-first UI — Keep business logic off the client.
- Public REST, internal typed RPC — Use the interface that fits the boundary.
Tech choices
- Monorepo with Turborepo, scaffolded by better-t-stack — Keep contracts, apps, packages, and tooling in one repo with consistent workflows.
- TypeScript — Standardize the codebase on one strongly typed language.
- Drizzle — Define relational schema in code and infer persisted types from it.
- Better Auth — Use a simple default auth system with clear session and API-key handling.
- oRPC internally — Preserve end-to-end type flow inside the product boundary.
- REST publicly — Expose durable machine-usable APIs with stable contracts.
- Postgres on Neon — Default to a strong relational primary store.
- TanStack Start for applications — Prefer a server-first full-stack web framework for product apps.
- Astro for simpler or content-heavy frontends — Use it when content and static delivery matter more than app complexity.
- Tailwind CSS with coss.com/ui — Use a fast default styling and component baseline.
- Alchemy for provisioning — Keep infrastructure definition consistent and automated.
- Cloudflare Workers, Queues, and R2 — Prefer managed primitives for compute, async work, and object storage.
- GCP Cloud Run only when needed — Move there when full Node.js or container runtime requirements are real.
- One authoritative quality gate — Run lint, types, tests, formatting, and architecture checks through a single full check command.