/work · Systems · Spheros Environmental · product line
The Spheros product-line platform
A three-layer platform where N customized client apps can't quietly become N unmaintainable forks, because the rule that keeps them apart is a lint error, not a paragraph in a README.
01 · Context
Every consultancy that builds the same kind of application for many clients eventually faces the same fork in the road: customize per client and drown in divergence, or standardize and lose the customization that won the work. The usual outcome is a shared starter kit that everyone copies once and then edits locally, which is divergence with extra steps.
Spheros needed the version where that doesn't happen: one deployed instance and one database per client, with the platform underneath staying genuinely shared. Three layers, extracted from a working demo rather than designed in the abstract: platform, a domain layer owning the water vocabulary and entity schema, and client apps on top.
02 · What I built
Six packages and a reference app:
- core: collection machinery, catalog and registry types, routing, RBAC vocabulary, audit and history, nav chrome
- kit: UI blocks, archetype shells, format and export libraries, and the block catalogue that documents them
- ui: shadcn-style primitives, vendored so the platform owns its own baseline
- water: the domain (entities, derivations, page components, domain widgets, schema lineage). The largest package by a distance, which is the point: domain knowledge concentrated in one place
- guidance: recipes, review checklists, agent definitions, skills, drift rules, and the convention ledger
- create-app: the scaffolder for new client apps
- showcase: the full-catalog reference app that proves the whole catalogue renders
The showcase is published and running, on seed data, at the link above. Its job right now is to be the demo: the proof of concept the firm shows the organizations it builds for, and the thing a new application starts from instead of an empty repository. What carries over is not the seeded water district, it is the decisions underneath it. The components, the pages, and the field relationships between entities are settled before anyone makes the first client-specific choice.
The convention ledger is worth singling out: written by auditing a real production app, normalizing its patterns into one convention, and resolving the places that app contradicted itself, down to where the Save button sits and why it moved. The kit and the archetype agents build against it, so “how should this page look” has a citation, not an opinion.
03 · The senior-engineer part
The layer contract is seven rules. The interesting thing is how little of it relies on anyone remembering it.
- Client apps may add pages, entity extensions, assemblies, branding and role profiles; they may not fork shared code, copy a kit block to edit it, or override a convention ruling.
- The domain package owns the entity core schema; client columns are extensions in the client app. Schema divergence is the one cost AI does not make cheap.
- The platform layer contains no domain vocabulary, enforced as a drift rule, so water terms grep empty across core, kit and guidance.
- An anti-snowflake covenant: every client app stays within about a quarter of platform minor versions. Content divergence unlimited; version divergence bounded.
- Config beats code, as a ladder: solve each ask at the highest rung that works: managed data, then config, then extension slots built on demand, then a client-local page from the same kit. A growing custom-code delta is a signal, not a fact of life.
- Every breaking change ships a codemod or agent skill alongside its changeset, so upgrades start deterministic and only the remainder is judgment.
- A promotion path with a human gate: client-local piece → domain package once a second consumer wants it → platform if it turns out to be domain-neutral.
Eleven ast-grep rules turn the mechanical parts into build failures: no domain imports in platform packages, no domain vocabulary in platform code, no raw table access, no local optimization of shared blocks, no non-deterministic seeding. Each message names the fix (“invert the dependency, or move this file down to water”), because a rule that only says no teaches nobody anything.
Compiling the contract into static analysis instead is what makes it survive turnover, deadlines, and me. That is the actual thesis of this project.
04 · How AI was used
The guidance package versions in lockstep with core, kit and ui: one changesets fixed group, one release train. That single decision is what separates this from a repository with good docs: the recipes, agent definitions, skills and convention rulings cannot drift out of date relative to the code, because they ship with it and their violations fail lint.
Three agents (page builder, page reviewer, drift auditor) and skills for branding an app, building a page, fixing drift, and logging a learning back into the ledger. The division of labour is stated rather than assumed: agents do most of an upgrade, humans review and merge.
The extraction ran the same way: a 707-file manifest, 544 import rewrites, and explicit gates: type-check green across five packages, then lint and drift green with 92 unit tests, then an exit gate with the full catalogue in seed mode, 27 end-to-end tests and a 398-route build. The log was written so a fresh session could resume mid-extraction, which is what documentation is for when your collaborator has no memory of yesterday.