Skip to content

Are We Drifting? — Part 11: The FE↔BE Mirror

Are We Drifting? — Part 11: The FE↔BE Mirror

Section titled “Are We Drifting? — Part 11: The FE↔BE Mirror”

The series has found one shape governing values, models, state, operations, and agents. This part turns it on the largest seam of all — the one between the frontend and the backend — and shows that even the way the two stacks are governed is the same shape, mirrored.

Frontend and backend are usually built by different people, in different languages, with different instincts. Left alone, their governance drifts as surely as their code: the frontend grows a rich culture of lint rules and structural conventions; the backend grows a different one; and an engineer who knows one stack arrives at the other as a stranger. The monorepo becomes two philosophies sharing a git remote.

So the question here is not about a single value or model. It is: do the two halves of the product share one model of what “governed” means — or two?

Both stacks are built on the same unit. Every load-bearing architectural rule is a Governed Sub-System (GSS) — a triple:

  • Structure — the canonical shape (a folder/file/type/proto convention) that names the right answer.
  • Enforcement — the load-bearing leg: a compile check, a lint rule, or a coverage script that fails the build. Without it, the structure is a wish.
  • Documentation — one canonical page.

This is the same source → projection → gate idea, applied to architecture itself: the structure is the declared intent, the enforcement is the gate, the doc is the human-readable projection. Both stacks also share a maturity laddercompile-enforced → lint-error → coverage-script → warn (scoreboard) → convention — so every rule’s real status is legible, not assumed. And both prefer guardrails that are opt-in by structure: the rule self-targets by a convention and stays dormant until you adopt the shape.

Here is the coupling made literal. Each row is one governance concept; the cells are how each stack instantiates it.

Governance conceptFrontendBackend
Layering & responsibilityFeature Layering — Screen / Route / Controller / Flow / RuntimeDomain Layering — handler/repo co-located per domain, no cross-domain imports
Data-source seamController / Data-source seam (controller.{fixtures,local,db})Repo Interface Seam — handler depends on a repo interface, not pgx
Exhaustive outcomesUI State System — matchState over ScreenState (compile + lint)Typed Error Model — connect.NewError codes, no bare fmt.Errorf from a handler
No raw values / single sourceDesign Tokens — semantic tokens, no raw oklch/hexStructured Observability — key-value slog, no string interpolation
Generated indirectionfeature registry — generate-featuresfeatures.generated.ts (--check)Contract Integrity (buf) + generated-interface compile assertion
Canonical test dataFixture Discipline (FE-1) — defineFixtures + registryMigration Discipline — append-only *.sql + real-Postgres repo tests

Read any row left-to-right and the same idea appears in two dialects. “Exhaustively handle every outcome” is matchState on the frontend and a typed error model on the backend. “Never write a raw value; use the single source” is design tokens on the frontend and structured slog on the backend. The concepts are coupled by design — which is what lets someone who has internalized one stack read the other.

A forced mirror would itself be a drift between the docs and reality, so the asymmetries are part of the map:

  • Frontend-only. The rendering guardrails — rich skeletons, motion tokens, design tokens as pixels, the icon catalog — and the Storybook + Playwright workbench have no backend analogue. The backend has no rendering surface and no “agent eyes” to screenshot.
  • Backend-only. Contract Integrity, Migration Discipline, and Middleware Centralization are backend realities (a wire contract, append-only schema, central HTTP middleware) with only a thin frontend echo.
  • Enforcement depth differs, for a real reason. The frontend runs many custom ESLint rules because ESLint is file-granular and syntactic — cheap and precise to extend. The backend leans on buf, the compiler, and a growing wall of CI scripts (check-vocab, check-mocks, check-sqlc, check-sql-scope, check-connect-errors, migration checks) because Go’s equivalents are package-granular and often need type information — closer to a day of work than an hour. So a few backend rules are CI scripts or partly lean on review where the frontend would have a bespoke linter.

The mirror is a design goal, not a straitjacket. Naming the cells keeps the map true to the territory.

The abstract case for governance is easy to nod along to. Here is the concrete one — a single frontend screen, before, with six independent drift modes, each the kind of thing that slips through review:

// BEFORE — six latent drift modes in one file
import { coachRosterFixtures } from './screen.fixtures' // FE-1: fixture imported into a screen
export function CoachRosterScreen({ state, uiState }) { // a parallel uiState prop
return (
<div style={{ background: '#0f0f17' }}> // a raw hex color
{matchState(state, {
loading: () => <div className="lm-skeleton" />, // an anonymous loading bar
ready: (s) => <Rows data={s.data} />,
// missing empty + error arms // a non-exhaustive match
})}
</div>
)
}
// <Suspense fallback={null}> // a blank first paint

Six drift modes. Six named rules. Each one is a lint error or compile failure — not a code-review note:

Drift modeRuleTier
Fixture imported into a screenliftmere/no-fixture-outside-routelint-error
Parallel uiState prop on a screenliftmere/no-uistate-screen-proplint-error
Raw hex color #0f0f17 in component codeliftmere/no-raw-hex-colorlint-error (ambient)
Anonymous skeleton div (no skeleton.tsx)skeleton-composition rulelint-error (opt-in)
Non-exhaustive matchState (missing arms)TypeScript exhaustiveness checkcompile-enforced
<Suspense fallback={null}> in routerliftmere/no-null-suspense-fallbacklint-error

The after version is not “tidier by convention” — it is the only version that compiles and lints. Six coherence checks a reviewer would otherwise have to perform by eye, performed by the build instead.

The backend’s equivalent table:

Drift modeRuleTier
Handler drifts from generated interfacevar _ ServiceHandler = (*Service)(nil)compile-enforced
Proto contract breaks without escalationbuf lintlint-error (CI)
connect.NewError called outside the error adaptercheck-connect-errorsCI script
Generated DB models drift from SQLmake check-sqlcCI script
Generated mocks drift from interfacesmake check-mocksCI script
Vocabulary .gen.go drifts from YAML manifestmake check-vocabCI script
Migration file edited after appliedcheck-migrations.shCI script

That is the whole argument of the series in two tables: the structure plus the gate is what lets a large, AI-generated diff be trusted, because the ways it could drift are the ways the build already refuses — on both stacks.

The check this removes operates at the level of people: the cost of every engineer (and every agent) having to learn two unrelated governance cultures, and the cost of reviewers manually enforcing consistency on both.

A shared model collapses that. Learn the GSS triple and the maturity ladder once, and both stacks are legible. An agent briefed on “structure + enforcement + one doc” applies it on either side. And the matrix itself is governed like everything else — adding a new sub-system on either stack adds a row as its final step, so the cross-stack map cannot silently drift from the rules it describes.

The next parts turn to testing — how fixtures, fakes, and real-Postgres containers keep tests from drifting away from production — then to AI output as an untrusted boundary, and to a running inventory of the wall of gates and where the pattern goes next.