Skip to content

← Blog   Engineering · · 8 min read

Nested entries: content that mirrors reality.

Collections, entries and fields are the whole content model — plus one more idea that does most of the work. Why sub-collections beat reference fields for anything that owns something else.

Birkly Team Building in the open

Birkly has three primitives. A collection is a named set of entries. An entry is a set of fields. A field has a type. There is no separate concept for a page, a post, a product or a taxonomy term — those are all just collections with different names.

People usually accept that quickly. The question that follows is always the same: what happens when one thing belongs to another? An author has posts. A product has variants. A documentation section has articles, and those articles have versions. Three primitives sound elegant until reality turns out to be shaped like a tree.

The answer is sub-collections, and it is worth explaining why we did not do the obvious thing instead.

The obvious thing, and why it hurts

Almost every CMS models relationships with reference fields. A post has an author_id that points at a row in the authors table. It is the relational-database answer, it is correct, and it has been the default for thirty years.

It also produces the same three problems on every project.

You always need one more query. You fetched the post. Now you want to render the author's name and photo, so you fetch the author. In a template loop over twenty posts, a naive implementation fetches twenty authors. Every CMS solves this eventually, and every solution is a layer of caching or eager-loading configuration that you now have to understand.

References dangle. Someone deletes an author who still has posts. Either the CMS refuses — confusing the editor, who does not think of it as a database constraint — or it allows it, and you have twenty posts pointing at nothing. Both outcomes are somebody's afternoon.

The model stops describing the thing. An author_id on a post says "these two records are associated." It does not say which one owns the other, which one should be deleted when the other goes, or which one you would naturally navigate from. That information lives in your head and in the code, not in the schema.

What a sub-collection is

A sub-collection is a collection that lives inside an entry rather than beside it. An authors collection has entries; each author entry can contain a posts sub-collection with its own fields and its own entries.

The structural claim is simple: ownership is part of the model, not something you remember. Fetching an author gives you their posts. Deleting an author takes their posts with them, because posts that belong to a deleted author are not orphans, they are gone. Navigation runs the way you would describe it out loud.

A worked example: documentation

Documentation is where this earns its keep, so use it as the test case. The shape is three levels deep:

sections → each section has articles → each article has versions

With reference fields, this is three flat collections plus two foreign keys, and every read of "show me this section with its articles and each article's current version" is a join or a cascade of API calls. Rendering a sidebar tree means either fetching everything and reassembling it in the template, or writing a bespoke endpoint.

With sub-collections, the tree is the storage. You fetch the section and you have the section. The template walks it the way it appears on the page, because the structure on disk and the structure on screen are the same structure.

There is a second-order benefit that only shows up once agents are involved. When an AI reads your documentation through MCP, a nested tree tells it what contains what. A flat table with foreign keys tells it there are some IDs. The Agent Guide can expose the whole tree as grounded context precisely because the tree exists as a tree.

When a flat reference is still the right call

Sub-collections are not a universal replacement, and pretending otherwise would set you up to build something awkward.

Use a sub-collection when the relationship is ownership and roughly one-to-many: an author's posts, a product's variants, an event's RSVPs, a section's articles. If deleting the parent should delete the children, that is your signal.

Use a flat collection with a reference field when the relationship is many-to-many or when both sides have an independent life. Tags are the clearest case: a tag is not owned by any one post, it is shared across many, and deleting a post should not delete the tag. Categories, contributors credited on several projects, venues used by many events — all flat, all referenced.

The rule of thumb we use internally: if you would say "the X's Y" in conversation, it is a sub-collection. If you would say "Y, which relates to X," it is a reference.

The trade-off

Nesting makes one class of query harder. "Show me every article across all sections, newest first" is trivial in a flat model and requires walking the tree in a nested one. If your primary access pattern is a global list rather than a hierarchy, the flat model is genuinely better and you should use it — Birkly does not stop you.

What we found in practice is that the hierarchical read is the common one and the global read is the occasional one, and it is better to make the common case free. Most CMSs made the opposite bet, largely because the relational database made that bet for them before anyone chose it deliberately.

Why this matters more than it sounds

Content models outlive the sites built on them. The schema you sketch in the first week is the thing you are still living with in year four, when the requirements have changed and the original developer has left. A model where ownership is explicit survives that better than a model where it is convention.

It is a small idea. It changes how you structure almost everything.

Birkly will be free and open source at release. Join the waitlist if you want to try the model on something real.

Read next