Addi · Architecture Disclosure · Tier 1
A digest, not a log: the decisions that shaped the architecture most, including one we superseded and kept, written in context, decision, consequences form.
Overview · Decisions
How to read this digest
This is a digest, not a log. It holds the decisions that shaped the architecture most, including one we superseded and kept, written in context, decision, consequences form. Context states the forces as we weighed them, value-neutral, including the alternatives we considered and rejected. Consequences include the negative ones, and every entry closes with what the decision cost us, because a decision record that only lists upsides is marketing. Each entry cites the architecture section where the resulting design lives.
Decision 1 · Accepted
Status: Accepted. See architecture section 5.
We serve two audiences with different postures: an invite-only internal operator dashboard, and a public self-serve product. They need separate identity instances, separate backend deployments, separate edge projects, separate domains. The obvious options were a second, separate consumer product, or one codebase carrying both. A separate product isolates risk and lets each side move at its own pace; it also means every capability is built and maintained twice, and the two products drift until they are only nominally the same system.
One product, one codebase, forked at the infrastructure layer and never at the code layer. The lanes differ in identity instance, backend deployment, edge project, and domain. Everything else that differs between them is expressed as configuration: which modules a tenant composes, which entitlements resolve.
Every capability is built once and reaches both audiences. The cost of a new lane is a deployment posture, not a rewrite. The negatives are real: internal-only surfaces must be actively kept off the user lane rather than being absent by construction; the configuration kernel carries the entire behavioral difference between the lanes and so must be trustworthy for this decision to stay safe; and a code-level defect is a candidate defect in both lanes at once, which raises the bar our delivery gates must clear.
The configuration kernel is load-bearing for both lanes, so it must be held to the standard of the strictest lane at all times. There is no cheap version of it.
Decision 2 · Accepted · Supersedes Decision 3
Status: Accepted. Supersedes Decision 3. See architecture section 8.
The agent is a writing principal, not a suggestion box, and the forces are safety of the record against usefulness of the agent. The conservative design is a blanket review queue on every agent write; it was our earlier design, recorded as Decision 3 with the full case against it. What that case left us is the narrowing question: what exactly are we protecting? Not fields, but human judgment already recorded in fields. If protection attaches to the provenance of the current value rather than to the identity of the writer, review can shrink to the only case where something is genuinely at stake.
Every field value records what produced it: a human, the agent, a report, an import, a template. That ladder is the write-authorization mechanism, and we state it as the write law: machine writes land freely into empty or machine-sourced fields, and never over a human-confirmed value; those writes queue as proposals instead, and acceptance stamps the value human-confirmed. Agent writes flow through the same per-module write doors as human writes. Where a capability is judged genuinely risky, it ships read-only rather than gated. Safety overall is budgets, traceability, and reversibility, not approval gates.
To the skeptical reader asking whether an agent should hold write access at all: the blast radius is bounded by construction. Agents get the door, never the tables, and no human-confirmed value can be silently displaced. Review attention concentrates on the one case that deserves it. The negatives: correctness now depends on provenance being recorded honestly on every write path with no exceptions, and the one-write-door invariant becomes a hard rule every module must pay for whether or not the agent ever touches it.
Withheld by design Tier 2
The scope and consent topology behind the door is held at Tier 2.Provenance had to be threaded through every write path in the system before the first agent write was safe. There was no incremental way to buy this.
Decision 3 Superseded by Decision 2
Status: Superseded by Decision 2. Kept for the record. See architecture section 8.
An agent that writes puts the record at risk, and the most direct control is to review everything. This was our earlier design for agent writes. It maximizes control, it asks the least of the write-path design, and it needs no machinery for telling one field from another, because every write is treated as equally dangerous.
Every agent write, regardless of target, queues for human review. Nothing the agent produces reaches a field until a person approves it.
The record is maximally protected and the agent is minimally useful. Human attention becomes the throughput ceiling for all agent work. Queueing writes into fields no human has ever touched asks people to review writes that displace nothing, and a queue dominated by safe items trains reviewers to stop reading it, which erodes the very protection the queue exists to provide. A blanket queue also protects everything equally, which means it protects the thing that actually matters, recorded human judgment, no better than it protects an empty field. These consequences are why we narrowed it: protection belongs to the provenance of the current value, not to the identity of the writer. Decision 2 records the design that replaced this one.
Nothing we still pay. We keep the entry because the write law is only legible against the design it narrowed, and deleting superseded decisions would hide the reasoning that produced the current ones.
Decision 4 · Accepted
Status: Accepted. See architecture section 11.
The reactive TypeScript backend is good at transactional and HTTP-shaped work. Property analysis is neither: it is long-running, it fans out across external sources that fail independently, and its heavy work (source document retrieval, spatial joins, document parsing) belongs in Python. Running analysis in-process is simpler to build and reason about, and it ties the health of the interactive product to the health of the least reliable external source we consult.
A deterministic analysis engine runs as a separate service behind one seam, in its own failure domain, with genuinely heavy Python work on a serverless Python tier. One async entry point; HMAC-signed callbacks; idempotent ingestion; tenant identity echoed on every callback. Producers fail soft: one missing source degrades the analysis with honest confidence, it never blocks it.
An analysis having a bad day cannot take the product down, and the engine can be deployed, scaled, and broken independently of the interactive surfaces. The negatives: asynchrony everywhere the product touches analysis, callback verification as a permanent obligation, results that are eventually consistent rather than immediate, and two runtimes to operate instead of one.
Every surface that consumes analysis had to be designed for the waiting state and the partial state, which is more design work than a blocking call would ever have demanded.
Decision 5 · Accepted
Status: Accepted. See architecture section 11.
The nationally pre-cached reference layer wants a dedicated warehouse; the designed target is a Postgres and PostGIS shaped store. Standing that infrastructure up front buys headroom we do not yet need and an operational surface we would pay for from day one. Deferring it without discipline means producers grow direct storage dependencies and the eventual migration touches everything.
Build the path, not the infrastructure. All reference data is reached through one access module; no producer touches storage directly. The storage engine behind that module is swappable by design, so the warehouse move, when scale demands it, is a one-module change.
We carry no warehouse operations today and lose no optionality tomorrow. The negatives we state plainly: the current storage arrangement does work a warehouse would do better, and the deferral is a form of debt we hold deliberately, with the trigger being scale rather than a date. The discipline that makes the swap cheap, no direct storage access anywhere, must be enforced continuously, because a single bypass quietly reprices the migration.
An indirection layer in front of every reference read, paid on every producer, for a migration that has not happened yet.
Decision 6 · Accepted
Status: Accepted. See architecture sections 9 and 11.
Generative reports are the fast path: point a model at the gathered data and let it write. The output is fluent and the build is short. It is also non-deterministic, resistant to per-value provenance, and impossible to re-run reproducibly, which collides with a product whose promise is evidence.
Cache the data, compute the meaning. No language model appears anywhere in the data producers. Reference data is pre-cached nationally and served locally at analysis time; interpretation is deterministic code; the four structured reports are deterministic renders over data, with provenance and confidence carried on every value, in words. Synthesis and grading belong to the agent, and agent-authored synthesis is labeled as such, a separate artifact from the structured reports.
Every value in a structured report is reproducible and can say where it came from. Disagreement between independent reads surfaces as signal instead of being averaged away by a fluent paragraph. The negatives: deterministic interpretation is expensive to write, every rule is authored rather than emergent, and the structured reports read as structure, not narrative; the narrative layer exists only where the agent has written it.
Withheld by design Tier 3
The interpretation internals themselves are held at Tier 3.We hand-built interpretation logic that a generative shortcut would have appeared to give us for free, and accepted a slower path to a demo in exchange for reports we can stand behind value by value.
Decision 7 · Accepted
Status: Accepted. See architecture section 10.
The agent's behavior is shaped by an operating doctrine. If that doctrine is pasted into sessions by hand, every session is a chance for it to be stale, edited, or missing, and the deployed platform and the agent's understanding of it drift apart. The alternative couples agent behavior to the platform's release cycle.
The platform serves the agent its current operating doctrine: the agent fetches its skill at session start, so behavior cannot drift from deployed truth. Behind it, agent output passes a mechanical house-style gate at the write door, so conformance is checked by code rather than trusted to instructions.
Doctrine versions with the platform and every session starts current. The negatives: the platform becomes the single distribution point for agent behavior, so a doctrine mistake reaches every new session at once, which is exactly why the mechanical gate at the write door exists as a second, independent control; and doctrine changes now move at deployment discipline speed rather than editing speed, which is a feature that occasionally feels like a cost.
The freedom to tune agent behavior informally. Every behavioral change is now a platform change, with everything that implies.
Decision 8 · Accepted
Status: Accepted. See architecture section 10.
The standard move is an embedded chatbot with metered usage: a revenue line, a controlled experience, a demo that sells itself. It also makes us the owner of a conversation UX, a model bill, and a second-class agent bolted to a web page. Our operators already run their own Claude, and the capable client already exists.
There is no in-app chat. The operator's own Claude connects over MCP through an OAuth 2.1 plus PKCE authorization surface built into the backend, with dynamic client registration, RS256 tokens, scoped access, and consent and audit recorded as schema. Every tool call independently re-verifies tenant identity. The agent is a first-class principal whose writes obey the write law like anyone else's.
Our job narrows to what we can do well: a safe, well-described tool surface with real authorization underneath, on open standards rather than a proprietary bridge. The negatives: we do not control the client experience or its quality; onboarding requires connecting an external client, which is a genuine adoption hurdle compared with a chat box that is simply there; and we forgo the metered-chat revenue line entirely.
The easy demo. A chat box in the corner sells itself in a screenshot; an authorization handshake does not, and we accepted that trade knowingly.
Decision 9 · Accepted
Status: Accepted. See architecture section 8, which carries the accountability doctrine this decision applies to pipeline movement.
Once an agent produces credible recommendations, automating pipeline movement is the obvious next step, and every automated stage change removes a human from a decision that money rides on. The forces are throughput against accountability, and the temptation is to automate broadly first and add controls after.
Pipeline movement is workflow by handoff: gated stages with human approval as the accountability rail. A recommendation never moves a deal on its own. The single automatic mover in the whole product is the configurable buy-box rejection, which records its reasons on every rejection it makes. Learned preferences are phased in from observed human decisions before any further automation is considered.
Every stage a deal passed has a person accountable for the handoff, and the one automation that exists explains itself. The negatives: throughput is bounded by human attention at every gate, operators carry approval work an automated pipeline would absorb, and the automation ambitions the agent makes possible are deliberately deferred behind evidence from observed human decisions rather than shipped on confidence.
Speed we could have claimed. We chose an audit trail of human accountability over a faster pipeline, and we accept the slower number that comes with it.