Architecture: structure over discipline
The platform was built as a single, unified codebase organized around clear domain and infrastructure boundaries, boundaries I designed in Act I specifically so they would hold up under Act II. The guiding idea was that the structure of the code, not anyone's discipline in a code review, should be what keeps it coherent. A few principles carried the most weight:
- Boundaries enforced by tooling, not goodwill. Code was separated by where it runs and who consumes it: client-facing code, server-only logic, and a shared layer of pure models and validation that both sides agree on. Those separations were enforced automatically; client code simply could not reach into server internals, because the tooling rejected it. This is the decision that let an early MVP scale into a larger team's codebase without fraying. The boundary is a compile-time fact, not a hope.
- A single, auditable path to data. Rather than scattering database access throughout the application, all of it flowed through one consistent layer. When several engineers later worked across the same areas, that discipline kept the same query from being reinvented three subtly different ways.
- Typed from the database to the browser. Contracts between the backend and frontend were expressed once and shared as types, so a change on the server surfaced as a compile error in a teammate's UI code rather than a runtime failure in production. That end-to-end type safety is the safety net that makes parallel work by multiple people viable.
- A foundation owned with the CTO. The database and deployment infrastructure were set up and owned by the CTO, with each environment isolated so any change could be previewed and tested in safety. I designed the domain model and the application architecture on top of that foundation.
- Background workers for the heavy and the asynchronous. Document processing, the AI extraction and search workloads, and realtime updates ran as separate background services, letting the AI surface evolve independently of the core product without slowing either one down.
Why these frameworks and models
Framework and model choices were made to maximize correctness-per-engineer-hour, first for one engineer racing to an MVP, then for a team building on top of it.
- Next.js and React (App Router). One framework for routing, server rendering, API routes, and the auth guard. Server components meant most data-fetching never shipped to the client, and the layout model matched the product's role-based navigation across distinct user types cleanly.
- A typed, transparent ORM. Essentially typed SQL: no query-builder magic, migrations are real SQL files, and the schema is TypeScript. For a compliance domain that needs precise reasoning about edge cases in the data, transparency beat convenience.
- A self-hosted auth library. Sessions, verification tokens, and progressive login lockout are exactly the things you don't want to hand-write, and exactly the things a hosted vendor makes hard to customize. A self-hosted instance I could extend (placeholder users, email-invite flows, two-factor auth).
- Schema validation as the shared contract. Validation schemas live in the shared layer and are imported by both client and server, so a form, an API route, and a database write all agree on the shape of the truth.
- A model-agnostic AI abstraction. Decoupling which model runs an extraction from the pipeline that calls it meant provider swaps became a config change, not a refactor.
On the AI specifically, the approach was task-routed, not one-model-fits-all. The highest-value structured-extraction workload runs through a constrained pipeline with schema-validated output and a retry guard for the real-world case where a model returns an empty or malformed result, because onboarding can't silently drop someone's data. Semantic search runs on a dedicated embeddings worker. Keeping the model layer behind an abstraction meant provider and model choices stayed swappable as price and quality trade-offs shifted.