/work · Systems · LRE Water / Spheros Environmental
Headwaters: the platform monorepo
I built the monorepo my team's application development runs on: fifteen production applications, eight shared packages, and three code generators, where provisioning a new authenticated client app is a command, not a project.
Live sites · 5
- cow.cnppid.com Central Nebraska Public Power & Irrigation District
- dashboard.svlh.gov St. Vrain and Left Hand Water Conservancy District
- headwaters-gcwin.vercel.app Grand County Water Information Network
- headwaters-five-rivers.vercel.app Five Rivers Cattle Feeding
- mtgcd.lre-up.com Middle Trinity's rainwater harvesting calculator
01 · Context
Headwaters is the monorepo I built for my core development team at LRE Water / Spheros Environmental. It has carried the majority of my team's application development since 2025: fifteen production applications sharing one toolchain, one component library, one data layer, and one authentication story.
They are not variations on a theme: Colorado DNR's ArkTools, the Towns of Castle Rock and Eagle, CNPPID, Middle Trinity GCD's rainwater-harvesting calculator, Greeley Loveland Irrigation Company, Orchard Mesa Irrigation District, Dolores accounting, a CCBWQA data importer, GCWIN, SVLH, Five Rivers Cattle Feeding. Public municipal sites, authenticated dashboards, calculators, importers. Several are rebuilds of older standalone sites, brought in rather than maintained forever in place. Naming them is not an invitation to go and look: the dashboards and importers sit behind a login. The five that are public are linked at the top of this page.
One is for AngloGold Ashanti, a mining company: the useful outlier. It is an early draft rather than a delivered app, and the engagement has not been picked up as a contract, which is worth saying because the argument does not need it to have been. That the platform carried an app from an entirely different industry without anyone forking it is a better argument for the layer boundaries than any diagram of them.
One boundary worth stating: I built this, and my employer owns it. Authorship is the claim here, not the asset.
02 · What I built
Fifteen apps on Next.js 15 and React 19, over eight shared packages, with Turborepo pipelines keeping builds cached and dependency-ordered across all of them:
- A shared UI package that is not a thin wrapper over shadcn; alongside sixty-odd primitives it carries the domain: react-hook-form field wrappers so a form is declared rather than wired, virtualized multi-autocomplete for fields where a district has thousands of options, a geocoder hook, and the mapping furniture
- A graph system split three ways along a deliberate seam: types and query validation, Recharts rendering, and dynamic SQL through Drizzle
- Shared geocoding, Drizzle config, and ESLint/TypeScript configs as packages, so consistency is enforced by dependency rather than by review
- Drizzle over PostgreSQL as the data layer, with Auth0, Sentry and Mapbox GL
What a generated app inherits, and how a request moves through it, described:
- A generated application lands inside a Turborepo monorepo: 15 applications under apps/, and 8 shared packages under packages/.
- Of the 15 applications, 6 were produced by the app-auth generator, 3 carry the map feature, and 5 carry the graph feature.
- The 8 packages are ui, graph-client, graph-core, graph-server, geocoding, drizzle-config, eslint-config and typescript-config.
- Every generated app inherits 4 of those 8: ui and drizzle-config as runtime dependencies, eslint-config and typescript-config as dev dependencies. Turborepo pipelines keep builds cached and dependency-ordered across all of them.
- Inside one of those generated apps, a request hits middleware.ts before anything else, and that single boundary has three outcomes.
- First: the /embed/* routes and the graph data API are deliberately public, with no authentication at all.
- Second: other /api/* routes return 401 without a session.
- Third: everything else goes through the Auth0 session middleware.
- An authenticated page then calls verifyAccess(roles), which reads the session through a per-request cached data access layer, getSession() wrapped in React cache. Server components then read data through Drizzle.
- Auth0 is the identity authority, and the database keeps a synced copy: on every session check the app calls the Postgres function data.sync_user_from_auth0, which upserts the user record. Admin server actions call back out to the Auth0 Management API.
- The app also reports errors to Sentry, and the three apps carrying the map feature load basemaps and vector tilesets from Mapbox.
- The database is split into two schemas by role, and this is the load-bearing idea in the picture. The data schema holds 14 tables: the writes, and the source of truth. The ui schema holds 10 views over those tables, and every read the application makes comes from the views, not from the tables.
- Drizzle introspects the live database with db:pull to regenerate the typed schema, so the types follow the database rather than the other way round.
- Across the three generators the database ends up with 34 objects: 2 schemas, 14 tables, 10 views, 1 function, 6 indexes, and 1 extension, postgis, installed by the map generator.
- In the drawing, an accent outline marks anything outside the app process: Auth0, Sentry, Mapbox, and the database.
The generator that starts an app is `app-auth`, and calling it a scaffold undersells it. Five prompts produce a hundred and eleven files, and then it offers four setup scripts, individually or together, that leave the machine: an Auth0 application with its scopes, roles and a post-login action bound into the login flow; a Sentry project and its alert rule; three migrations run over mutual TLS; and typed schema regenerated from the live database.
The generator system: three commands, one provisioned application, described:
- Three generators write into one application: app-auth starts a new authenticated app from five prompts and 111 files; add-map grafts on a GIS application in 148 files, including a legend admin and map sharing; add-graphs grafts on URL-driven charts in 83 files, including public embed routes.
- What lands inside the generated application: the application code (routes, components, hooks and admin surfaces), configuration edited in place rather than only created (env file, route map, sidebar navigation, theme CSS), twelve migrations in one numbered namespace shared across all three generators, and a typed database schema regenerated from the live database rather than hand-written.
- What gets provisioned outside it, by real API calls: an Auth0 application with its scopes, two roles and a post-login action; a Sentry project and alert rule; Mapbox basemaps and vector tiles for apps carrying the map feature; and a PostgreSQL database that ends up holding 34 objects across schemas, tables, views, a function, indexes and the postgis extension.
- Provisioning is not automatic. Each setup script is offered on its own, or all of them together, so a person decides what runs.
- The Auth0 objects are a one-way door: they have no rollback and are not idempotent, so running the generator a second time creates duplicates rather than reconciling.
What the feature generators install is the part that surprises people. `add-map` lays down roughly 150 files and five migrations, and what they add is a small GIS application:
- Channel-based symbology: shape, color, border, fill and stroke each bind independently to a data attribute
- Four legend types (categorical, binned, gradient, attribute) with numeric-break classification behind them
- Filters across category, number, date and boolean, with AND/OR logic and per-attribute null handling
- A legend admin, so non-developers change colors through pickers and inline-editable tables rather than filing a ticket
- Map state serialised to the URL, plus a shares table, so a hydrologist can send a colleague the exact filtered, styled map they are looking at
- Drawing tools, measurements, basemaps, location search, typed popups
`add-graphs` installs about eighty files, four migrations, and a rule: every graph is described entirely by its URL. That is what makes charts shareable, bookmarkable and embeddable: the generator ships /embed routes for iframes alongside a data API, SWR-cached fetching, 2D zoom, PNG export, dual Y-axes, and a table view of the same query.
Not every app is a dashboard. The regulatory importer is a queue: upload becomes a job, records land pending, and are grouped, reviewed and corrected before they commit. Agencies whose submissions get audited need the reviewing step to be part of the software, not part of the etiquette.
03 · The senior-engineer part
The generators are where this stops being a folder of apps and becomes infrastructure.
`turbo gen app-auth` scaffolds an entire authenticated application: middleware, Drizzle config, Auth0 wiring, Sentry instrumentation, dashboard shell, auth data-access layer, error boundaries, and a user-admin drawer that can deactivate, reactivate and send password resets. Then it provisions the infrastructure: creates the Auth0 application with its callback URLs, creates the Sentry project and retrieves the DSN, seeds role-scoped users, and runs migrations written to be idempotent.
Every one of these was a decision to make the second, fifth and fifteenth instance cheap, taken when only the first existed and nobody would have blamed me for hard-coding it. That is most of what the job actually is, and it never shows up in a screenshot.
04 · How AI was used
The monorepo carries its own CLAUDE.md (build commands, code-style rules, Server Component preferences, import and error-handling conventions), so agents working in any of the fifteen apps write code matching the house style rather than their own defaults. The generators do the same job for humans and agents alike: they make the correct thing the default thing. A generator is a better instruction than a paragraph asking someone to remember the instruction.