Twenty client sites all need a blog post, an event and a person. Either you copy the schemas twenty times and maintain them twenty times, or you put them in a pack once and subscribe each site to it. RustyCMS does the latter — and still lets every site deviate where it has to.
Three layers
1. Repo defaults ./types/
2. Type packs (in subscription order) <content>/_types/<pack>/
3. Site override (highest priority) <content>/<site>/types/
Every site sees the sum of all three. A pack is maintained once; every subscribing site gets the change immediately — no copying, no deploy.
// <content>/site-a/.rustycms.json5
{
locales: ["de", "en"],
types: ["core", "press", "events"],
}
Merged per field, not replaced
This is where most inheritance models fall down: an override that wants to change one field has to copy the whole schema — and freezes everything else in the process. RustyCMS merges per field. A site override names only what differs: an extra field, a different label, a stricter pattern. Everything else keeps coming from the pack, and a new pack field shows up in sites whose override doesn’t know about it.
Boolean flags are OR-merged — an override can switch required on, but never silently off. Field order is preserved; missing entries are appended.
Manage without file access
Packs and subscriptions go through the API and the admin UI: create a pack, put a schema in it, subscribe a site, reorder. Every write rebuilds the cascade for all sites and invalidates the cache. If you’d rather edit files: the watcher covers _types/ too, a direct edit has the same effect.
curl -X PUT /api/environments/site-a/type-packs \
-H "X-API-Key: $KEY" -d '{"packs":["core","press"]}'
# Check the resolved schema — the way the site sees it
curl /api/collections/press_release?_environment=site-a
Deliberately left out
No “pack A inherits from pack B”, no pack dependencies, no conditional schemas, no loading from remote URLs. Three layers and one clear merge can be explained. A graph of packs cannot.