Python / Postgres / MetaObjects
Not every adoption is a rewrite. This Python voice assistant took exactly two pillars — prompt construction and schema migrations — in about seventy lines of metadata, and left the rest of its codebase alone.
Most case studies here are about a spine that drives everything. This one is the opposite, and it is the more common shape: an existing Python application that adopted two capabilities, in about seventy lines of metadata, and changed nothing else. It runs in production on a home voice assistant — wake events, alarms, scheduled tasks, notifications — and the interesting part is what it declined to adopt.
The honest objection to a metadata layer is that it looks all-or-nothing. A working application already has an ORM it likes, services it trusts, and a shape its team knows. Being told the answer is to re-express all of it as metadata is a good way to get told no — correctly, because the migration cost lands immediately and the payoff arrives later.
Roxy had two specific problems and no appetite for a rewrite: prompt text scattered through Python as string literals that silently rotted when a field was renamed, and a Postgres schema evolving by hand.
Prompts became declared metadata with the text held outside the code and resolved through a provider at runtime, rendered from a typed payload rather than a loose dictionary. The payload is the point: it names the slots once, and it is what the drift gate checks the template against. Rename a field and the mismatch is a build-time failure instead of a subtly worse answer in production.
The Postgres schema became five declared entities — wake events, notifications, alarms, scheduled tasks, panels — with their keys and indexes, in about seventy lines. Migrations are generated by diffing that declaration against the committed migration chain; a separate command checks the declaration against the live database.
Everything else stayed exactly as it was. No generated entity classes, no ORM change, no runtime framework. The application does not know MetaObjects is there except at the two seams where it asked for help.
Roxy is a Python application. Its schema commands run through the Node CLI:
meta migrate --db "$DSN" --dialect postgres
meta verify --db "$DSN"
The first generates the next migration by diffing the declaration against the committed chain. The second is the live one — it opens that database and reports where the real schema and the declaration disagree. Two commands, two jobs; only the second touches production.
That the schema tooling is Node at all looks like a gap and is a decision. Schema migration is owned by one toolchain on purpose, so a migration means the same thing in every language rather than five engines drifting into five dialects of "the same" DDL. The Python side gets the runtime it actually needs — the prompt renderer — as an ordinary Python package.
So the boundary is crossed knowingly, in a dev-only tooling package, and the payoff is that the schema story is identical for a Python app, a Java app and a TypeScript one.
Doug works directly with teams evaluating MetaObjects. Tell us what you're building and we'll tell you, honestly, whether MetaObjects fits.
Email doug@dougmealing.com