# Host compatibility

> Which features each host supports across Hyperclay, HTML Clay, Hyperclay Local, and your own server, and how ClayJS discovers host capabilities.

Source: https://clayjs.com/docs/compatibility/

ClayJS runs in the browser, while a host serves the HTML file and accepts saves. Every ClayJS document works across [Hyperclay](https://hyperclay.com/docs/), [HTML Clay](https://htmlclay.com/docs/), [Hyperclay Local](https://hyperclaylocal.com/docs/), and [your own server](https://clayjs.com/docs/endpoint/), 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](https://clayjs.com/docs/endpoint/)) |
|---|---|---|---|---|
| **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](https://clayjs.com/docs/conditional-saves/)** | Yes | Yes | Yes | When `/_/meta` announces `conditional` and returns `document.saveId` or `save-history` |
| **[Live sync](https://clayjs.com/docs/plugins/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](https://clayjs.com/docs/plugins/page/) 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`](https://clayjs.com/docs/plugins/wire/) and [`ai-edit`](https://clayjs.com/docs/plugins/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](https://clayjs.com/docs/plugins/data/) (`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](https://clayjs.com/docs/plugins/upload/) (`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](https://clayjs.com/docs/guide/hosts/#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](https://clayjs.com/docs/plugins/people/), 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:

```json
{
  "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](https://clayjs.com/docs/conditional-saves/)). |
| `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()`](https://clayjs.com/docs/plugins/data/) to read the saved file as JSON. |
| `data-write` | Allows [`clay.writeData()`](https://clayjs.com/docs/plugins/data/) to write JSON into the saved file. |
| `upload` | Allows [`clay.upload()`](https://clayjs.com/docs/plugins/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](https://clayjs.com/docs/plugins/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`](https://clayjs.com/docs/plugins/page/). |
| `page` | Enables host-backed merge state for the [`page`](https://clayjs.com/docs/plugins/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`](https://clayjs.com/docs/plugins/people/). |
| `wire` | Enables [`clay.wire`](https://clayjs.com/docs/plugins/wire/) and reads `document.helpers` (`{ name, state }` with state `ready`, `denied`, or `unavailable`). [`ai-edit`](https://clayjs.com/docs/plugins/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 `GET` with `If-Match` set to the ETag that save carried, `Document-URL` set to `window.location.href`, `cache: "no-store"` and `redirect: "manual"`.
- `428` means no condition was sent, `412` means another version of the document is stored, and `404` means the host keeps no page state for these bytes. Each of them, and every other failure, keeps the journal exactly as it was.
- A `2xx` answer carries `version` at 1, `documentId`, `generation`, `etag`, `stateHash` and `tag`. 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 matches `stateHash`, and when that state projects to the article the save sent.

```json
{
  "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](https://clayjs.com/docs/edit-mode/) and sends requests to `/_/save/{token}`, `/_/meta/{token}`, and `/_/upload/{token}`. (If a host sends only the legacy pre-1.9.0 `htmlclaytoken` attribute, ClayJS logs a warning and keeps edit mode off.)
- **`documentetag`**: Seeds the initial `ETag` stamp for the version of the file that was served.
- **`sitevisibility`**: Sets `clay.siteVisibility` to `"private"` or `"public"` (or `null` when absent).
