ADR 0007 — What cratly asks of its host, and where that could be answered differently
- Status: Proposed
- Date: 2026-09-13
- Context repos: cratly.io site (this repo),
editor
Problem
The editor is a browser application with no backend: what it can do, it does as the signed-in user against a GitLab instance. That has carried the product a long way — the review workflow, the drafts, the build, the preview are all GitLab's, and none of them had to be written.
It also means one supplier decides what cratly can offer. Three pressures make that worth examining now:
- A shared runner fleet is shared with everything else on it. Site builds are short — ten to fifteen seconds — but they queue behind work that has nothing to do with any site, and a build waiting for a free runner is indistinguishable, from the outside, from a build that is slow.
- Not every customer has GitLab, and of those who do, not every one wants the editor to reach it.
- A general-purpose CI host cannot be told what it is building. Every site build is the same shape — install the adapter and a static-site generator, render, publish — and a CI fleet treats it as an arbitrary script. What follows is work that has to be done over and over per site: keeping a build image warm with the right packages, telling the dependencies a build needs from the tooling it does not, routing jobs at runners that are not busy with something else. Measured on one site, the difference between getting that right and not was a build of 34 seconds against one of 174 — the same commit, the same output. A host that knew it was building a cratly site would not need to be configured into knowing it.
The naive reading of "make the host exchangeable" is that cratly needs somewhere to keep files. Counting what the editor actually asks for says otherwise. Its GitLab service has 24 methods:
| What it asks for | Methods |
|---|---|
| Merge requests — open, read, update, merge, rebase, merge-base, delete, their pipelines | 9 |
| Branches — create, delete, list, compare, protection state | 7 |
Files and commits — getFile, getTree, commit | 3 |
| Pipelines and artefacts — read jobs, retry, download the archive | 3 |
| Session — current user, project | 2 |
Storing the file tree is three of them. The other twenty-one are a review workflow and a build. An interface offering a file tree and nothing else would replace the easy eighth and leave the rest unbuilt — which is the trap this decision exists to name.
A second finding belongs here: GitLab does not sit behind one seam today. Besides services/gitlab.js, the auth store, the repo store, useMergeFromMain, lib/connections and two views address api/v4 or the instance URL directly.
Decision
The contract is written in cratly's own concepts, not in GitLab's API.
Four of them, and a site needs all four answered:
- A file tree, and everything that happens to one: reading it, writing a set of changes as one atomic unit, moving and deleting within it, and validating what a change would produce before it is written. This is the centre — every other concept refers to a state of this tree.
- A draft — a named, isolated state of the tree that a person works in without touching what is published, and that can be compared against what is.
- A review — a draft offered for publication, with the discussion and the decision recorded against it, and the means to bring it up to date with what has been published meanwhile.
- A build — a draft or the published state turned into a site, with a status that can be polled, a report that can be read (see ADR 0006), and a result that can be served as a preview. The contract asks for a built site, not for a machine that will run a script: what a host does to produce one — which environment it keeps warm, how it schedules the work — is the host's business, and the place where an implementation built for this one workload can be markedly better than a general fleet configured to approximate it.
Authentication is separate from all four and is expressed as OIDC, so a host is named by an issuer rather than by a product.
GitLab becomes one implementation of that contract — branches for drafts, merge requests for reviews, pipelines for builds — rather than the shape the editor is written in. A bespoke service is another: it can answer the same four far more directly, because it need not be a git host to do so.
Which concepts may be answered apart
The four are not equally bound to each other. A draft is a state of the tree, and a review is a draft offered against what is published; neither means anything without the tree it refers to. File tree, draft and review therefore come from one implementation.
The build may come from another. It asks for nothing but a state of the tree, and it answers with something the tree does not contain — a site, a status, a report, a preview. A site may keep its content, drafts and reviews on one host and have them built by another: a GitLab or a Forgejo instance for the content, say, and a service made for building sites for the build.
A build answered apart takes on three obligations the combined case gets for free:
- It learns of changes from the tree's host, rather than being told by the editor. A change written by other means — a developer pushing a commit — is built all the same, and a build never depends on a browser that happened to be open.
- It reads the tree with a credential of its own: read-only, not tied to any person, and not one that expires on a schedule someone has to keep. It never acts in the name of whoever wrote the change.
- It accepts the identities of the tree's issuer. Whoever is signed in to the editor is signed in to the build as well; a second login for the same person is not an implementation detail but a defect.
Which implementation answers the build is part of the editor's connection to a site, next to the host it connects to — not part of the site's content, which must not have to name the machine that builds it.
Before any of that, the seams are collected. Every direct reach for api/v4 or an instance URL outside the service layer is a place the contract cannot cover. That work is worth doing on its own, whatever is decided about a second implementation.
What this does not decide
Whether a bespoke host for the tree gets built, and whether it stores its trees in git at all. Naming the contract is what makes that a question that can be answered later rather than one that has to be answered first.
How a bespoke build is answered is the subject of ADR 0008.
Consequences
- The editor's service layer is written against the contract, and its vocabulary changes from branches and merge requests to drafts and reviews. The documented workflow already speaks that way; the code does not yet.
- A second implementation is measurable against the first: whatever GitLab answers, the contract has to ask for.
- The runner pressure is not waiting for any of this. The published pipeline templates take
runner_tags, so a site can be built on runners kept for site builds — see the pipeline reference. - The editor's connection to a site names two implementations where it named one: the host of tree, drafts and reviews, and the one that builds. For a site built by its own host's CI they are the same.
- Where a host cannot answer a concept — a service with no review step, say — the editor has to say so rather than hide it. A capability a host lacks is not a feature the editor may silently drop.