Schema lifecycle: expand / contract
Your content schema — models and fields — evolves on its own loop. Additive changes are free. Destructive changes are gated by one rule: you may not remove schema the published site still queries.
Expand is always safe
create_model and create_field are additive DDL — a new column, auto-migrated.
A new field is simply invisible to the queries your published site already runs, so
expanding never breaks anything. (Adding a required field to a model with
existing rows needs a default_value, which backfills the column.)
Contract is gated by the footprint
The footprint
Every published version stores a footprint — a snapshot of exactly what GraphQL
surface the site uses: the Type.field pairs, type names, and root fields its
documents reference. It's computed by walking your queries against the live schema
at publish time.
The guard
Destructive schema ops — delete_model, delete_field, and breaking
update_field/update_model (a rename or a field retype) — are checked against
the published footprint before they run. If the live site still names the thing,
the op is refused.
Blocked by the migration guard: "The published site (version v7) still queries field
Post.subtitle." — you're trying to contract something still in use.
Non-breaking updates (validators, labels, hints, appearance) always pass — they don't change the GraphQL surface, so the footprint is unaffected.
The safe sequence
When you do need to remove or rename a field, the guard enforces expand-before- contract. Do it in this order and nothing ever breaks:
1. EXPAND create the replacement field
2. BACKFILL copy data into it
3. UPDATE CODE point the site's queries at the new field, then publish_site
(this snapshots a new footprint that no longer names the old field)
4. CONTRACT delete the old field — now allowed
Step 3 is the pivot: publishing new site code atomically updates the contract. Until you publish, the old footprint protects the live site; after you publish, the old field is free to drop.
This is the one place the schema loop and the site-code loop are coupled — and it's a one-way constraint, only at contract time. See how the loops stay decoupled.