Technology
Git-Based CMS or API-Based CMS: Where Should Your Content Physically Live?
Zealsync builds a git-backed publishing product, so take the disclosure first and the reasoning second. Eight axes decide where content physically sits, and each is stated with the condition that disqualifies the answer we would otherwise prefer.

The choice between a git-based CMS and an API-based CMS is usually presented as a choice between two products. It is not. It is a choice about where a byte of your content physically sits when nobody is looking at it, and what the authoritative record of how it got there looks like. Every other difference follows from that.
Two answers to one question: where the content physically sits
In the git-based model, the canonical copy of an article is a file in a version-controlled repository. It has a path. It is usually Markdown or MDX with a structured header, sometimes YAML or JSON. The history of that file is a chain of immutable objects: each commit points at a tree, each tree points at the blobs that are your files, and every blob is addressed by the hash of its own contents. Nothing in that chain is edited in place. Change a file and you write a new object; the old one stays where it was.
In the API-based model, the canonical copy is a record in a service's database. It has an identifier rather than a path. You read it over HTTP, either once at build time or on every request, and the service decides what queries it will answer, what fields it will return and what its revision history looks like. The store is authoritative and the files you see are always derived.
These are frequently confused with the headless question, and they are not the same question. Headless describes delivery: the content store does not render your pages, so you render them yourself. Both storage models can be headless, and a headless CMS storage model can be either files or a database. You can be headless and file-based. You can be coupled and database-backed. Deciding you want a headless architecture settles nothing about where the content lives.
Eight axes decide it. Two of them carry most of the weight and are argued at length below; three are compressed here and treated properly elsewhere; the rest sit in between.
- Relational depth: how many entries point at other entries, and how independently those pointers are edited.
- Media weight: how large your binaries are and how often they are revised.
- Editor population: whether the people who change content are the people who deploy it.
- Editing environment: how many people are in the same document at the same time, and how often each of them saves.
- Consuming surfaces: how many independent systems read the same entries, and whether they can be rebuilt when one changes.
- Approval: where the gate sits and whether the path around it is closed.
- Latency: the worst-case delay between a person deciding to publish and a reader seeing the page.
- Exit cost: what you actually walk away with when you leave.
The useful way to run these is not to score them. It is to look for the one axis where a disqualifying condition genuinely fires, and let that decide. Most projects have zero or one such axis. If none fires, both answers work and you are free to choose on something softer.
The disclosure, and why it belongs at the top rather than the bottom
Zealsync develops Acrosite, a git-backed content publishing product. That is an interest in the outcome of this article, and it belongs in the second section rather than in a line of small print after two thousand words, where a reader who has already been persuaded discovers what the persuasion was for.
The disclosure carries an obligation with it. Nothing below uses Acrosite as evidence. No section argues that a git-based model is correct because a Zealsync product implements one, describes what any Zealsync product supports, or infers a general truth from a particular piece of software. The arguments stand on git object semantics, HTTP caching behaviour and ordinary build-pipeline mechanics, all of which you can verify without reference to any vendor, including this one.
An interested party's argument is only worth reading if it names the conditions under which its own preference loses.
That is why every axis below ends with a disqualifying condition rather than a preference. A disqualifying condition is a statement of the form: if this is true of your project, the model we would otherwise prefer is the wrong one, and here is the observable tell that it is true. If you find one that describes you, stop reading and act on it.
Relational depth: entries that point at other entries
A file is addressed by its path. A relationship between two files is therefore a string: a slug in a header field, a relative path, an identifier you agreed on. Nothing in the storage layer knows that the string refers to anything. If you delete the target, the reference does not break at the moment of deletion. It breaks later, at build time if you have a check for it, or in front of a reader if you do not.
A database-backed service treats a relation as a type. The store can refuse a save that would leave a dangling reference, and it can answer the reverse question cheaply: what points at this record. In files, the reverse question is a scan of the tree. That scan is fast at a few hundred entries and still perfectly workable at tens of thousands, because searching text files is not expensive by modern standards. The cost is not read performance. The build resolves references once, and the reader is served static output that has no idea a relation was ever involved.
The cost is editorial. An editor removing an author record cannot be told, at the moment they remove it, which twelve articles will be left pointing at nothing, unless something computes that answer for them. In a relational store, the store itself is that something. In a file-based setup, it is a tool you build or adopt, and its absence is invisible until the day it matters.
Argue the other side honestly. The relational depth of most marketing and editorial content is close to nothing. An article has a topic. It has, at most, an author and a short list of related pieces. That is one level deep, and it is flat enough that a build-time check catches every problem it can have. Buying a relational store to manage a one-to-many with three fields on it is not a decision, it is an anxiety. Shallow content should not be judged by the failure modes of deep content.
The disqualifying condition is depth combined with independent editing. If entries reference entries that reference entries, and different people edit each level on their own schedule, referential integrity has to live in the store. A product catalogue where variants reference option sets that reference availability rules is that shape. A documentation set where component pages compose from shared parameter records reused across dozens of pages is that shape. So is any structure with many-to-many relations that carry their own attributes, or an explicit ordering that somebody maintains by hand. In those cases files disqualify themselves, and the tell arrives before the decision does: your team keeps writing scripts to find out what broke.
The counter-tell is worth stating too, because this is the axis people most often talk themselves into. If you have gone eighteen months without a broken reference reaching production, relational depth is not your problem and it should not be casting a vote.
Media weight, and the permanent cost of binaries in history
This is the axis that surprises people, because the failure is slow rather than sudden and nobody is present at the moment it becomes irreversible.
Git stores content as objects addressed by the hash of their contents. Replace an image and you have written a new blob; the previous blob is still reachable from the commits that referenced it, which is precisely what makes history useful. Delete the file entirely and nothing is recovered, because deletion is a new commit that removes the path, not an edit to the past. The bytes are still in the object store, and they are still in every clone.
Compression does not rescue this. Packfiles delta-compress well across similar text, which is why a thousand articles of prose occupy a few megabytes and always will. Already-compressed formats behave differently. Re-export a JPEG, a PNG, a WebP or an MP4 with a trivial change and the byte stream differs substantially throughout, so there is no useful delta to store and each revision costs close to its full size. Shallow and partial clones reduce what a CI runner has to fetch, and they do not reduce what the archive contains.
There are two live alternatives and both are respectable. Keep the media outside the repository in an object store or a media service and commit only references, accepting that your content and your assets now have separate lifecycles and separate backups. Or keep the binaries in history deliberately, with the growth curve understood in advance, using a large-file extension that moves the bytes to a side store behind small pointer files. The second is a real answer rather than a workaround, provided it is chosen at the start. Adopted in a panic after the repository is already heavy, it becomes a migration on top of a problem.
The disqualifying condition for keeping media in the repository is frequent revision of large binaries. An image-heavy catalogue that is re-shot seasonally, a report set re-exported as PDFs every month, anything involving video. If that is the workflow, references and an object store are correct and the argument is over. If your media is a few hundred illustrations that change rarely, this axis does not fire, and the file-based model absorbs it without complaint. The tell that you got it wrong is a fresh clone in CI that takes minutes, at which point the cost is already sunk.
Who edits, how often, and from where
Two separate axes live here, and conflating them produces bad decisions in both directions.
The first is population. If the people who change content are the people who deploy it, files are the natural shape and nothing needs to be built. If they are not, everything depends on the editing layer standing between the person and the repository. That layer has to hide branching, merge semantics and commit syntax completely, and it has to be a structured editor rather than a code editor, because asking a writer to be careful with a header block is asking them to do a developer's job badly. At this point the question of file based content vs database content becomes a question about the quality of one piece of software rather than about storage at all.
The second is environment, and specifically concurrency. Two people editing the same file is a merge in the git model. Git merges by line and knows nothing about your document: two editors working in different sections of one long article merge cleanly and neither notices, while two editors in the same paragraph produce a conflict with markers in it. A conflict shown to a non-technical editor is a failure of the layer, not of the editor. A database-backed store handles the same collision differently, with field-level locking, presence indicators, or a last-write-wins rule that at least fails predictably.
Frequency matters alongside it. Repositories handle deliberate, occasional, whole-document changes very well, because that is the workload version control was built for. They handle continuous small mutations badly. A status flag toggled forty times a day, a field somebody nudges while on a call, a note edited between meetings: a commit for each of those is absurd, and batching them means the recorded history is a fiction produced by the tool rather than a record of what people did. That fiction is worse than no history, because it looks authoritative.
The disqualifying condition is simultaneous multi-author editing of the same documents with live presence as a requirement. Several people typing into one document at once, seeing each other's cursors, expecting the result to converge without anyone thinking about it. That is achievable over a repository, and doing it means building a collaborative editing system on top of a version control system designed for asynchronous, whole-file, offline work. The engineering is real and it is not where your advantage is.
The counter-case: a team of four writers publishing a few times a week, each working on their own piece, has none of this. Do not let a requirement you do not have decide the architecture. The tell for whether it applies is embarrassingly simple. Ask when two of your people last needed to be inside the same document at the same time. If the honest answer is never, the axis is silent.
How many surfaces consume the same content
One website is the easy case. The build reads the content, produces HTML, and a CDN serves it. Because the output is static and asset filenames can carry content hashes, those responses are cacheable aggressively and servable without an origin round trip, which is the cheapest and most reliable delivery arrangement available. Nothing about that requires a runtime content API, and adding one buys nothing.
Several surfaces is a different problem. A mobile application, a partner feed, an in-product help panel and a marketing site all reading the same entries need to query them: filter by a field, paginate, request a subset of the fields, ask for what changed since a timestamp. Shipping a repository to each consumer is not a serious proposal, so a git-based source answers this by publishing a derived surface, usually a generated JSON index served as a static file.
That derived surface is legitimate, and it has a freshness contract you now own. It is exactly as current as the last successful build, and the cache headers on it are your decision and your problem. If a consumer holds a response for an hour, your content is an hour stale for that consumer regardless of what your CMS believes. Once you have built the index, the versioning of its shape and the invalidation story, the honest question is whether you have reconstructed an API-based CMS with less tooling and nobody maintaining it.
The disqualifying condition is three or more independent consumers that need to query the content, run on their own release cycles, and cannot be rebuilt on demand when an entry changes. When a consumer ships through an app store review, you cannot rebuild it to change a sentence, and the content has to be readable at runtime. If every consumer is a site you control and can rebuild in minutes, the condition does not fire no matter how many of them there are.
Judge this axis on what exists and what is committed, not on what somebody might want. Consumer count is the number most often inflated during a decision and most often unchanged two years later.
Approval, latency and portability, stated briefly and linked out
Three axes remain. Each is genuinely decided by considerations that sit outside the storage question, so each gets its conclusion here and its full treatment elsewhere. The conclusions stand on their own; the links are for depth, not for completion.
Where the gate has to sit for it to refuse anything
A gate is only a gate if the path around it is closed. In a repository the natural gate is a protected branch with review required before a merge; in a service it is a workflow state on the record that one role cannot advance alone. Both can be real, and both are routinely fake, because somebody retains direct push rights or an editor role can move a record straight to published. Neither storage model makes the gate honest. The storage model only decides where you go to check. The test is behavioural rather than architectural: try to publish something without approval and see whether you can. That question is worked through properly in where drafts live and what stops them, which owns it.
The delay between a decision and a live page
Under a git-based model, publishing is a commit that triggers a build and a deployment, and the delay is build time plus propagation. Under a runtime model, the change can be visible on the next request. Neither is instant in practice, because a cache sits in front of both and invalidation has its own timing. The number that matters is not the average, it is the worst case somebody experiences while refreshing and waiting. Scheduled publishing separates the two models more sharply than immediate publishing does: a build-triggered pipeline needs something to run at the appointed minute, while a runtime read needs a query that filters on a date and a cache that expires when it should. Different failure modes, one question, and what scheduling actually guarantees is where it is answered in full.
What leaving costs under each model
With files in a repository, the exit artefact is the repository. You already hold the content in a readable format with its history attached, so migration is a transformation of shape rather than a recovery of bytes. With a service, exit is an export, and the fidelity of that export is the whole question: whether relations survive it, whether unpublished drafts are included, whether revision history comes with it, whether media URLs keep resolving once the account closes. Run the export before you need it, because an export you have never tested is a belief rather than a plan.
Now the honest qualification, because this is the point a file-based advocate most often overstates. Portability is real and it is usually over-weighted. The cost of moving content systems is dominated by rebuilding the editorial workflow and retraining the people who use it, not by extracting the text. Exit cost is a tiebreaker rather than a decider. It becomes a decider in one situation: when the content is a record you may be required to produce, intact and attributable, years after the vendor relationship ends.
Hybrid arrangements and the seams they introduce
The common hybrid puts structured, high-churn, relational data in a service and long-form editorial in the repository. A narrower one keeps text in the repository and media in an object store. Both are frequently correct, and both should be entered with the seams named in advance rather than discovered.
A seam is any place where two systems can disagree about what is true, and three are worth naming before you commit. Identity: the same entity now has two identifiers, and something has to decide which is canonical when they diverge. Ordering: when a sequence is assembled from both sources, one system has to own the order and the other has to accept it. Freshness: a build can bake yesterday's service data alongside today's prose and produce a page that is internally inconsistent while being individually correct in both halves.
Preview is where hybrids most often fail in a way editors notice. A preview is only useful if it shows both halves as they will actually appear, which means the preview path has to read draft state from both systems, not only from the one the writer happens to be looking at. Building that is a real piece of work, and it is routinely deferred until after launch, by which point the editorial team has already learned not to trust preview.
There is a structural cost as well. The build becomes the reconciliation point between two systems, so a failed build stops being a developer inconvenience and becomes an editorial outage. And a new colleague in their first week asks where a given piece of content lives; under a hybrid the answer takes a paragraph rather than a sentence, and every ambiguity in that paragraph will eventually be resolved wrongly by somebody in a hurry.
The disqualifying condition for a hybrid is ownership. If no single person or team is accountable for what happens when the two systems disagree, do not build it. An unowned seam does not stay still. It accumulates small manual reconciliations until the reconciliations are the system.
When both answers are wrong and the real problem is editorial
Some teams are choosing where content lives for the third time in four years, and each migration was justified on its own terms. That pattern is worth interrupting, because storage is not what is failing.
- The publishing rate has not moved across two migrations, which means the constraint was never the store.
- The bottleneck is that a particular approval never arrives, and no tool has authority over a person's calendar.
- The content is stale because nobody is accountable for reviewing it on a cycle.
- There are four hundred entries and most of them should be deleted, which is a decision nobody wants to sign.
- Every conversation about the tool is more comfortable than the conversation about who is allowed to decide what gets published.
The diagnostic is cheap. Write down the last ten things that failed to publish on time and the actual reason for each. If none of the ten reasons is a limitation of the tool, replacing the tool is deferred work presented as progress, and the eight axes above will produce an answer that changes nothing you care about.
Replacing the store is the most satisfying way to avoid deciding who is allowed to say no.
The opposite error exists and deserves equal weight. Some teams have decided that every tooling complaint is a process complaint in disguise, and they persist with a store that genuinely obstructs the work: an editor that mangles structure, an approval step that cannot refuse anything, a publish that takes a working day. If your list of ten does name the tool repeatedly, and names it in the same way each time, that is evidence and it should be acted on rather than reframed.
So the method, stated once and plainly. Take the eight axes. Find the ones where a disqualifying condition actually fires against your project as it exists now. If exactly one fires, it decides, and the remaining seven are commentary. If several fire in opposite directions, you are looking at a hybrid, and you should read the seams above again before committing to it. If none fires, both models will serve you, and you should choose on the editing experience the people who use it daily will actually have, because that is the difference they will feel every week and the axes will not.
Then treat the verdict as provisional in a specific way. Revisit it when relational depth increases, when media weight changes shape, or when a consumer appears that you cannot rebuild on demand. Do not revisit it on a schedule, and do not revisit it because a comparison article was published. Those three are the inputs that genuinely change the answer, and if none of them has changed, neither has the answer.
Can non-developers use a git-based CMS?
Yes, provided an editing layer stands between the person and the repository, and provided that layer hides the version control model completely. The editor should never see a branch name, never resolve a merge conflict, and never be asked to write commit syntax or hand-edit a structured header. If any of those leak through, the arrangement is not a CMS for non-developers, it is a repository with a form on top, and it will be abandoned. One thing the layer must still expose is a truthful statement of whether the change has actually reached the live site yet. Saved, approved and published are three different states, and collapsing them to reassure the editor is the single worst thing such a layer can do.
What happens to images in a git repository?
They enter history and stay there. Each revision of an image writes a new object addressed by its contents, and the earlier object remains reachable from earlier commits, so repository size grows with every version of every asset and is not recovered by deleting the file later. Deletion is simply another commit. Compression does not help either, because already-compressed formats such as JPEG, PNG, WebP and MP4 do not delta usefully against their own previous versions. Two live alternatives exist: keep media outside the repository in an object store and commit only references, or keep the binaries in history deliberately using a large-file extension, with the growth planned for at the start rather than discovered when a fresh clone starts taking minutes.
Relevant Zealsync pages


