A Framework Is a Bundle of Claims

An attempt to define the best possible web application framework from first principles — eight problems derived from the definition of a web app, nine design decisions each paired with the evidence that would overturn it, and seven gates that almost nobody ships.

webfrontendframeworksarchitectureresearchessay

Asking the question properly

“What is the best web framework?” is usually a proxy war. The answer people give is the framework they already use, and the argument is a defense of sunk costs. I wanted to try the opposite: define the best possible web application framework before checking whether anything existing matches it, and without caring whether the answer flatters my own stack.

To do that, the word “best” has to be taken apart first, because it bundles several questions:

  1. The ceiling — the best app you can possibly build with it (latency, offline, real-time).
  2. The floor — what an average team gets even when they don’t try (correctness, security, speed by default).
  3. Checkability — of all the claims the framework makes, how many are verified by a machine.

The third one is the spine of this whole essay, so let me state it as a definition:

A framework is a bundle of claims about your application. The best framework is the one that makes the strongest claims and ships each claim paired with the gate that checks it.

Almost every claim made by the current generation is a declaration, not a check. “The cache is invalidated by revalidateTag” — forget the tag and the stale read returns silently, forever. “The server and the client render the same tree” — React warns in development and silently repairs in production. The oracle exists; the check was never built.

What a web application is

Definitions are productive when you enumerate from them, so here is mine:

A web application is a program whose UI runs in an environment the author does not control (the browser), whose state of record lives in an environment the author does control, with an unreliable network in between, used by many principals at once, and rebuilt while it is being used.

Each clause of that sentence yields exactly one problem. This is the part I refuse to shortcut: if you start from the feature lists of existing frameworks instead, the problems nobody answers never enter your field of view.

# Problem From which clause
P-1 State and its copies “state of record” — coherence and invalidation of every cache and replica
P-2 The boundary “unreliable network” — latency, partial failure, hostile input, version skew between the two ends
P-3 Principals “many principals” — every effect executes as someone
P-4 Rendering and updates “UI runs” — first paint, update granularity, streaming
P-5 Delivery “an environment the author does not control” — the code itself arrives over the network, every visit, and sometimes only partially
P-6 Evolution “rebuilt while it is being used” — schema migration, rolling deploys, version skew, the installed base
P-7 Time and concurrency “at once” — optimistic updates, conflicts, real-time
P-8 Checkability (meta) whether the answers to P-1..P-7 are claims or checks

Reading 2026 as a set of answers

Every framework family is an answer sheet for those eight problems, usually with most boxes blank:

Family Best answer it gives What it leaves blank
Next.js (RSC) P-4 streaming is state of the art; half of P-1 P-2 (the boundary wears a function costume), P-3 (ambient auth), P-6, P-8
React Router / Remix P-5 (progressive enhancement as philosophy), explicit P-2 (loader/action) P-1, P-6 — the philosophy exists, the check does not
TanStack Start P-2 typing, end to end the types don’t speak of failure or versions — a Remote<T> wearing a T costume
SvelteKit / SolidStart P-4 update granularity (signals) P-1, P-6
Qwik P-4 startup (resumability — never do the work twice) ecosystem: 672 detected sites on the public web as of July 2026
Astro P-5 (zero JS by default, islands) documents more than applications
HTMX / Hotwire erases P-1 and P-2 by refusing to split P-7 optimistic UI, offline, the latency floor
Phoenix LiveView P-7 (server push, diff streaming) plus not splitting per-connection server state, no offline
Rails / Laravel / Django P-6 data migration is the most mature anywhere; conventions as a floor P-2 and P-4 are a generation old
Convex P-1, P-7 (transactional reactive queries) P-5; the database is part of the framework
Zero / Electric / PowerSync the frontier of P-1 and P-7 (query-driven replication; Zero hit 1.0 in June 2026) P-3 is hardest here; P-6 gets harder with schemas resident on clients
Lamdera the only one answering P-6 with type-checked migrations; P-2 typed in one language scale, ecosystem — irrelevant to its value as precedent
Meteor (history) made the D-3 claim below in 2012 its implementation (oplog tailing) died at scale — the claim wasn’t wrong, it shipped without the scale check

Three problems are answered by almost nobody:

P-6, version skew. The moment you deploy, clients holding the old HTML call the new server. This happens to every app on every deploy, and the boundary has no version in it. The only real answer today is Vercel’s Skew Protection — an infrastructure feature on paid plans, not part of any framework’s semantics. Lamdera alone answers the data half: change a persisted type and the compiler demands a migration function.

P-3, principals. Authorization lives ambiently in middleware and context objects — a property of the route, when it needs to be a property of the effect. CVE-2025-29927 let a single header skip the auth layer entirely; CVE-2025-66478, a CVSS 10.0 deserialization RCE in the RSC wire protocol, is what it looks like when a boundary is not treated as a parser of hostile input.

P-8, checks. Server/client render agreement, invalidation soundness, authorization coverage — every one of these has an available oracle, and none of them ships as a gate.

Nine decisions

Here is the actual definition — nine decisions, each with the reasoning and, importantly, with the evidence that would overturn it. A definition that can’t say what would falsify it is a mood, not a definition.

D-1. Placement is a per-feature declaration, not an app-wide choice

Today, where state lives — all on the server (LiveView), split (RSC), resident on the client (local-first) — is decided once, for the whole app, by choosing a framework. But real applications are mixtures: the settings page is fine server-only, the editor needs local-first, the payment flow should be server-only. The best framework spans the spectrum server-only ↔ streamed ↔ interactive ↔ local-first as a per-feature declaration, and code for placements you don’t use never appears in the output. The “don’t split” school is absorbed as a first-class point on the spectrum, not a rival architecture.

Overturned if: placement declarations turn out to be unstable in practice — the same feature wanting different placements depending on runtime context — for a majority of features.

D-2. The boundary is a protocol, not a function

Server functions sell the boundary as a function call. That costume hides four things: partial failure, retries, version difference between the two ends, and hostile input. The boundary must have (1) a name and a version, (2) a schema, with serializability decided by types, (3) failure in its type — a Remote<T>, not a T — with idempotency declared so retry semantics can be derived, and (4) a compatibility gate (G-2 below). Function-call syntax as sugar is fine. Lying types are not.

Overturned if: the ceremony of typed failure demonstrably doesn’t pay for itself in caught failure bugs, in which case the default should flip to implicit boundaries with one uniform retry semantics.

D-3. Reads are subscriptions; invalidation is derived, never hand-written

Cache invalidation by human annotation — revalidateTag, staleTime — is a claim without a check: the forgotten tag returns stale data silently. In the best framework a read is by default a live query: the system derives which queries a write affects and updates them. Queries it cannot derive must be explicitly declared as allowed to go stale. The default flips: staleness becomes the marked case.

Meteor made this exact claim in 2012 and died on oplog tailing. The premises changed: logical replication and CDC are now table stakes on the database side, and query-driven replication (Zero) and transactional reactive queries (Convex) exist in production.

Overturned if: measured on real applications, the derivable fraction of queries is too low — if the escape hatch is the majority, it cannot be the default.

D-4. Principals are never ambient

Every effect that crosses the boundary receives its principal explicitly, as a capability. “An effect reachable without passing an authorization check” should be a compile error — or at minimum something G-3 counts at runtime. CVE-2025-29927 is the canonical form of the failure: put authorization in a layer along the path, and input that bypasses the layer bypasses the authorization with it. Exposure is a property of the effect, not of the route.

Overturned if: the ceremony drives developers to mint one omnipotent principal and pass it everywhere — ambient authority reinvented. (The gate can watch for this too: if every effect sees the same principal, warn.)

D-5. HTML is the substrate; JavaScript is an enhancement

This is not ideology, it is partial-failure engineering — the answer to P-5. The code arrives over the network every visit, and sometimes it doesn’t: bad cellular, corporate proxies, extensions, or just slowness. An app whose forms work as HTML degrades under delivery failure; an app with no fallback fails totally. First paint and the primary write paths complete over HTML + HTTP; JavaScript makes them faster.

Overturned if: the app class has no meaningful degraded form (editors, games, maps) — which D-1 absorbs by letting those features declare enhancement-required.

D-6. Never do the same work twice

Hydration — the client re-executing everything the server already executed, to reconstruct the result it was already sent — and VDOM re-rendering — re-executing a subtree because one value changed — are two faces of one disease: dependencies weren’t tracked, so recomputation papers over the gap. Startup should resume where the server left off; updates should flow through a dependency graph (signals). Recomputation is the exception, not the model.

An honest note: the leading resumability implementation, Qwik, shows 672 sites on the public web. Poor adoption doesn’t falsify a design, but it is evidence that asking humans to maintain serializable boundaries by convention does not work — which is why D-6 without D-9 is a re-run of Qwik.

Overturned if: serialization + resume measurably costs more than re-execution for the dominant shape of apps.

D-7. Evolution is a first-class subject

An application is not “the current code.” It is every version of the code currently alive, plus all existing data. So: (1) schema migrations are type-checked against all existing data — Lamdera is the precedent — (2) the boundary declares a skew window (which client versions must stay alive), and a gate checks every version pair inside the window, and (3) expand/contract two-phase deploys are something the framework knows about, not tribal knowledge.

Overturned by: nothing. This is not a trade-off; it is a description of reality. Implementations can be too expensive; the problem doesn’t go away.

D-8. Gates ship with the framework

Every claim above ships paired with its check (the seven gates below). A gate also detects when a pinned divergence is fixed — otherwise the framework accumulates declarations that are no longer true.

D-9. This cannot be built as a library

The decisions above demand four properties from the language itself: (1) serializability decidable by types — which values may cross the boundary, (2) placement visible as an effect — server-only effects uncallable from client-placed code, (3) deterministic rendering — the precondition for G-1, and (4) schema versions as values — the precondition for D-7. The current generation approximates (1) and (2) with the string "use server" and lint rules. The presence of a string is not the establishment of a property — the CVSS 10.0 boundary CVE is that approximation’s invoice. The best framework is therefore compiler-cooperative: a language, or a typed subset with code generation.

Overturned if: TypeScript’s type system (or a future effects-like feature) becomes able to express (1) and (2) soundly.

The shape that falls out

Restate the nine decisions as one object and you get seven layers:

  1. Compiler — one program split across targets (server / client / edge / worker); placement declared per feature; boundary-crossing values type-checked.
  2. Protocol — the wire format of the boundary: name, version, schema, typed failure, idempotency. Designed as a parser of hostile input.
  3. Data — the state of record plus live queries; invalidation derived; transactions end to end, including rollback of optimistic updates.
  4. Principal — capabilities carrying who into every effect.
  5. Rendering — HTML substrate, resumed startup, dependency-graph updates, streaming as the normal case.
  6. Evolution — checked migrations, declared skew windows, installed-base management.
  7. Verification — the gates. This layer checks the other six.

The seven gates

# Gate What it checks Who ships it today
G-1 Render agreement same input ⇒ server and client produce the same tree; undiagnosable differences are pinned as known divergences, with machine-checked retirement conditions nobody (React: dev warning, silent production repair)
G-2 Boundary compatibility the deploy candidate accepts calls from every client version inside the skew window — the protobuf compatibility gate, applied to the UI boundary nobody (Vercel Skew Protection routes around the problem; routing is not a check)
G-3 Authorization coverage every boundary-crossing effect reaches a principal check — counted at runtime, not grepped for nobody
G-4 Invalidation soundness no path exists where a write is followed by a stale read; derived queries hold by construction, declared-stale reads get poisoned-input tests nobody
G-5 Migration the migration consumes every existing datum, and can be rolled back Lamdera, at the type level only
G-6 Budgets bundle size, interaction latency, accessibility fail CI as numbers bolt-on only (Lighthouse CI etc.)
G-7 No-JS pass forms and primary flows do their job with JavaScript disabled — the check for D-5 nobody

Two design rules for all seven: every gate asserts how many checks ran — “all green” must never be produced by an empty test set — and every gate has a column for “the oracle is wrong,” because without one, the first legitimate divergence gets resolved by deleting the test.

What I don’t know

  • Q-1. Are placement declarations stable? If the same feature wants to be local-first offline and server-backed online, static declaration may be the wrong tool.
  • Q-2. What fraction of real-world queries can invalidation actually be derived for? Nobody has measured this. If escape hatches dominate, D-3 cannot be the default.
  • Q-3. Local-first × authorization. The server may reject a write the client already applied optimistically. “The server can say no” and “the client doesn’t wait” compose into UI semantics nobody has defined in general.
  • Q-4. Who has authority over the skew window? Open tabs live forever. Declaring a window (D-7) still leaves the behavior at its edge — forced reload? degraded mode? — undefined.
  • Q-5. How does the language actually seal nondeterminism (time, randomness, locale, floating point) so that G-1’s oracle is meaningful? One leak fills the divergence list.
  • Q-6. The biggest unchecked claim is this essay’s own spine: that shipping the gates catches defects at a rate types and ordinary tests do not. That is a claim. It stays a claim until someone builds the thing and feeds it poisoned inputs.

How this document fails

Each decision above carries its own falsifier. Two more for the whole:

  • Build the gates, port one real application, run poisoned inputs — and if G-1..G-7 catch nothing beyond what types and existing tests catch, the spine of this essay was wrong for the web.
  • If browsers grow platform-level state sync and version management, or WASM with shared memory changes what the boundary is, the enumeration in P-1..P-8 starts from false premises and must be redone.

I build a programming language as a hobby, and the obvious next question is whether any of this maps onto it. I have deliberately not asked that question here; a definition written to flatter its answer isn’t a definition.

Sources

← Back to Notes