Chapter 02Understand
How it works
Two flows, two coupling points, eleven rules that are not up for debate. Enough to read the code without getting lost, and to know straight away where a change belongs.
The read flow
This is the visitor's flow, and it is deliberately short: everything happens at build time, nothing at runtime.
A visitor receives an already complete HTML file. No network call fetches content, no database is queried, and JavaScript is required for no displayed word. A public page carries about 175 bytes of inline script, which read a cookie and do nothing else — see “the editing bootstrap” below.
The write flow
This is the flow of a client changing their page. It is longer, and every step has a role.
-
The overlay sends the whole page content
Not one field, not a diff: the entire JSON file as it should be after the change, along with the version read when the page was opened.
-
The function refuses in a precise order
Rate, then declared size, then identity, then received size, then shape, path, schema, lock, write. The order is not cosmetic: a size cap placed after reading the body protects nothing, and refusing a caller who keeps hammering does not require knowing who they are.
-
The same Zod schema as the build validates the content
Not a copy of the schema: the same object, imported from the same file. Content that would fail the build never enters the repository.
-
The optimistic lock compares versions
If the file changed in the repository since the page was opened, the write is refused with an explicit conflict — never a silent overwrite.
-
A commit, then a rebuild
The commit is attributed to
EDITOR_NAME/EDITOR_EMAIL. The host detects the push and rebuilds the site: thirty to sixty seconds.
The repository is split in two
This is the decision that keeps operations sustainable beyond three clients. What is identical from one site to the next is shared and versioned; what belongs to the site is copied then adapted.
| Where | Per site | |
|---|---|---|
| Overlay, content model, components, server routes, security | packages/inline-core |
versioned dependency |
| Content, theme, layout, languages, route adapters | the site project | created once, then adapted |
Anything in inline-core is never copied into a site. A business
rule that shows up in /functions/api is a rule in the wrong place: on the day
of a fix, it will have to be found again in ten repositories.
The two coupling points
Two things, and only two, depend on an outside provider. They are isolated behind an interface, in two files:
packages/inline-core/src/server/git-provider.ts readFile / writeFile (GitHub | GitLab)
packages/inline-core/src/server/auth.ts verifyAuth / createSession (site key | delegated)No identity check and no Git API call exists anywhere else in the code. That is what makes it possible to change host in a few hours, to move from GitHub to GitLab by writing one implementation, or to switch to multi-user authentication without touching the rest.
The eleven absolute rules
They take precedence over everything else. They are repeated here because they are rarely broken out of conviction, and often out of reflex.
| # | Rule | Why |
|---|---|---|
| 1 | output: 'static' — never SSR, never hybrid | It is what guarantees all content is in the served HTML. |
| 2 | No editable zone inside a hydrated component | client:only removes content from the index; client:load re-renders it on load and wipes the edits in progress. |
| 3 | The Git token never leaves the server | It belongs to the agency, not the client. Never in a response, a served file or browser storage. |
| 3 bis | The site key is verified server-side only | A hash served to the browser can be attacked offline. |
| 4 | No secret in the repository | Environment variables of the server function, and nowhere else. |
| 5 | No free-form styling | Styles go through the Zod enums. No hex, no pixels in content. |
| 6 | Do not use src/pages/api/* | In static output those run at build time, not per request. Everything goes in /functions. |
| 7 | Every write is validated server-side | Identity, schema, path, size, sanitising. Client-side validation counts for nothing. |
| 8 | No technical jargon in the interface | “Publish”, not “Commit”. The client will never see the code. |
| 9 | No content tree in the overlay | That is the moment the tool becomes a CMS again and the client gives up. |
| 10 | Videos are never uploaded | A heavy file in Git breaks the repository and the builds. Provider + id, nothing else. |
| 11 | Do not widen the scope | Out-of-scope requests are flagged, not implemented. |
Four mechanisms worth knowing
The schema lives in a neutral file
The write function cannot import astro:content: it runs outside the Astro
context. So the Zod schema is isolated in inline-core/src/schema.ts, which
imports nothing from Astro. The build and the write validate with the same object,
not with two copies that would eventually diverge.
The editing bootstrap, and why it grants nothing
Every page carries about 175 bytes of inline JavaScript: it reads the inline_edit
cookie and, if present, appends a <script> tag pointing at the overlay. A
public page therefore never downloads the editor.
The inline_edit cookie contains no secret. Forging it only shows an interface
whose writes will all be refused server-side: routes check the signed session
cookie, never the marker. Editing the DOM without a key is no more consequential than using
the browser's developer tools.
The optimistic lock
On open, the overlay reads the file through /api/content and receives a
version — on GitHub, the blob SHA. That version travels back when publishing. If it
no longer matches, publishing is refused and a message invites a reload. Nothing is
overwritten, and the local draft is not lost.
Verifying a key is expensive on purpose
About 350 ms: that is the cost of argon2id at recommended parameters, and it is what makes
brute force impractical. That cost is paid at sign-in only, once per 8-hour window. On a host
billing CPU time, check that your plan covers that duration on /api/auth.
The four routes
| Route | Role | Identity | Rate |
|---|---|---|---|
POST /api/auth | Opens a session from the key | — | 5 / 15 min |
GET /api/content | Reads the repository state and version | yes | 60 / 5 min |
POST /api/save | Validates and publishes a page | yes | 30 / 5 min |
POST /api/upload | Stores an image in the repository | yes | 30 / 15 min |
Full detail of requests, responses and error codes: Route reference.
What does not exist
- No database. The Git repository plays that role.
- No application server. Four stateless functions, called on write only.
- No server-side session. The cookie is signed; nothing is stored.
- No user accounts. One key per site.
- No cache to invalidate. The HTML is rebuilt, therefore replaced.
Next, in practice: Local setup.