ccc-pages: web pages with a memory
Technical chapter.pages raw
A page is a web address with a memory. The memory is a set of records and files that your agent writes; the page reads them and draws them. Nothing on a page is typed by hand: you tell your agent what you want to see, and it writes. When the numbers change, the agent rewrites a record and everyone who opens the page sees the new numbers. Nobody rebuilds the page, and nobody sends a new link.
That makes a page a cheap way to share something that keeps changing: an order sheet, a shift dashboard, a quote for one customer, a log of what the machines did today. A page can also take things in. A machine can post events to it, and a short script published with the page (handler.js) can compute totals or answer a form. The script runs on the page service at page.sui.pics, so you run no server.
Who can open a page is your choice. By default a page is unlisted: anyone who has the link can read it, and nobody can find it without the link. A private page shows only an unlock form until the reader opens it with a share link made for them, one link per person, and each link can be switched off on its own.
flowchart LR
A[your agent
in the terminal] -- "page-data put" --> D[(the page's records
/d/slug/key)]
A -- "page-file upload" --> F[(files
/f/slug/id)]
M[a machine or system] -- "POST /i/slug + secret" --> D
B[a browser] -- "POST /h/slug/…" --> H[the page's own API
handler.js]
H -- "reads" --> D
D -- "?raw=1 · ~view" --> Pg[the page
/a/slug/]
F --> Pg
Pg --> R[anyone with the link]
Seven live examples
Each example below is a real page, running now, with made-up data for a mask factory. Each one has a short explanation, a live box that reads the example page as you look at it, a Tell your agent text you can copy to have your own agent build the same thing, and a closed section on how it is built.
| Example |
What you see |
What it shows a page can do |
| Shift dashboard |
four headline numbers and fourteen days of output |
a dashboard whose numbers change when one record is rewritten |
| Order sheet |
a sortable table of orders |
a table whose rows are one record |
| A computed view |
order totals by status for the customer you pick |
a page that does arithmetic over its own records, with no server of yours |
| Webhook inbox |
the newest events that machines sent to the page |
machines and systems writing to a page with a secret |
| A page with its own API |
a one-line form; your line appears in the list |
a page that is a small backend |
| A private quote page |
a quote as one customer sees it |
private pages, one share link per customer, a quote that expires |
| This page |
the handbook's own latest change and the address of the text you are reading |
a page whose words are records; an edit lands without a rebuild |
After the examples come five ideas for a mask factory, each a text to hand your agent, and a closed chapter on how a page works inside, for the reader who wants the addresses and commands.
Where the detail lives
Your agent learns all of this from the ccc-pages skill, which the install puts on your computer. Every text on this route begins with "Load the ccc-pages skill", so the agent reads the skill before it builds anything.
Five ideas for a mask factory
Technical chapter.pages-ideas raw
The examples above use made-up data. These five are the same building blocks pointed at a real factory's work. Each is a text you can copy and give to your agent as it is; the agent asks you for what only you have (a file, a list of suppliers, a customer's name) and uses example data where you have none yet, saying so on the page. Start with the one closest to a question you are asked every week.
A word on real data: an ordinary page is unlisted, so anyone who has its link can read it. When a page carries customer names, prices or anything else you would not forward to a stranger, the texts below ask for a private page with one share link per person.
An order sheet from the ERP's export
Your order system can already export a spreadsheet file (CSV). The agent turns that file into a sortable, filterable order sheet your team opens on a phone, and a single command refreshes it from the next export.
Load the ccc-pages skill and make me an order sheet page fed by my ERP's CSV export.
Ask me for the path to the latest export. If I have none yet, make a made-up CSV of 30 mask orders (customer, sku, quantity, due, status) and say on the page that the rows are examples.
Use the data-table template with the data store on. Because the rows name real customers, publish the page `--private` from its first publish, and mint one viewer share link per colleague I name, labelled with their name; give me the links in chat only, never on any page.
Write a small script that reads the CSV and writes it as one record `orders` of schema `table@1`, and keep the script beside the page's files.
Show me: the page URL, the raw URL of the `orders` record, and the one command I run after each new export to refresh the sheet without republishing the page. Then run that command on a changed copy of the CSV and show me the page picked up the change.
A shift dashboard from the line lead's evening message
At the end of each shift the line lead already sends someone the numbers. Paste that message into your agent's window and the dashboard updates; the whole plant reads the same four numbers in the morning.
Load the ccc-pages skill and make me a shift dashboard page that I update by pasting the line lead's end-of-shift message to you.
Use the dashboard template with the data store on, and a record `metrics` of schema `metrics@1` holding four tiles (masks made this shift, first-pass yield %, orders open, next due date) and a 14-day series of daily output. Start it with made-up numbers and say on the page that they are examples until the first real message.
Write down the exact short message format the line lead should send, one number per line, so I can pass it on. From then on, when I paste such a message to you, update `metrics`: the four tiles, today's point in the series, and the "as of" time, and drop the oldest point so the series stays at 14 days.
Show me the page URL, the raw URL of `metrics`, and the message format. Then take this sample message and update the page from it: "Shift B, line 2: 18,240 masks, first-pass yield 97.8%, 42 orders open, next due Friday."
A supplier scorecard, refreshed monthly
Which supplier of melt-blown fabric, ear-loop elastic, nose wire or cartons delivered on time and to spec last month? The agent keeps one record per month, so the page shows this month's ranking and how each supplier is trending.
Load the ccc-pages skill and make me a supplier scorecard page, refreshed once a month.
Ask me for my suppliers and last month's figures. If I have none at hand, use made-up suppliers for melt-blown fabric, ear-loop elastic, nose wire and packaging cartons, and say on the page that they are examples.
Data store on. Keep one record per month named `scorecard.<yyyy-mm>` holding, per supplier: on-time delivery %, defects per million, lead time in days and price change %. The page lists the records with prefix `scorecard.`, shows the newest month as a table ranked by on-time delivery, marks each figure better or worse than the month before in words as well as colour, and shows a small trend per supplier across the months it has.
Because supplier prices are confidential, publish the page `--private` from its first publish and give me one labelled viewer share link for myself, in chat only.
Write three months of records, then show me the page URL, the raw URL of the newest record, and the command I run next month to add a month without republishing the page.
A quote page for each customer, with its own expiry
A customer opens a clean page with their quote on it, and the page tells them when the quote expires. Each person at the customer gets their own link, and you can switch off one person's link without touching anyone else's.
Load the ccc-pages skill and read its publishing reference on private pages. Make me a quote page for one customer that I can repeat for the next customer with one command.
Ask me for the customer's name, the line items (product, quantity, unit price), the payment and delivery terms and how many days the quote is valid. If I give none, use a made-up customer and say on the page that it is an example.
Use the markdown template, published `--private --data on` from its first publish. Store the quote as a `raw@1` record named `quote` whose `stale_after` is the end of the validity period; the page renders the quote with a line saying when it expires and, once it has expired, says so instead of showing the prices.
Mint one viewer share link per person at the customer that I name, each labelled with that person's name and expiring when the quote does; give me the links in chat only, never on any page.
Show me the page without a link (the unlock form) and with one, the command that switches off one person's link, and the command that makes the next customer's quote page from this one.
A machine event log fed by the MES
If your manufacturing execution system (MES) or a line controller can call a web address, it can send the page a short message whenever a machine stops, starts or runs low on material. The page keeps every event, and anyone on the floor can see what happened today and which machine stopped most.
Load the ccc-pages skill and read its data-and-endpoints reference on inbox ingestion and on computed views. Make me a machine event log page that my MES posts events to.
Data store on. Mint the page's inbox secret with `page-inbox secret`, keep it in a file with mode 600, and never show it to me in chat or on any page.
The page lists records with prefix `inbox.` newest first: event name (for example `machine.stop`, `machine.start`, `material.low`), machine, reason and time, with a filter by machine. Add a `handler.js` with a computed view `stops` that counts today's `machine.stop` events per machine, declared in `handler.json` as `{"views":["stops"]}`; publish, replay and activate it, and show those counts at the top of the page.
Post six made-up events with `page-inbox send`, each with its own delivery id, across three machines.
Show me the page URL, the output of `page-data get <slug> '~stops' --data-only`, and the exact curl my MES would run to post one event, with the secret shown as a placeholder and a delivery id so a retry is stored once.
How a page works inside
Technical chapter.pages-inside raw
This chapter is the technical model behind the examples: the addresses a page answers, what each one stores, and the commands an agent runs. The ccc-pages skill carries every detail; its references/data-and-endpoints.md and references/publishing.md are the two to read next. Every command below runs from a UCC seat, with BIN="$CCC_SKILL_DIR/ccc-pages/bin".
One slug, seven addresses
A page is named by its slug, a short stem plus 16 random hex characters (order-sheet-5063bca214b7f486). The random half is what keeps an unlisted page unfindable. Everything the page owns hangs off that one name on https://page.sui.pics:
| Address |
What it is |
Who writes it |
/a/<slug>/ |
the page itself: one HTML file or a file set, in immutable versions |
the owner, with page-publish |
/d/<slug>/<key> |
the store: one record per key |
the owner with page-data; editors through a share link |
/d/<slug>/<key>?raw=1 |
a record's body alone, in its own content type |
read-only |
/d/<slug>/~<name> |
a computed view: a record the page's handler.js computes on each read |
read-only |
/f/<slug>/<id> |
files: pictures, PDFs, exports |
the owner, with page-file |
/i/<slug> |
the inbox: events posted by machines with the page's secret |
anyone holding the secret |
/h/<slug>/… |
the page's own API, answered by handler.js |
whoever the handler lets in |
The store
page-publish … --data on turns the store on. A record is an envelope {"schema":"<name>@<n>","data":…} of at most 256 KiB, under a key of letters, digits, ., _ and -. The schema is your own name for the shape (metrics@1, table@1); the service checks its grammar, not its content.
"$BIN/page-data" put "$SLUG" metrics --schema metrics@1 --data-file metrics.json --notify none
"$BIN/page-data" get "$SLUG" metrics --data-only
"$BIN/page-data" list "$SLUG" --prefix inbox. --values --limit 100
"$BIN/page-data" delete "$SLUG" metrics --notify none
Every write carries a version: a plain put retries at the newest version and reports that it overwrote someone else's write; --if-version <n> makes one attempt at the version you read and exits 4 when someone wrote first, for you to re-read and merge. A browser lists a prefix with GET /d/<slug>?prefix=inbox.&values=1.
Raw reads
?raw=1 serves a record without its envelope. A raw@1 record, {content_type, body, stale_after?} with content_type text/plain or application/json and a body up to 64 KiB, is served as its body in its own type; that is how every section of this handbook is stored. A raw@1 record past its stale_after answers 503 stale instead of old content, which is how a quote expires on its own. Any other schema is served as its data in JSON.
Files
page-file upload <slug> <path> stores the bytes and a file@1 record at f.<id>; the file is served at /f/<slug>/<id> for as long as the page lives, and a large raster image also gets a 512 px thumbnail at …/thumb. Raster pictures, PDFs, plain text and JSON serve inline; other types, SVG included, download.
Computed views
A page published as a file set can carry handler.js and, beside it, handler.json. A view is a function view(name, params, records) that returns a record; reads(ask) names the records it is handed. GET /d/<slug>/~summary?customer=… runs it on every read, with no network, no secrets and 50 ms of CPU. It reads the page's records under the reader's own access, so a view never shows a reader what they could not fetch themselves. handler.json declares which names are served: {"views":["summary"]}.
New handler code goes live in three moves, and never on the first:
"$BIN/page-publish" "$SITE" --slug "$SLUG" --data on # handler.js + handler.json beside index.html
"$BIN/page-inbox" handler replay "$SLUG" # runs the candidate over past events, prints its sha8
"$BIN/page-inbox" handler activate "$SLUG" --sha <sha8> # refuses a sha with no replay receipt
The same file can carry fold(event, state, records), which shapes each inbox event before it is stored: keep it, drop it, or merge it into an earlier one.
The inbox
POST /i/<slug> stores an event as the record inbox.<seq> when it carries the page's secret, in the X-Inbox-Token header (or GitHub's signature, or GitLab's token). The event name comes from X-Inbox-Event. A delivery id in X-Inbox-Delivery makes a retry safe: the same id twice is stored once.
umask 077; "$BIN/page-inbox" secret "$SLUG" | jq -r .secret > inbox.secret # once per page; --rotate replaces it
"$BIN/page-inbox" send "$SLUG" --event machine.stop --delivery line2-0917 \
--data '{"machine":"welder 3"}' --secret-file inbox.secret
"$BIN/page-inbox" list "$SLUG" --values --limit 100
The secret is shown once, when it is minted. It belongs in a file with mode 600 on the machine that posts, never on a page or in a record.
The page's own API
When handler.json declares request, the handler's request(req, ctx) answers GET, POST, PUT, PATCH and DELETE on /h/<slug>/… and returns {status, body} as JSON. ctx.identity says who is calling, as the service found it: the owner, a share-link holder, or nobody. The handler alone decides what each may do — page visibility is not a gate on /h/ — so an API that checks nobody is open to everyone with the link, on a private page too.
{"request":{"private":true}} gives the handler this.env.STORE, a private range of the page's store that only the handler can read or write; /d/ refuses it to every caller, the owner included.
{"request":{"tokens":["mint","revoke"]}} lets the handler mint and revoke viewer share links for its own page, the way an invitation form would.
- A browser that holds a share-link cookie adds
x-ccc-auth: cookie to every write.
- Limits: 100 requests per 10 seconds per page and caller address, request bodies up to 64 KiB, 5 seconds per call.
Private pages and share links
page-publish … --private makes a page that answers 401 page_locked to anyone without a grant, and a browser shows an unlock form. Make a page private at its first publish: what was public once may already be copied, and a page cannot be turned private later by editing its settings.
"$BIN/page-token" mint "$SLUG" --role viewer --label "Brightline Warehousing" --expires 30d
"$BIN/page-token" list --slug "$SLUG"
"$BIN/page-token" revoke <id>
mint prints a share link once. Its secret sits after the #, which a browser never sends to a server; opening it exchanges the secret for a cookie in that browser. Use one labelled token per person, so one revoke shuts out one person. A revoke takes effect within the hour. Roles are cumulative: a viewer reads; an editor also writes the store and the page's source; a publisher also publishes new versions and manages the page's share links.
Versions and rollback
Every content publish is a new immutable version under the same slug; the address does not change. A metadata change (a new description) makes no version.
"$BIN/page-publish" --slug "$SLUG" --versions # every version, which one is live
"$BIN/page-publish" --slug "$SLUG" --switch 3 # serve version 3 again: the rollback
"$BIN/page-publish" --slug "$SLUG" --delete-version 1 # free an old version's storage
Each publish or switch writes the page's release record, {version, label, at}, so a page that is already open can notice a new version and offer a reload. Records and files are not versioned with the page: a rollback changes the page's code and layout, never its data.
Beyond this chapter
The skill covers more than the handbook shows, including page templates for reports, explainers and data tables; ask your agent which templates the skill offers.