Cinema Ops Platform
2026Data Platform · Governed Access · Agent Tools
A governed data platform for exhibition. Four unlike sources — an API, landing files, an operational Postgres and a ticketing stream — land raw in an append-only bronze layer, are cleaned into silver and modelled into gold, and leave through a fixed set of read-only tools an agent can use without ever being able to see a person.
A question on a Tuesday
A site manager wants to know whether the matinee earned its slot: did the occupancy, or what people bought on the way in, justify keeping it? Today that answer arrives after next week’s schedule has already locked, because the systems that run a cinema were built to record the house, not to be asked about it.
It is the read path, the other direction of travel. Theatrical writes to the platform; this reads from it — safely enough that the one asking can be an agent.
Four sources, three layers, one way out
A film metadata API, landing files, an operations database and a ticketing stream arrive in four different shapes. They land in an append-only bronze layer with a dead-letter queue, quarantine and idempotent merges; they are cleaned into silver; they are modelled into a governed gold layer of dates, films, sites, bookings and sessions.
Nothing reads gold directly. The only way out is a fixed set of read-only tools, over MCP and HTTP, whose response types were designed before a single row existed.
Idempotent, ordered, and never someone else’s problem
Idempotent, not exactly-once. Bronze merges are INSERT … ON CONFLICT DO NOTHING on a payload hash, so a re-run lands nothing twice; exactly-once delivery was rejected as the wrong thing to chase. Late data. The operational database is read on a high watermark with a deliberate overlap window, because a transaction’s business time can precede its commit. Change data capture was rejected for an operational reason: a replication slot on someone else’s database means my consumer stalling becomes their disk filling up. Bad events. Unparseable stream messages go to a dead-letter topic with their original bytes and the reason in headers. Grain. A booking’s id rides on the ticket fact as a degenerate dimension; its total does not, so it can never be summed once per ticket.
The extractor base class makes run() final — a subclass that overrides it fails at import — so every source goes through the same ordered steps.
A disclosure with a GROUP BY on it
Personal data isn’t filtered out on the way to the agent; there is nowhere for it to go. The query never selects it, the response type has no field for it, and the database role holds no grant on it. Card data is classified excluded: no source carries it and no extractor reads it, so there is no copy anywhere to govern, encrypt or leak. The agent’s role is granted columns, not tables — GRANT SELECT (customer_key, signup_date) — and the access log has no UPDATE grant at all.
Aggregates have a floor of five. An aggregate computed over one ticket is not an aggregate; it is a disclosure with a GROUP BY on it.
Done is a green exit code on a clean clone
Every claim ships the command that proves it. Kill tests show the extraction role cannot rewrite bronze; the access log records refusals as well as answers; the agent tools run against an eval suite and a red-team set. An injection-resistance claim with no test behind it is not a security property. It is a hope with good posture.
And the record is honest about its edges. It holds no real operator data — the sites are synthetic, and the film metadata’s last hop is seeded, which the README says. Its failure modes are written down as predicted, not observed. What breaks at circuit scale, and what was deliberately not built, each has a section of its own.
Read the argument, run the platform
The source, the architecture record and the essay behind it are public. docker compose up brings up Postgres, the asset graph and a seeded gold layer.