The endpoint spec
The one route a host implements, and a complete server in a couple dozen lines of Express.
A clayjs server implements one route:
| Part | Value |
|---|---|
| Route | POST /_/save |
| Body | The file's full HTML, as plain text. Always: this route has exactly one body shape, on every host. |
| Header | Document-URL: the full URL of the page being saved. |
| Success | 200 with JSON { "msg": "Saved" } |
| Failure | Any non-2xx. The status is authoritative; host-generated failures use JSON { "msg": "why" }, but clients tolerate non-JSON intermediary errors. |
A complete server in a couple dozen lines of Express:
import express from "express";
import fs from "node:fs/promises";
import path from "node:path";
const root = process.cwd();
const app = express();
app.use(express.static(root));
app.use(express.text({ type: "*/*", limit: "10mb" }));
app.post("/_/save", async (req, res) => {
try {
const origin = req.get("Origin");
if (origin && new URL(origin).host !== req.get("Host"))
return res.status(403).json({ msg: "Cross-origin save refused", code: "forbidden" });
const page = decodeURIComponent(new URL(req.get("Document-URL")).pathname);
const target = path.join(root, path.normalize(page.endsWith("/") ? page + "index.html" : page));
if (!target.startsWith(root + path.sep))
return res.status(403).json({ msg: "Outside the folder", code: "forbidden" });
if (!/^\s*<!doctype html>/i.test(req.body))
return res.status(422).json({ msg: "Not a complete HTML document", code: "invalid-document" });
await fs.writeFile(target, req.body);
res.json({ msg: "Saved" });
} catch {
res.status(500).json({ msg: "Couldn't save" });
}
});
app.listen(4600);
The server half is only half of it: on your own host nothing arms edit mode for you, so a page served this way is read-only until you set window.clayEditMode before the script tag. A real deployment adds authorization and atomic writes. The same contract powers hyperclay.com, Hyperclay Local, and HTML Clay. Two details you may ignore: every save also carries a Save-Trigger: user|auto header (whether a person triggered it), and HTML Clay uses a token variant, POST /_/save/{token}, read from <html savetoken>.