- ClayJS
- Reference
- Hosts and protocols
- Host compatibility
Host compatibility
Which features each host supports across Hyperclay, HTML Clay, Hyperclay Local, and your own server, and how ClayJS discovers host capabilities.
ClayJS runs in the browser, while a host serves the HTML file and accepts saves. Every ClayJS document works across Hyperclay, HTML Clay, Hyperclay Local, and your own server, and ClayJS discovers which optional features the current host supports at runtime.
A plain static server or a file:// URL can open a document in view mode, and the demo plugin can simulate saving into browser storage, but saving back to the file requires a host that implements POST /_/save.
Feature support by host#
| Feature | Hyperclay | HTML Clay | Hyperclay Local | Your own server (Host it yourself) |
|---|---|---|---|---|
| Who edits | Owner, team editors, edit-link holders | The local user | The local user, plus teammates through device sync | Whoever your server allows (set window.clayEditMode = true in the page) |
Saving (POST /_/save) | Yes | Yes | Yes | Yes (the only required route) |
| Max save size | 5 MB | 50 MB | 20 MB | Up to your server |
| Conditional saves and receipts | Yes | Yes | Yes | When /_/meta announces conditional and returns document.saveId or save-history |
Live sync (sync plugin) | Every editor and viewer | Open tabs on this computer | Open tabs on this computer, plus other devices via sync | When your server implements /_/sync and announces sync |
| Outside disk edits reach open tabs | No disk except through device sync | Yes (agents, text editors, Git) | Yes | When your server watches disk and broadcasts on /_/sync |
| Presence | Avatar stack (no JS API) | None | None | When /_/meta announces presence |
by on sync frames and editing marker | Yes | No | No | When /_/meta announces presence-editing |
Live stores (live plugin) | Every editor at full speed | Tabs in one browser at full speed | Tabs in one browser at full speed; other devices at save speed | Tabs in one browser at full speed, or across clients with live-data |
| Page editor merge state | Kept beside the file | Shared across open tabs; rebuilt from the article on fresh open | Kept beside the file | When /_/meta announces page |
| Page recovery | Yes | Yes | Yes | When /_/meta announces page-recovery-v1 and names document.pageState.href |
wire and ai-edit | ai-edit only, for owners and team editors on a paid plan | Both wire and ai-edit | Both wire and ai-edit | When /_/meta announces wire and lists helpers in document.helpers |
Data API (readData / writeData) | Read only (data-read) | Read and write (data-read, data-write) | Read and write (data-read, data-write) | When /_/meta announces data-read and data-write |
Uploads (upload plugin) | 10, 20, or 100 MB by plan, in account uploads/ | 25 MB, in ~/htmlclay/uploads/ | 25 MB, in the folder’s uploads/ | When /_/meta announces upload and your server implements POST /_/upload |
| Works offline (Offline in a local host) | No | Yes: one bundled ClayJS release at /_/clayjs/<version>/, including older Journals’ exact library and font URLs | Yes, the same | When your server bundles a release the same way |
| Version history | One version per save (reload open editor tabs after restoring) | Yes | 60 days, newest 20 versions | Up to your server |
How ClayJS learns what a host supports#
ClayJS never infers host capabilities from the hostname, port, or trial requests. Instead, it reads capabilities from the GET /_/meta discovery endpoint and from attributes the host stamps on <html> when serving the page.
Capability discovery (GET /_/meta)#
When a feature needs to know what the host supports, ClayJS sends a GET request to /_/meta (or /_/meta/{token} when <html savetoken> is present) with the Document-URL header set to window.location.href, cache: "no-store", redirect: "manual", and a 6-second timeout. In visitor draft mode, ClayJS skips discovery and treats the host as a bare core host immediately.
Only a 2xx JSON response with a numeric spec property is accepted as a capability document:
{
"spec": 1,
"extensions": ["conditional", "save-history", "sync", "upload", "data-read", "data-write", "wire", "people"],
"document": {
"etag": "\"8f02c16e7b4d9a30\"",
"saveId": "b3f1c8e2-...",
"upload": { "maxBytes": 26214400 },
"helpers": [{ "name": "ai-edit", "state": "ready" }],
"people": { "me": { "id": "u1", "name": "Ada" } }
}
}
If /_/meta returns 404, a redirect, an HTML error page, or any non-JSON body, ClayJS treats the server as a bare core host ({ spec: null, extensions: [], document: null }) that supports only POST /_/save. Discovery never throws or rejects.
Extension names in extensions#
| Extension | What it enables in ClayJS |
|---|---|
conditional | Sends If-Match on saves and handles HTTP 412 conflicts (see Conditional saves and receipts). |
save-history | Queries /_/meta with Save-ID and Save-Request-Hash (document.saveOutcome) after a timed-out save, and enables history for clay.live. |
data-read | Allows clay.readData() to read the saved file as JSON. |
data-write | Allows clay.writeData() to write JSON into the saved file. |
upload | Allows clay.upload() to post files to /_/upload (when document.upload is also present, pre-checking document.upload.maxBytes). |
sync, sync-worker, sync-worker-data | Selects /_/sync as the live sync transport (without a spec response, sync falls back to legacy /_/live-sync/save and /_/live-sync/stream). |
presence, presence-editing | Enables the presence avatar stack and active-block editing outlines in sync. |
live-data, live-provenance | Enables host relay and provenance for clay.live and page. |
page | Enables host-backed merge state for the page plugin. |
page-recovery-v1 | With document.pageState.href, reads the page state stored for the exact saved bytes and may retire journaled page work a proven checkpoint covers; without either, the records stay. |
people | Reads document.people for clay.me and clay.people. |
wire | Enables clay.wire and reads document.helpers ({ name, state } with state ready, denied, or unavailable). ai-edit activates when document.helpers lists "ai-edit" as ready or unavailable. |
The page-state resource#
document.pageState.href names the route that returns the page editor’s stored Yjs state for the exact bytes a client accepted. ClayJS asks it only when an accepted save’s own capture cannot prove a checkpoint, and it asks conditionally:
- The request is a
GETwithIf-Matchset to the ETag that save carried,Document-URLset towindow.location.href,cache: "no-store"andredirect: "manual". 428means no condition was sent,412means another version of the document is stored, and404means the host keeps no page state for these bytes. Each of them, and every other failure, keeps the journal exactly as it was.- A
2xxanswer carriesversionat 1,documentId,generation,etag,stateHashandtag. ClayJS accepts it only when the document identity, the generation and the ETag match the ones it accepted, when the digest of the decoded state matchesstateHash, and when that state projects to the article the save sent.
{
"version": 1,
"documentId": "b3f1c8e2-4d1a-4f8e-9c2b-7a6d5e4f3c21",
"generation": "9c1f4a7e2b8d4c6f0a3e5d7b9c1f2a4e",
"etag": "8f02c16e7b4d9a30",
"stateHash": "5f0c1d8a3b7e94f2c6d05a1e8b3f7c9d2e4a6b8c0d1e3f5a7b9c1d2e3f4a5b6c",
"tag": "{\"v\":1,\"g\":0,\"y\":{\"seed\":\"...\"}}"
}
Hyperclay and Hyperclay Local answer /_/page-state under the document lock, and HTML Clay answers /_/page-state/{token} for the token in <html savetoken>. Hyperclay names its document row id and HTML Clay the id behind its history key; Hyperclay Local mints no document id at all and answers documentId: null.
Host-injected attributes on <html>#
A host can also stamp three attributes onto the root <html> element when serving a document. ClayJS reads them once at boot and strips all three before every save:
savetoken: Arms edit mode and sends requests to/_/save/{token},/_/meta/{token}, and/_/upload/{token}. (If a host sends only the legacy pre-1.9.0htmlclaytokenattribute, ClayJS logs a warning and keeps edit mode off.)documentetag: Seeds the initialETagstamp for the version of the file that was served.sitevisibility: Setsclay.siteVisibilityto"private"or"public"(ornullwhen absent).