← Back to projects

Multi-tenant core insurance platform for insurers and insurance agencies. A falsifiable prototype on 10 million policies decided the data model before any feature was built.

Role: Architecture Lead (side project) Period: Sep 2026 – present Status: building

TypeScriptNestJSNext.jsPostgreSQLPrismaDocker AI: Claude Code
10,000,000

Test policies in the benchmark: the scale of a large insurer's portfolio

Search speed was proven at millions of policies before any feature was built

Prototype R1 report, 2026-09-22

3,030 MB vs 492 MB (6x less)

Search-index size on disk: alternative design vs chosen design

Six times less storage to keep searches fast, so hosting cost grows slower with the portfolio

Prototype R1 report, 2026-09-22

118 ms vs 1.2 ms

Time to filter policies by two attributes: alternative design vs chosen design

Instant results on the search screen even with millions of policies

Prototype R1 report, 2026-09-22

233 s vs 113 s

Time to make a new attribute searchable: portfolio kept live vs portfolio locked

Adding a new field to a line does not take the system offline; agents keep working

Prototype R1 report, 2026-09-22

Architecture

ARCHITECTURE / REYBER 01 Risk form fields set per line 02 JSONB attributes source of truth 03 Trigger copy scalars to a column 04 Indexed column plain index, 1.2 ms 05 Projection table list attributes 06 Isolation tests RLS, DB and HTTP Chosen by a 10M-policy benchmark: two-attribute filter 118 ms down to 1.2 ms.
REYBER dynamic attributes: JSONB, trigger-kept index column, projection table.

Context

REYBER is a multi-tenant platform that manages the life cycle of insurance business: products, policies, coverage, collections, claims and communication with the insured. It serves two segments with one base: insurers, and insurance agencies that administer policies from many insurers across every line. The promise that makes it hard is multi-line for real: life, health, autos, boats, aircraft, pets and cargo must live in the same engine, with each line’s risk attributes configurable without writing code or migrating the schema.

It is a side project in construction: 25 research and requirements documents, 71 business rules and a build plan of 36 work packages; code started on 22 September 2026.

My role

  • Decided: the architecture, the order of work, and which questions must be answered by measurement before anything is built.
  • Verified: every gate by breaking it on purpose, and every benchmark result against the query plans, not just the clock.
  • Delegated to AI: code and prototype scripts, written by Claude Code under the written protocol, and drafts of the research documents, which I review before they count.

Research

The risky question was the data model for dynamic attributes: how to store a risk whose shape changes by line and still search it fast. I wrote the hypothesis so it could fail: one table with relational columns for the stable parts and a JSONB column for the variable ones, holding one tenant’s portfolio of 10 million policies (13,027,325 insured objects), answering the search-screen filters inside a time budget, with no schema change per new line. The decisions for each outcome were written down before measuring. Four configurations were compared on the same data and the same table: a general GIN index, per-attribute expression indexes, a projection table (one row per attribute value), and generated columns.

The first results were wrong in an instructive way. With row-level security on, which is how the product runs, PostgreSQL will not use an index on a condition whose operator is not marked leakproof, and every JSONB operator is not. Two of the four strategies were not being measured as index strategies at all; they were table scans with indexes sitting unused beside them. The protocol had required measuring under the application’s role precisely because “measuring without RLS is measuring a different product”, so the error showed up before a decision was built on it.

Decisions

DecisionAlternatives rejectedWhy
Hybrid G′+C: attributes stay in JSONB; searchable scalar attributes are copied to a plain column kept by a trigger; list attributes go to a projection tableProjection table for everything (C); generated columns for everything (G); GIN aloneC costs 6x more index (3,030 vs 492 MB), writes 1.7x slower and is 98x worse on a two-attribute filter (118 vs 1.2 ms). G cannot search inside lists, which in insurance are not edge cases (coverages, beneficiaries, additional drivers).
G′ over G for the scalar columnsGenerated columns (G)Adding a searchable attribute to G took 113 s with the portfolio locked. G′ takes 233 s but with the portfolio live: a concurrent reader saw p95 of 3.6 ms against 4.1 ms at rest. The price, a trigger that can drift, is accepted and written down.
CI runs the project’s own Docker ComposeThe services: block of the CI providerA bare database container has no role setup, so CI would connect as a superuser that ignores RLS and the isolation tests would pass while proving nothing.
Same stack as VetNova with five deliberate changes: a job queue, stateless multi-instance API, the official WhatsApp API, observability from day one, CI on every pushThe same stack unchanged; a new stackReuse what ships with its smoke tests; change only what the new non-functional requirements force.

Validation & QA

Isolation is tested twice: a database-level suite and an HTTP-level smoke that proves what the first cannot. Case zero of the isolation suite checks that the test itself can fail; without it, the other cases would pass against a connection that bypasses RLS. Every gate was deliberately broken, on the compiled artifact and never on source, before I trusted it. The data dictionary is generated from the database, and a CI gate fails if it is stale or any column lacks a comment. Screens are compared pair by pair against the approved designs.

Results

Dynamic attributes on 10,000,000 synthetic policies

Two data-model options measured on the same data under row-level security. Lower is better in both panels; each panel has its own scale.

Dynamic attributes on 10,000,000 synthetic policies Two data-model options measured on the same data under row-level security. Lower is better in both panels; each panel has its own scale. Index size C · projection table 3,030 MB C · projection table: 3,030 MB G′ · hybrid 492 MB G′ · hybrid: 492 MB Filter on two attributes C · projection table 118.0 ms C · projection table: 118.0 ms G′ · hybrid 1.2 ms G′ · hybrid: 1.2 ms
Source: REYBER benchmark report · 2026-09-22
Data table
PanelSeriesValue
Index sizeC · projection table3,030 MB
Index sizeG′ · hybrid492 MB
Filter on two attributesC · projection table118.0 ms
Filter on two attributesG′ · hybrid1.2 ms
  • The data model for dynamic attributes is decided on evidence, with the numbers above, before the first feature was written.
  • 38 migrations, 70 tables with a tenant, row-level security enforced on all of them.
  • Round one of the prototype closed on 22 September 2026. The report keeps its own mistakes in it: the leakproof finding, a prefix search that was broken in both candidates, and a result that looked like a win and was not.

What it solves and where else it applies

Insurance agencies sell many lines, and each line describes its risk differently: a car has a plate, a pet has a breed. Building one system per line is slow and expensive. REYBER keeps one engine where each line’s attributes are configured, not programmed, and a measured prototype showed that searching stays fast at 10 million policies.

The same pattern applies in other places:

  • E-commerce catalogs: products with different attributes per category (size for shoes, capacity for appliances) searchable without changing the database each time.
  • CRMs with custom fields: each customer defines their own fields and still filters and reports on them quickly.
  • Medical records by specialty: each specialty records different data in one system, with fast search across all patients.

What I’d do differently

I would write the protocol’s “measure under the real role” rule on day one of any prototype, because I only trusted it after seeing what happened without it. The benchmark measured one dominant tenant of 10 million policies; several tenants of that size at once were not measured, and I would add that scenario before calling the model closed. The queries that no index can fix (counting a huge list, aggregating it) turned out to be a product decision, not a database one. The product is not deployed yet: hosting is decided but not provisioned.

Evidence

Full version: private repository, available on request.