All case studies Voice assistant · the small-adoption case

Roxy: two pillars, seventy lines, in production

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.

2 of 6
pillars adopted, deliberately
prompts + schema; no codegen, no ORM
~70
lines of metadata for the whole schema
5 entities, indexes and keys included
5
prompt templates, text kept OUT of the code
provider-resolved, not string literals
1
language boundary crossed on purpose
Python app, Node-owned schema tooling

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 challenge: "adopt the framework" is usually too big an ask

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.

The solution: take the two pillars that hurt, leave the rest

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.

The money shot: the app is Python; the schema tooling is not

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.

What that buys you

  • Adoption is incremental and reversible. Two seams, seventy lines. Nothing about the rest of the codebase had to be renegotiated to start.
  • Prompt text stops being source code. It lives outside the Python, is resolved by a provider, and is checked against a typed payload — so a renamed field fails a gate instead of quietly degrading an answer.
  • A small schema still gets real migrations — generated from the declaration by diffing the live database, not hand-written and hoped over.
  • "Which pillars?" is a real question with a real answer. Six exist; taking two is a supported outcome, not a partial failure.

What to read it as

All case studies

Considering MetaObjects for your next platform?

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