Knowledge base · Essay
Why a schema change should ship with its content
- Kind
- Essay
- Published
- 22 Sep 2026
- Reading
- 8 min
- Centres on
coho diff v0.0.x
Applies to: every Coho project
You will learn: why Coho keeps the content model on the branch with the content, what that changes when you add or change a field, and what Coho checks before a change reaches your site
The short version
A schema change is only half of a change. A new required field means nothing until your entries have values for it. A renamed field means nothing until your site reads the new name. If the type and the content go out separately, there is a stretch of time when one has shipped and the other has not, and your site has to live through it.
In Coho, the content model lives on the branch, next to the content. You change a type and the entries that depend on it in one place, review them in one diff, merge them in one step, and release them in one tag. Nothing is half-shipped, because there is only one change.
Two changes that have to land together
Take an ordinary request: every article needs a subtitle under its headline. That is two pieces of work:
- The type: add a
subtitlefield toarticle, and make it required, because a headline with an empty line under it looks broken. - The content: write a subtitle for each of the forty articles you already have.
Neither half is useful alone, and each half does harm alone. A required field with no values makes forty entries invalid. Forty subtitles with no field to hold them have nowhere to go.
When the content model is managed apart from the content, as migrations run against a shared space or a schema edited in place while editors keep working, you have to choose an order, and every order leaves a gap:
- Schema first. The field appears everywhere at once, production included. Until every subtitle is written, the type promises something the content does not keep. Either the field stays optional "for now", or your site copes with entries that break their own type's rules.
- Content first. The values have nowhere to live until the field exists, so the writing waits for the migration, or happens in a spreadsheet and gets pasted in later.
- Both at once, by hand. Someone runs the migration and the import in a quiet hour and hopes nobody saves in between.
It does not end at launch, either. When something goes wrong, rolling back the content leaves the schema where it was, and rolling back the schema strands the content written for it.
One change, not two
In Coho, types, fields, and validation live on branches together with the entries. A new branch starts with the content model and the content exactly as they were where you branched, and from then on it records its own edits to both.
So the subtitle is one piece of work, on one branch:
coho branch create feature/subtitles --use # copies nothing, and switches to it
coho type put article --file article.json # subtitle added, required
# write the forty subtitles, in the portal or from a script
coho diff v0.0.x # the type and the forty entries, together
While you work, nothing else moves. v0.0.x keeps its own content model, other branches keep theirs, and production keeps serving the last release.
Every save on the branch is checked against the type as it stands on that branch. Once subtitle is required there, each article you save there must have one. Your teammates on other branches are not affected: their articles are checked against their own version of the type.
When the work is done, the diff shows it the way a reviewer needs to see it: the change to article, and the forty entries that change with it, as one unit. A reviewer approves the field and the words that fill it in the same review, not in two tickets a week apart.
What the merge checks
The merge is where the two halves meet the rest of the project, and it is where Coho does the checking:
coho merge --into v0.0.x -m "Subtitles for every article" --reviewed
It lands whole or is refused whole. The type change and the forty entries arrive on v0.0.x in a single step, or none of them do. There is no moment when v0.0.x holds the new field and only half the subtitles.
It checks the result, not the two sides. Before anything is written, Coho works out what v0.0.x will hold after the merge and validates every entry whose type the merge changes, against the type as the merge leaves it. That includes entries the branch never saw.
That last part is the one that saves you. Say a colleague added a forty-first article to v0.0.x while you were writing subtitles. Your branch is valid. Their article was valid when they saved it. Together they are not: the merged article type requires a subtitle that the new article does not have. Coho refuses the merge with VALIDATION_FAILED and names the entry, so you can bring it onto your branch and give it a subtitle. You find out on the merge, not as a broken page on your site.
It is the merge you reviewed. --reviewed passes the diff's merge token, so if anything on either side changed after you looked at the diff, the merge is refused with MERGE_REFS_MOVED instead of landing something you have not seen.
A release carries both
A tag freezes the content model and the content together. v1.4.0 means the same types and the same entries today, next month, and next year, so every question you can ask about a release has one answer:
- What changed?
coho diff v1.3.0 v1.4.0shows the field and the forty subtitles side by side, exactly as they were reviewed. - What is live? When
prodpoints atv1.4.0, it serves the entries ofv1.4.0, shaped by the types ofv1.4.0. There is no newer schema sitting under older content. - How do I go back?
coho rollback prodpoints production at the previous release, and the content model goes back with it. The subtitles and the field that holds them leave together, and neither is left behind without the other.
Your code can follow the same coordinate. A typed client generated from a tag describes exactly the content of that tag, because the types it was generated from were released with the content they describe. Generate from the release you are about to promote, and the types in your code and the content your site reads were made by the same change.
Changing a type in place
Branches are the safe way to change a type, not the only way. You can write a type directly on a version branch, and sometimes that is the right call: a new optional field, a longer maximum length, a clearer help text. Coho keeps you honest in three ways when you do.
Changes that can lose data ask first. Removing a field, narrowing its type, or dropping a locale is refused with DESTRUCTIVE_SCHEMA_CHANGE, listing each change, the types it reaches, and how many entries they hold. It goes ahead only when you confirm it. The entries stay as they were, and are checked against the new type the next time each one is saved.
You can check a release before you cut it. A type changed directly on a version branch does not re-check the entries already there. Tagging with --validate does:
coho tag create v1.4.0 --branch v0.0.x -m "Subtitles" --validate
Every entry at that point is checked against its type and its links. If any fail, nothing is tagged: the command lists the entries (up to a hundred) and exits with TAG_CONTENT_INVALID. Turn it on for every release if you would rather see an error at release time than at runtime.
Removing a field is two releases, not one. Code that is live today still reads the field you want to remove. Mark it as omitted first, release that, move your site off it, and only then remove it. Retiring a field in two phases walks through it.
Common questions
Can I change a type without touching any content? Yes. Adding an optional field, for example, is a change to the type alone. It still goes through the same branch, diff, merge, and tag, so it is reviewed and released like everything else.
What if two branches change the same type? A type merges the way an entry does. Changes that do not overlap both land. Changes to the same part of the type are a conflict, shown on the diff before you merge, and you choose which side wins. Either way, the merged type and every entry it governs are validated before anything lands.
Does promoting a release change what my site's code receives? Yes, and that is the point. The release brings its content model with it, so the content your site reads always matches a type that was released with it. Adding a field is safe for code that does not know about it yet. Removing or renaming one is not, which is why removing a field takes two releases.
Is there a migration step to run when I promote? No. The migration is the content you wrote on the branch, and it was merged, validated, and tagged with the type. Promoting moves a pointer to a release that already holds both.