Imported from closeio/click (
examples/pong/AGENTS.md). Install upstream withnpx skills add closeio/click --skill pong. Copyright stays with the author.
Iterating on this Click site
This folder is a static site for Click, the internal static-hosting
platform: deploy the folder and it is live at https://pong.click.<domain>/.
It was generated by click new from this prompt:
create a Pong game. Make sure it works
Treat that prompt as the starting point, not the spec.
To keep evolving the site, edit the files in this folder directly and
redeploy — never run click new over this folder again; it regenerates from
scratch and would overwrite every change made since.
Workflow
- There is no build step and no dev server: these files are served exactly as-is. Edit the HTML/CSS/JS, then redeploy.
- Deploy with
click deploy . --name pong(run from this folder). Deploys are an rsync-style sync: changed files are uploaded, and files deleted locally are removed remotely. - The pages call the platform's APIs on their own origin, so identity, the
database, and realtime only work on a deployed site (or the local stack) —
opening
index.htmlfrom disk won't have them.
Rules the site must keep following
- Static files only: HTML, CSS, vanilla JavaScript (ES modules are fine). No build steps, no npm, no external CDNs or fonts — fully self-contained.
- The entry page stays at
index.htmlin the site root. Link between folders with trailing slashes (the platform serves/docs/but 404s/docs); there is no SPA fallback. - Never add files under
api/,db/, oroauth2/— those are reserved platform prefixes, and deployed files there are shadowed. - ALL application state lives in the per-site document database
(
/api/db/<collection>CRUD) — never localStorage, never in-memory only. - Keep the realtime EventSource lifecycle intact: one stream per page, closed
on pagehide and while
document.hidden, reopened (with a re-query) on return. - Keep loading/empty/error states, non-blocking confirmation for destructive actions (no native alert/confirm/prompt — they freeze the page), and both light and dark color schemes.
The platform API guide follows — identity, the document database, realtime events, and read-only SQL. Follow its patterns exactly.
Building a site on Click — the platform APIs
You are writing a static site that will be deployed to https://<name>.click.<domain>.
This page lists everything that the platform gives your site on its own
origin. There is no server to run, no CORS, and no API key in your code.
Plain fetch() from your page is the whole integration.
For AI assistants and agents: this file is written to go into your context. If you build a Click site in a repository, reference this file from the
CLAUDE.mdorAGENTS.mdof the project. For example: "This is a Click site — the platform APIs available to pages are described in docs/site-api.md of the closeio/click repo". Everything below is available on the own origin of the site at runtime.
The model in one paragraph
Your site is a folder of static files, served at https://<name>.click.<domain>.
Every visitor is an already-authenticated employee: the auth proxy of the
platform signs them in before your first byte is served, so your page never
handles a login. On the same origin, the platform mounts shared APIs under
/api/ and /db/: identity, a per-site document database, a realtime change
stream, and read-only SQL. To deploy, run click deploy <dir> --name <name>
(the README
covers CLI setup).
Who is viewing the page
const me = await fetch("/api/identity").then(r => r.json());
// { "email": "vic@example.com", "name": "Vic", "authenticated": true, "site": "myapp" }
In local open-mode development, authenticated is false and the identity is
dev@localhost. Build pages that degrade well in that case.
The document database
Schemaless JSON collections, scoped to your site. Other sites can never read them. Collections are created implicitly on the first insert.
| Call | Effect |
|---|---|
GET /api/db |
list your collections → ["notes", …] |
GET /api/db/{coll}?limit&offset |
list documents, newest first (limit default 50, max 200) |
POST /api/db/{coll} |
insert a JSON object → 201 + the document |
GET /api/db/{coll}/{id} |
fetch one document (404 if missing) |
PUT /api/db/{coll}/{id} |
replace a document's data |
DELETE /api/db/{coll}/{id} |
delete → 204 |
DELETE /api/db/{coll} |
drop the whole collection → {"deleted": n} ⚠ owner-only |
DELETE /api/db |
purge all site data → {"deleted": n} ⚠ owner-only |
Documents come back as {id, data, created_at, updated_at}:
// write
const doc = await fetch("/api/db/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ author: me.name, text: "hi" }),
}).then(r => r.json());
// { "id": "641e84a3-…", "data": { "author": "Vic", "text": "hi" },
// "created_at": "…", "updated_at": "…" }
// read (newest first)
const notes = await fetch("/api/db/notes?limit=50").then(r => r.json());
These rules will bite you if you ignore them:
- Bodies must be JSON objects, not arrays or scalars, and 1 MiB or smaller.
- Collection names must match
[a-z0-9][a-z0-9_-]*. Other names return 400. - Errors are JSON:
{"error": "human-readable message"}. - Data is shared per site, not per user. Every signed-in employee who can
open your site can read and write its documents. Store only data that any
colleague can read. Use
/api/identityto label writes, not to secure them. - The two ⚠ bulk-destructive calls need the owner of the site or a granted
collaborator (
click share). Every other call is open to any employee. - The whole-site purge (
DELETE /api/db) is a lifecycle operation. It also releases ownership of the site name, and it is whatclick deletecalls. The next authenticated deploy claims the name again.
Realtime: react to changes live
GET /api/db/events is a Server-Sent Events stream of the document changes of
your site. Events carry metadata only, so re-query to get the data:
const events = new EventSource("/api/db/events");
events.onmessage = (e) => {
const ev = JSON.parse(e.data); // { site, type, collection, id }
if (ev.collection === "notes") reloadNotes();
};
type is insert | update | delete | drop | purge. EventSource reconnects
automatically. The platform also ends a stream after about 30 minutes as a
backstop, and that reconnection is transparent. This is how the guestbook
example updates across browser tabs. Read examples/guestbook/app.js for the
complete working pattern.
⚠ CAUTION: Close the stream when your page is not showing. An open
EventSourceholds one browser connection for the whole lifetime of the page. It holds it in the back/forward cache too, after the user navigates away. Over HTTP/1.1 the browser allows only about 6 connections per host. A multi-page site that subscribes on every page and never closes holds all six sockets. The next navigation then hangs the entire site. Close the stream onpagehideand whendocument.hiddenis true. Reopen it and re-query on return.subscribe()inexamples/demo/assets/api.jsis the reference implementation of this lifecycle.
Read-only SQL (libSQL backend)
When the platform runs the per-site database backend, your site owns a real SQLite database. Your page can then query it with SQL, read-only, through a short-lived token minted for your site:
import { createClient } from "@libsql/client/web";
const { url, authToken } = await fetch("/api/db-token").then(r => r.json());
const db = createClient({ url, authToken }); // scoped to YOUR site's DB, read-only
const { rows } = await db.execute({
sql: "SELECT id, data, created_at FROM documents WHERE collection = ? ORDER BY created_at DESC LIMIT 20",
args: ["notes"],
}); // note: no `site` column anywhere — the database itself is your site
The schema behind /api/db is one table:
documents(id TEXT PRIMARY KEY, collection TEXT, data TEXT /* JSON */, created_at TEXT, updated_at TEXT).
Use json_extract(data, '$.field') from SQLite to query into documents.
Tokens expire after about 1 hour. If a query fails with an authentication
error, fetch /api/db-token again. All writes still go through /api/db:
the database engine itself enforces the token as read-only. On a platform
without this backend, /api/db-token returns 501. Feature-detect that and
fall back to /api/db.
Static serving rules
- Folder URLs resolve to
index.html, but only with a trailing slash./docs/works and/docsreturns 404, because there is no redirect. Write your links accordingly. - There is no SPA fallback. An unknown path is a 404, not
/index.html. Use real files or hash routing. - Use URL-safe filenames. Keys with spaces or special characters are not re-escaped when proxied.
/api/,/oauth2/, and/db/are reserved prefixes. The platform shadows files that you deploy under those paths.- Content-Type comes from your file extensions at deploy time.
Everything at a glance
| You want | Use |
|---|---|
| Know who is viewing | GET /api/identity |
| Store / read JSON | /api/db/{collection} CRUD |
| Live updates | EventSource("/api/db/events") |
| SQL queries (read-only) | GET /api/db-token + @libsql/client/web |
| List all sites on the platform | GET /api/sites |
| Deploy / share / tear down | the click CLI (deploy, share, delete) — see the README |
Working references: examples/hello (identity), examples/guestbook
(documents and realtime), examples/dashboard, and examples/demo.
examples/demo is the everything example: full CRUD, realtime, SQL with the
/api/db fallback, ownership management, and the sites directory across three
pages.