Docs / Architecture / Architecture
Explanation For engineers

Architecture

How the factory is built, at the level you need before you change it: what it is for, what it connects to, what runs where, and the two rules everything else answers to. Each page after this one goes a level deeper.

At the centre of all of it is the data contract: v0.1 when Source ends, 0.x while it is agreed in Refine, 1.0.0 once it goes live — one artefact, called the same in the interface and in the code.

Goals and constraints

Five quality goals, in this order. Quality and risks gives each one's cost, and the tests that show it holds.

  1. Auditability. One record; every decision on it named and timed; nothing in it edited or deleted once written.
  2. Determinism. The same contract produces the same SQL, with no model in between.
  3. Failing closed. Told two contradictory things, or too little, the factory refuses rather than guesses.
  4. Portability across warehouses. Another engine is a connection, not a rewrite.
  5. Operability on one small box. One person can run it: one service, one database file, no build step.

The constraints, each with the decision that set it:

Constraint Why
Server-rendered Flask and Jinja; no build step, no client framework Decision 0003
The record is a database of its own, apart from the warehouse Decision 0002
The contract is the source of truth; SQL is derived from it Decision 0001
The process is a validated file, not code Decision 0004
Agents propose; named people decide Decision 0005
The API reads and never writes Decision 0006

In context

People work in the application, each on their own part of the record. Systems and agents read the same record — over the API with a service token, or over MCP — and cannot change it. Everything on the right is a connection, set in configuration: with none of them set, the factory still runs on its own — nobody signs in, DuckDB is the warehouse, and the catalogue is a file.

Use case teamasks for data, agrees thecontract, signs off the testSource ownerapproves loading its tables,agrees the contractData engineerbuilds, validates, takes it liveAuditorreads the record, changesnothingAdministratorpeople, roles andconnectionsAgents and systemsread the record over the APIor MCP, and never writeData Product Factoryone record of everyrequest, decision andrelease, and who madeeachdpf/web/entra.pyIdentityMicrosoft Entra ID, or thesite it runs insidedpf/warehouses/WarehouseDatabricks SQL, or DuckDBon a laptopdpf/llm.pyModel providerAnthropic's API, for thethree agents that draftdpf/trackers/Delivery trackerJira or Azure DevOps, thebuild plan as ticketsdpf/itsm.pyAccess requestsServiceNow, a ticket foreach demanddpf/catalog.pyData catalogueInformatica, Unity Catalog ora file
The factory in context. Who works in it, and what it connects to.

What runs where

One web application, one command line, two databases.

The web application is Flask under gunicorn, two workers, and it serves everything that answers HTTP: the pages, this documentation, the read API at /api/v1, and MCP over HTTP at /api/v1/mcp. The command line, python -m dpf, is where an operator administers the factory and cuts a release of it, and where an agent on the same machine reaches MCP on stdio.

Both run the same application code. The work that spans systems — drafting with an agent, building and testing in the warehouse, cutting a release, promoting, rolling back, detecting drift — is a function in pipeline.py, called the same way from a page and from the shell, so the record tells one story whichever way the work came in. Most reads and writes of the record are in repo.py, and the rules are in domain/, which touches neither database. Under them are the two databases — the control plane, which is the record, and the warehouse, which is the data — and the outside services the factory calls when they are configured. The identity provider is the one outside service only the web application talks to; see Identity.

Peoplein a browserAgents andsystemswith a servicetokenOperatorsat a shell on theserverdpf/web/The web applicationFlask under gunicorn: the pages, these docs, the read API and MCP over HTTPdpf/cli.pypython -m dpfadministration,releases, and MCP onstdiodpf/pipeline.pyThe application codepipeline.py for the work that spans systems, repo.py for the record, domain/ for the rulesdpf/db.pyControl planeSQLite: the recorddpf/warehouses/WarehouseDuckDB, orDatabricks SQL: thedatadpf/llm.pyModel providerthe agents' draftsdpf/trackers/The rest of theestatetracker,ServiceNow,catalogue
What runs where. The containers — two ways in, one body of application code, and what it keeps and calls.

Why the record is apart from the data is on The record; how the warehouse stays swappable is on The warehouse.

The two invariants

Everything here is answerable to these two rules. A change that would break one is the wrong change.

1. The contract is the source of truth; the SQL is derived and regenerable. Given a contract, domain/generate.py produces byte-identical SQL every time, with no model involved. That is what makes an audit trail mean something: the question "what produced this table?" has one answer, and it can be checked. Break this and the factory is a chatbot that writes SQL.

2. Every shipped column and every test traces to a versioned requirement. Columns declare satisfies, expectations declare verifies, and both live inside the hashed contract body, so one file is self-contained evidence. A criterion that nothing verifies is reported as uncovered rather than passing quietly. Break this and it is a build tool, not a control plane.

satisfies, verifiesgeneratebuildpromotedpf/domain/requirement.pyRequirementversioned,accepted by apersondpf/domain/contract.pyContractthe agreedinterface, hasheddpf/domain/generate.pySQL, tests, docsbyte-identical, nomodeldpf/domain/release.pyReleasethe SQL, stored,approveddpf/pipeline.pyLivea prod schema,and the interfacepublished
From requirement to table. The contract in the middle — traced up to what was asked for, derived down to what runs.

A release keeps the SQL it was built from, and going live runs that stored text rather than regenerating it; a rollback is a new promotion of an earlier release, never the deletion of the one it replaces. Runtime follows both in order.

The process is configuration

dpf/workflow.yaml names the phases, their steps, the gate that ends each phase and who is accountable for it — the drawing on the overview is generated from it. The application validates the file when it starts and refuses a step that does not exist or a dependency in the wrong order, rather than rendering a process that is subtly wrong. DPF_WORKFLOW points at another file to run another process: see the workflow reference, and Governance for why the three gates ask three different questions of three different people.

In this section

Page What it answers
Components The building blocks, what each may and may not do, and where a change goes
The record What the factory stores, what it computes instead, and what never changes
Runtime The scenarios that matter, step by step, in the order they happen
Deployment What runs on the server, and what leaves it
Identity The doors, and how a person becomes a role
Security model What the controls protect, and how they fail closed
The warehouse The seam between the factory and the engine that holds the data
Crosscutting concepts The rules that apply everywhere
Versioning How the factory and these pages are versioned together
Quality and risks What the design optimises for, and what it still owes

Why it is this way, decision by decision, is in the decision records.

Documentation for version 0.2.0 Reviewed 2026-10-19 Covers dpf/domain/generate.py, dpf/domain/contract.py, dpf/pipeline.py, dpf/web/__init__.py, dpf/web/api_v1.py, deploy/dataproductfactory.service