Skip to content

One monorepo, many insurance brands

A monorepo platform for an insurance client with a themeable UI library, a composable form system, and enough repo-wide consistency that non-developers could generate new application flows with an AI agent.

The client was an insurance company running several brands under one umbrella. Each brand had its own corporate identity, but they wanted the same thing behind all of them: custom-built frontends, on a limited budget, as a long-term solution rather than a series of one-off builds.

Looking at the brands together, the requirement set was effectively identical for each one:

  • a public marketing site with product pages and FAQ,
  • application forms (the core of buying an insurance product),
  • damage-claim forms,
  • a customer portal.

And crucially, every brand’s frontend talks to the same backend for everything that matters — authentication, form submissions, contract data. The brands differ in look and content; they do not differ in what the platform underneath has to do.

When I came to it, the codebase was a Ruby on Rails backend paired with a Nuxt 2 frontend — for a single brand. Two problems made that a dead end:

  1. Nuxt 2 was end-of-life. A migration to Nuxt 3 was expensive no matter what. And company-wide steering was pushing toward Next.js frontends where possible, so in-house developers could move between projects without re-learning a stack each time. Paying the migration cost to land on the framework we were moving away from made no sense.
  2. Coordinated deployments. Whenever a change touched the backend and the frontend together — say, a new mandatory input field on an application form — we had to orchestrate the two deployments by hand. Every cross-cutting change carried that release-coordination tax.

I designed and implemented a monorepo migration of the whole codebase. The Nuxt 2 frontend was rebuilt as a modern Next.js app, and the Rails backend moved into the same repository, so a single change that spans both ships as one coherent unit.

The layout separates products from shared machinery:

apps/ # one Next.js frontend per brand
packages/
api/ # shared abstraction over the GraphQL backend
auth/ # shared authentication layer
ui/ # the component library (themeable, a11y-tested)
forms/ # the composable form system
  • apps/ holds the brand frontends. A brand is a thin application: its identity and content, composed out of shared building blocks.
  • packages/api and packages/auth are the shared abstraction layers over the GraphQL backend. Every brand authenticates and reads/writes contract data through the same well-defined seam, instead of each app re-implementing it.
  • packages/ui is the custom component library — one battle-tested set of components that every app draws from.
  • packages/forms is where the real leverage lives (below).

The payoff of the structure: repo-wide consistency. Every app uses the same components, the same form mechanics, and the same backend abstractions. Fixing or improving something in a shared package improves it everywhere at once.

Insurance is forms — long, multi-step, stateful ones. So packages/forms was the piece that most needed to be right. It’s built on react-hook-form and owns the form-state logic that every brand would otherwise re-implement:

  • multi-step flows with validation at each step,
  • session-storage persistence, so a user who leaves mid-application can come back and continue from the data they’d already filled in.

The design goal was a Lego-like form library: assemble a new application or damage-claim form by composition rather than by writing it from scratch — “code as config.” A new insurance product’s flow becomes a description of which blocks go where, not a bespoke implementation.

Each brand looks like its own product while sharing one component set. ui achieves that with semantic design tokens and Tailwind’s @theme directive: components are written against meaning (--color-surface, --color-accent, and so on), and each app supplies the token values for its brand. The seam between “the component” and “how this brand looks” is explicit, which is what keeps a single library from turning into a pile of per-brand overrides.

Accessibility is part of “well-tested,” not an afterthought: the components carry automated accessibility checks in CI (WCAG 2.2 AA), so a regression that breaks keyboard access or contrast fails the build rather than reaching a brand’s live site.

Why it became a great fit for agentic coding

Section titled “Why it became a great fit for agentic coding”

Because the repo was so consistent — same components, same form primitives, same backend seams, same conventions everywhere — it turned out to be an excellent substrate for agentic coding.

We wrote custom Claude skills for generating new application flows. Given a proper product spec, the agent could produce a new form end to end — including Playwright tests and visual-regression tests — because there was exactly one right way to do each part and it was encoded in the repo.

The consequence: someone with only rudimentary technical proficiency could drive it. A new insurance product’s application flow could be kicked off by our product manager from the product spec and then reviewed by a developer, instead of consuming a developer’s implementation time from the start.

Two concrete before/after numbers, measured against how the same work went before the monorepo:

  • A whole new brand — a complete new frontend application — went from months of development effort to roughly a week.
  • A new product — new application and damage-claim forms on an existing brand — went from multiple weeks of implementation to one or two days.

And the coordinated-deployment tax is gone: a change that spans backend and frontend now ships as a single unit out of a single repository.

None of this was free, and the honest version of the story is that every kind of sharing came with a matching cost. Three were worth accepting on purpose:

  • A shared look means a bounded design space. To an untrained eye the brands look genuinely different — type, color, sharp vs. rounded edges, imagery, and content carry most of the perceived identity. But a designer only gets so much room before a request would mean forking a component instead of theming it. That was a deliberate trade: the client got custom-designed brand pages at a fraction of the cost of one-off builds, in exchange for living inside the boundaries of a shared system.
  • “Fix once, fixed everywhere” is also “break once, break everywhere.” A change to a shared package touches every brand at once, so meticulous test coverage and visual-regression testing weren’t optional — they’re the thing that makes a fix in one app safe for the rest. When an issue surfaces in one brand and I change a shared component, the regression suite is what confirms that every other brand’s resulting change is intentional rather than collateral damage.
  • The composable form system pays off on the common case and pushes back on the unusual one. When a product needs something the Lego set doesn’t have, there’s a fork in the road: extend the system (slower, but every brand gains the capability) or reach for a one-off escape hatch (fast, but it chips away at the consistency the whole platform — and the agent workflow — depends on). Keeping the “one right way” intact is an ongoing discipline, not a one-time win.

The wins didn’t come from any one clever component. They came from finding the shared shape underneath superficially different brands, then putting the seams in the right places — a backend abstraction, a themeable component set, and a composable form system — so that the expensive, repeated work became cheap and repeatable. The agentic-coding payoff was a consequence of that consistency, not a separate initiative: a codebase disciplined enough for a person to move around quickly is, not coincidentally, disciplined enough for an agent to.