Docs · Host it yourself

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:

PartValue
RoutePOST /_/save
BodyThe file's full HTML, as plain text. Always: this route has exactly one body shape, on every host.
HeaderDocument-URL: the full URL of the page being saved.
Success200 with JSON { "msg": "Saved" }
FailureAny 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>.