Skip to content

Chapter 34: What Would We Build If We Started Now?

We’ve reached the question the first half of the book was designed to earn.

If we were starting over today, with the browser we have now, what would we build?

Not what would we have built when browsers were inconsistent and underpowered. Not what would we have built when every interaction meant a full reload. Not what would we have built when JavaScript lacked modules, CSS lacked variables, and native controls couldn’t be styled. Today, with the platform Part II walked through — semantic HTML, the DOM as a context tree, attributes as protocol, events as native communication, forms as transactions, storage as state, server-owned navigation, CSS as a runtime, animation handled by the browser, accessibility and internationalization as platform commitments, the decoration-versus-replacement principle as our compass.

The answer isn’t no framework. The browser still doesn’t provide a complete product architecture. It gives us strong primitives, but not the coordination layer a real application needs. Analytics has to attach somewhere. Audit logging has to happen somewhere. Permissions have to be checked somewhere. Notifications have to be shown somewhere. The application’s capabilities — the things that aren’t rendering — need a home, and the platform doesn’t provide one.

So the question becomes more precise:

What’s the smallest architecture we can add on top of the modern browser to make applications modular, accessible, inspectable, and capable?

Part III answers that question by naming the principles. Part IV builds the answer by hand. Parts V and VI ship the answer as web components and as Kitsune, the maintained reference implementation. This chapter opens Part III by sketching the shape.

A tempting answer to every frontend problem is another renderer.

The temptation is understandable. Renderers are where the field has spent the past fifteen years of design attention. React’s render-from-state model. Vue’s template syntax. Svelte’s compile-time approach. Solid’s fine-grained reactivity. Lit’s declarative templates. Each new generation of framework starts by deciding how UI is produced and builds the rest of its architecture around that decision.

But rendering isn’t the only missing piece. It’s not even the most important one.

The browser already renders HTML. Server frameworks (Rails, Django, Phoenix, Astro, plain HTML) can render documents. Lit can author custom elements that render themselves. React, Vue, Svelte, and Solid can render component trees. The rendering question is mostly settled — pick the renderer that fits your application’s needs, follow the decoration-versus-replacement guidance from Chapter 33, and move on.

The persistent missing layer is coordination.

Where do product capabilities live? How does analytics observe an action without being imported into a button? How does a storage module save drafts without every form knowing about IndexedDB? How does a permission system affect actions without being scattered through table rows? How does observability see meaningful user flow rather than raw clicks? How does the application explain why something happened, given that the user pressed a button five seconds ago?

These aren’t primarily rendering problems. They’re application architecture problems. And the dominant frontend frameworks address them indirectly — by encouraging every capability to express itself in the framework’s vocabulary (hooks, providers, contexts, higher-order components) and then live inside the render tree. Chapter 13 named this the component trap. The trap exists because the framework’s center of gravity is rendering, and there’s no obvious second center to put the rest of the application’s concerns.

The architecture this book proposes has a second center. It’s small, but it’s distinct from the renderer, and the separation is what makes the rest of the design work.

Starting from the modern browser, the missing layer looks like this:

An application shell — the root element that hosts the runtime and gives the application a place to live in the DOM.

A runtime — a small coordination kernel that routes events between sources and observers, dispatches commands to handlers, manages module lifecycles, and exposes diagnostic information.

Modules — units of capability that install into the runtime. An analytics module. An audit module. A storage module. A permission module. A notification module. Each one observes events, handles commands, or exposes services to other modules.

Boundaries — semantic regions of the DOM that supply context to events fired inside them. A surface boundary names what part of the application the user is in. A feature boundary names which product capability the region belongs to. An entity boundary names what the region is about. The boundary’s context flows down through containment, the way CSS custom properties and accessibility roles already do.

Events — facts about what happened, fired from components and observed by modules. Bubble through the DOM. Get enriched with boundary context. Reach the runtime as application-level events with full context attached.

Commands — requests for things to happen, dispatched from components and handled by registered handlers. The CQRS-flavored split from Chapter 23 between something happened (events) and something should happen (commands).

Providers — stable services modules can inject and use. The user’s identity. The current locale. The active theme. The router’s current state. Anything that’s a thing the application has rather than a thing the application does.

Diagnostics — a way to make the runtime’s behavior visible during development. Which events fired. Which modules observed them. Which commands dispatched. How long each handler took. The diagnostic surface is what makes the rest of the architecture debuggable.

Web-native components — built on custom elements, with the decoration-vs-replacement principle applied, participating in the metadata protocol, contributing to forms when appropriate, accessible by default.

Framework adapters — small bridges that let the runtime coexist with React, Vue, Astro, server-rendered HTML, and the other rendering systems the team might already use. The architecture doesn’t ask teams to throw out their renderer; it adds a coordination layer that works with whatever’s underneath.

This is the missing layer. The chapter’s central claim is that it’s small enough to be tractable. Not a giant framework. Not a runtime that takes over the application. A thin coordination kernel and a small set of conventions, sitting between the browser’s primitives and the renderer the team is using.

The rest of Parts III–VI develops each piece. Part III names the principles each piece embodies. Part IV builds them by hand. Part V wraps them in web components. Part VI ships them as Kitsune. The thing this chapter is doing is making the shape visible.

The principle worth landing carefully is browser first, not browser only.

Browser-first doesn’t mean refusing tools. It means the browser’s primitives set the foundation. HTML remains semantic. CSS remains adaptive. Events remain native communication. Forms remain transaction boundaries. Custom elements remain portable components. Accessibility remains a platform contract. The added architecture doesn’t replace these. It organizes around them.

The decoration-versus-replacement principle from Chapter 33 applies here too. The missing layer should decorate the platform with an opinion, not replace the platform with a parallel reality. A boundary is a real DOM element with attributes. A custom element is a real custom element. An event is a real DOM event with composed bubbling. A storage operation is a real storage write with a real storage event firing for cross-tab synchronization. The runtime adds coordination on top of these primitives without hiding what’s underneath.

This is the architectural difference between the proposed missing layer and the SPA frameworks Part I described. A React application’s render tree is the application’s center of gravity; everything else is wired into the render tree. A platform-first application’s platform is the center of gravity; the renderer is one consumer of platform events, and the missing-layer architecture is another. Both consume platform primitives. Neither is the application’s substrate.

That positioning has architectural consequences. The platform-first application can swap renderers without rewriting the rest of the architecture (the modules still work; the boundaries still work; the events still bubble). The platform-first application can mix renderers — server-rendered shell, Lit components in the interactive parts, React widgets where they make sense, AI-generated markup where it shows up — and the missing-layer coordination still works because it operates on the platform’s substrate, not on a specific renderer’s tree.

This is one of the longer-tail reasons the platform-first approach matters for the AI generation specifically. The next generation of UIs won’t be hand-authored in a single framework’s idioms. They’ll be a mix of generated markup, server-rendered components, framework-rendered components, and platform-native elements. The coordination layer needs to work across all of these. Operating on the platform’s substrate is the only way that’s tractable.

The chapter should be honest about what the missing layer doesn’t address.

It doesn’t pick a renderer. Teams still have to decide whether to ship Lit, React, Vue, server-rendered HTML, or some combination. The missing layer integrates with each; it doesn’t substitute for any of them.

It doesn’t solve data fetching. The application still needs a way to talk to a server, manage cache invalidation, handle authentication, and recover from network failures. Part IV introduces a repository pattern for data work; that pattern earns its place by being small and platform-aligned, not by pretending data fetching is trivial.

It doesn’t replace careful design. Accessibility (Ch 29), internationalization (Ch 30), and security (Ch 31) are still the application’s responsibility. The architecture preserves the platform’s contracts; it doesn’t enforce them. A team that uses the architecture badly can still ship inaccessible applications.

It doesn’t solve every coordination problem. Some applications genuinely need the kind of tight integration React provides — Figma, Linear, real-time collaborative tools, complex creative editors. For those applications, the missing-layer approach is a worse fit than a full framework. Chapter 33’s altitude argument applies: the missing layer is the right altitude for some applications and the wrong altitude for others.

For most applications, the missing layer is enough. The chapter’s bet is that most is a substantial fraction — content sites, marketing applications, internal tools, admin dashboards, e-commerce, social features, documentation, news, content management, government services, banking interfaces (much of which is form-and-table work), educational tools, and most of what teams actually ship. For these, the missing layer plus the platform plus a small amount of renderer-specific work is a better architectural fit than the framework-everywhere default.

The rest of Part III names the architectural principles the missing layer embodies. Five chapters, each developing one principle:

Chapter 35 — From Components to Capabilities. Why component modularity isn’t enough. The introduction of capabilities as a separate axis of modularity, distinct from the component tree.

Chapter 36 — Boundaries as Application Geography. Bounded contexts borrowed from domain-driven design. Why the DOM is the right place to mark architectural boundaries. The surface/feature/entity vocabulary.

Chapter 37 — Events, Commands, and Causality. The CQRS-flavored split between facts (events) and requests (commands). Why the split keeps the application’s causality legible.

Chapter 38 — The Browser-Native Application Loop. The closed loop: user → component → boundary → runtime → modules → state → UI. The architecture in one diagram.

After Part III, the architecture’s principles are named. Part IV builds them by hand to make the moving parts visible. Part V wraps them in web components. Part VI ships them as Kitsune. The whole thing accumulates from these five chapters.

Three things to carry from this chapter into the rest of Part III.

The platform is the substrate. The architecture is layered on top of the browser’s primitives, not in parallel with them. Every architectural piece — modules, boundaries, events, commands — uses platform mechanisms underneath.

The missing layer is small. The whole runtime, in Part IV’s hand-built implementation, will be under a thousand lines of TypeScript. The architecture is a discipline more than a heavy library. The cost of adopting it is low; the cost of maintaining it over time is also low, because most of what it depends on is the platform itself.

Capabilities are first-class. The component tree is one axis of modularity. The capabilities — analytics, audit, storage, notifications, permissions, observability — are another. The architecture treats them as separate axes that compose at runtime, rather than collapsing them into the render tree.

The chapter that follows next picks up the capability thread directly and makes the case that this is one of the missing layer’s most consequential ideas.

Take a small application you’ve worked on or know well. A settings page. A user profile editor. An admin dashboard.

Sketch the architecture in two ways.

The first sketch: components only. What components does the application have? How do they pass props? Where do hooks live? What’s the dependency graph among them? Where does analytics live? Audit logging? Storage? Permission checks? Where does each capability tangle into the component tree?

The second sketch: components plus capabilities. Identify the same application’s architecture, but split the capabilities (analytics, audit, storage, permissions, notifications) into a separate layer. Draw the capabilities as modules attached to a runtime. Draw the components as emitters of events and dispatchers of commands. Where does each capability live now? How does it relate to the components?

Then answer:

  1. Which sketch is simpler to draw?
  2. Which sketch is simpler to extend (add a new capability, add a new component)?
  3. Which sketch makes the application’s causality more legible?
  4. Which sketch survives a renderer change better?
  5. Which sketch does a junior developer understand more quickly?

There’s no objectively right answer. The exercise’s point is to feel the architectural difference between everything in the component tree and capabilities as a separate layer. The rest of Part III develops the second approach. The exercise gives you the picture to argue with as you read.