Zoo

staging

The zoo

ratcloud's building blocks, each one working live, in front of you. A feature without an exhibit is not offered; a broken exhibit is seen here, not logged.

Checked · 19 green, 3 red, 3 waiting for a person to try, 2 not built yet

A red light is a promise broken right now. The zoo shows it rather than hiding it, and the card says what is wrong; the green ones still work.

Identity

1

One sign-in, every app

nobody has seen this work yet

Sign in once at account, and every app knows who you are without asking again.

See it working

Who am I, here on the zoo:

?visitornot signed in

Now open a second app on its own address: it should show the same card, with no prompt.

Sign in to record that this worked for you.

The account shim: an app requires rc-auth.php and calls auth_user(). A signed-in browser is silently handed a token for that app; there is no sign-in page per app and no password anywhere.

how a person checks this
2

Visitors get a real page

last worked

Someone who is not signed in sees the page, not a bounce to a sign-in screen.

See it working

You are a visitor. This whole page loaded without a sign-in.

auth_user() is null for a visitor. The app decides what a visitor sees; the platform never forces a sign-in.

how checks itself
3

Sign out everywhere

nobody has seen this work yet

Signing out at account signs you out of every app in this browser.

See it working

Sign in first; then this button signs you out of every app at once.

rc_sign_out() sends the browser to account, which ends the one session behind every app's token. No app has to be told.

how a person checks this
4

Your name and picture follow you

last worked

Change your name and picture at account, and every app shows the new ones.

See it working

Sign in to sign the guestbook.

Change your name or picture at account, then reload: your entry changes too. (The robot renames itself to the time of every check, which is how this exhibit checks itself.)

Zoo Robot 18:25@zoo-robot · #11
The robot was here, on staging. ·

rc_users($ids) turns stored ids into current names and pictures, one call per page. An app keeps ids, never copies of names or pictures.

how checks itself
5

Find a friend

last worked

Any app can search for people by name, including people who have never used it.

See it working

Sign in to search for people.

rc_user_search($q) finds people by the start of their username or of any word of their name, across the whole platform.

how checks itself

Deploy

6

A push is a deploy

last worked

Pushing to main puts the code live within a minute.

See it working
  • live0c1c124a02 by johannes,
  • staging77ff74344d by johannes,

git push is the deploy. What is running, and since when, is in RC_FACTS/deploy.json, written by root, readable by the app.

how checks itself
7

The branch decides

last worked

main goes live, staging goes to a twin with its own data, and any other branch goes nowhere.

See it working
  • livemain 0c1c124 · deployed ·
  • stagingstaging 77ff743 · deployed ·
  • nowherebuild/seal 98461f1 · ignored ·

This guestbook has 1 signature; the live zoo's has 2, and they are different books.

Push main for live, staging for https://<app>-staging. Each environment has its own RC_DATA, so its own databases; the same code, never the same data.

how checks itself
8

A bad manifest is shown, not applied

the sample app's typo was accepted, or is gone: no error to show (last worked )

A mistake in an app's settings file (ratcloud.conf) leaves the old settings running and says what was wrong.

See it working

The sample app zoo-broken has not been checked yet.

ratcloud.conf is checked before anything is applied. A bad one refuses the push, in the pusher's terminal and in RC_FACTS/manifest.json; the site keeps running.

how checks itself

Isolation

9

Try the doors

last worked

An app cannot open another app's files, and it cannot reach the server's own internal network.

See it working

    Root tries them too, as every app, every hour:

    • pass21 app(s) and 21 preview uid(s), 420 pair(s), 16611 paths refused (a preview opens none of its own app's live or staging), own paths open
    • pass35 uid(s) (apps and previews): loopback, link-local, private ranges and the box's own addresses (but 80/443) refused; own publish port open; declared exceptions: app-builder may reach the box's public sshd (rc egress builder ssh)

    Every app runs as its own unix user in a sandbox, behind a firewall. RC_DATA is the one place it can write; everything else on the box is someone else's.

    how checks itself

    Realtime

    10

    Live for everyone

    last worked

    A change one person makes appears on everyone's open page within a second, without a reload.

    See it working
    1079

    Open this page in two tabs and press in one.

    Declare a channel in ratcloud.conf; browsers listen with EventSource at /rc/sub/<name>/<id>, the app POSTs to $RC_PUBLISH/pub/<name>/<id>.

    how checks itself
    11

    Private streams stay private

    last worked

    A live stream meant for one person can't be read by someone who guesses its name.

    See it working

    Sign in to get a stream of your own.

    Add `auth` to a channel in ratcloud.conf, and the app decides who may listen: nginx asks it first, with RC_CHANNEL and RC_CHANNEL_ID.

    how checks itself

    Storage

    12

    Your files

    last worked

    You can upload a file, see it listed, and download it again unchanged.

    See it working

    Last roundtrip, : 6577 bytes uploaded, listed and downloaded again: matched

    Open storage, upload something, and download it again.

    storage keeps each person's files (B2-backed) behind their sign-in; an app links to it and never holds storage's keys. The check here uploads random bytes, lists them, downloads them and compares.

    how checks itself
    13

    Apps can save files for you

    saving 50 postcards failed (0): (last worked )

    An app can save a file into your storage, under its own folder, without ever holding storage's keys.

    See it working

    Sign in to have the zoo save a postcard for you.

    rc_app_headers("storage", true) proves to storage which app calls and for whom; storage saves only under Apps / <app>. In bulk: app/begin gives presigned PUTs (200 a call), the bytes go straight to B2, app/commit checks them all at once, app/urls hands out downloads.

    how checks itself

    Sealed

    26

    Sealed means sealed

    last worked

    A file an app seals for you opens on your devices and nowhere else; the server keeps only what it cannot read.

    See it working

    Sign in to seal a postcard only your devices can open.

    The self-check seals a fresh postcard for the robot each time, : 269 bytes in the bucket, none of them its words.

    storage's sealing block: rc-crypt.js and rc-seal.js (https://storage.<domain>/seal/v1/) seal in the browser with a key only your devices and your recovery key open; the app's server forwards keyring calls and stores envelopes it cannot read.

    how checks itself
    27

    A new device needs an old one

    last worked

    A device that never opened your sealed files gets in only with a code typed on one that has, or with your recovery key.

    See it working

    Sign in, seal a postcard (26), then open this page in a private window.

    rc-seal.js: seal.request() shows a code, seal.approve(code) on a device that has the key lets the new one in, seal.useStorage() lets storage's window do it with your recovery key; every keyring is checked under the key it guards.

    how checks itself

    Intelligence

    14

    Ask a model

    last worked

    Any app can ask a language model a question without holding a key.

    See it working

    Sign in to ask the model something.

    The ai app holds the key; an app calls it with rc_app_headers("ai", true) and gets an answer marked who asked, for whom.

    how checks itself
    15

    You can see what you spent

    last worked

    Every question through ai is on your ledger, per app.

    See it working

    Sign in to see what you spent.

    ai writes every call to a ledger; GET ?api=usage tells an app what the signed-in person spent today and this week, by app.

    how checks itself

    Notifications

    16

    Ping me

    nobody has seen this work yet

    An app can send a notification to your phone or browser.

    See it working

    Sign in to be notified.

    The push app keeps each person's devices (Web Push); an app asks it to notify someone with rc_app_headers("push", true).

    how a person checks this

    Building

    17

    A new app in a minute

    not built yet: waiting on the dashboard

    Creating an app gives it an address, a staging twin and a repo to push to.

    See it working

    The dashboard's create form asks root (through its spool) for a new app: a user, both addresses with certificates, and a repo.

    how not built yet
    18

    Add a laptop with one line

    not built yet: waiting on the dashboard

    Pasting one line into Claude Code lets that machine push; removing it stops it.

    See it working

    The dashboard lists each creator's machines (their ssh keys) with each one's last push; removing one revokes it.

    how not built yet
    19

    Show me how

    last worked

    Every exhibit shows the code that makes it work.

    See it working

    Every card has a how link. Try the counter's or the doors': the actual file serving this page, with its sha256.

    An app's code is plain files in its checkout; the zoo serves its own exhibits' files as they are on disk, so what you read is what runs.

    how checks itself
    20

    Errors you can read

    last worked

    An app's error log is visible to its creator without a shell.

    See it working

    It appears below within a minute.

    2026-10-06T18:15:21+00:00 ratcloud zoo[1681200]: PHP Warning:  zoo exhibit 20: a deliberate warning, staging, fb7f6a99 in /srv/apps/zoo/staging/exhibits/20-errors-you-can-read.php on line 26
    2026-10-06T18:15:30+00:00 ratcloud zoo[1681200]: PHP Warning:  zoo exhibit 20: a deliberate warning, live, 9d2af85f in /srv/apps/zoo/live/exhibits/20-errors-you-can-read.php on line 26
    2026-10-06T18:20:23+00:00 ratcloud zoo[1682847]: PHP Warning:  zoo exhibit 20: a deliberate warning, live, bafaf2f9 in /srv/apps/zoo/live/exhibits/20-errors-you-can-read.php on line 26
    2026-10-06T18:20:25+00:00 ratcloud zoo[1682862]: PHP Warning:  zoo exhibit 20: a deliberate warning, staging, da46ba63 in /srv/apps/zoo/staging/exhibits/20-errors-you-can-read.php on line 26
    2026-10-06T18:25:24+00:00 ratcloud zoo[1684578]: PHP Warning:  zoo exhibit 20: a deliberate warning, live, 5d2a74b1 in /srv/apps/zoo/live/exhibits/20-errors-you-can-read.php on line 26
    2026-10-06T18:25:25+00:00 ratcloud zoo[1684578]: PHP Warning:  zoo exhibit 20: a deliberate warning, staging, acb55071 in /srv/apps/zoo/staging/exhibits/20-errors-you-can-read.php on line 26

    PHP warnings and job output land in RC_FACTS/log.txt within a minute, readable by the app and shown on the creator's dashboard.

    how checks itself
    21

    Jobs on a clock

    last worked

    An app can run a script every few minutes.

    See it working

    Last tick: , written by the zoo's own five-minute job.

    Add `cron = bin/job.php every 5m` to ratcloud.conf: root runs it as the app, never two at once, with its output in RC_FACTS/log.txt.

    how checks itself
    25

    Change an app by asking

    last worked

    A model changes your app and shows it running on a preview address, and nothing reaches staging or live until you tap ship.

    See it working

    Last night the builder's robot asked, on the sample app zoo-scratch:

    Make GET /ping answer with exactly the text "otter-616b09" (plain text, status 200). Keep everything else as it is.
    • preview: /ping answered 200 otter-616b09 (has the word)
    • staging: /ping answered 404 not found (without it)
    • live: /ping answered 404 not found (without it)
    • the builder's key pushing main: refused

    6 turns, 6k tokens from the builder's own budget, 2026-10-06T04:25:01+00:00.

    The builder app: say what should change; it edits a clone, pushes preview/run-<id> (its key can push nothing else), and the creator reviews the diff and the preview, then ships through root's ship request.

    how checks itself

    Operations

    22

    The zoo checks itself

    red: exhibits 8, 13 (last worked )

    Every self-running exhibit is rechecked on a schedule, and the front page says when.

    See it working

    Checked (took 40.1 s).

    The back of house, checked by root :

    • passAn app cannot read another app's data, code or repo, checked from outside as each app's user. 21 app(s) and 21 preview uid(s), 420 pair(s), 16611 paths refused (a preview opens none of its own app's live or staging), own paths open
    • passNo app can use admin rights (sudo), and no creator gets a shell or port forwarding, only git. 21 app user(s) and their preview users without sudo or shell; 2 creator(s): git-shell (git and rc-data only), restricted keys, one pinned sudo rule per app; 3 key(s) granted preview/* only, none of a platform app to the builder; 1 owner(s), each holding an account
    • passRoot never runs code an app pushed, and never writes into a directory an app owns. no root-owned file in any app's data, spool or checkout; every app process and job runs as its app
    • passAn account number is never reused, even after a reset or a restore. AUTOINCREMENT, never below root's high-water mark; live: next id above 246, staging: next id above 414
    • passOne app's sign-in token gets nothing from account when another app presents it. robot's ai token: nobody at album, itself at ai, nobody after sign-out (live, staging)
    • passLive stream names are prefixed by app and by environment, so staging can't reach live. 46 channel id(s) all <app>.<env>.<name>; 56 publish ports, loopback only, each reachable by its own environment's uid alone
    • passEvery address has a valid certificate and sends the platform's security headers. 57 addresses: valid certificate (56 days left), security headers present
    • passThe web server's and ssh's configs on disk pass their own tests, so a reload or a reboot brings them back. nginx -t and sshd -t pass
    • passNo app can take the whole box: memory, CPU and tasks are capped per app, and the disk has room. 21 app slice(s) capped (MemoryMax=512M MemoryHigh=384M CPUQuota=100% TasksMax=192); 77% of the disk free; largest data: spelarbok 5.8 MB, builder 4.3 MB, reader 1012.0 KB
    • passA builder run's agent is a laptop in a box: its own uid, slice and network, no app's files, and only the public internet and the box's public 22 and 443. 0 run(s) on the worker, 0 of 2 up: each its own uid in rc-agent.slice (MemoryMax=1536M MemoryHigh=1408M CPUQuota=100% TasksMax=512; 768M a run), its own network; a run's uid cannot read apps' code or data or root's state, and reaches only DNS, the public internet and the box's public 22 and 443
    • passEvery app directory under /srv/apps has a registry entry and every entry a directory, so no probe is left behind. 21 app(s), directories and registry agree

    The zoo is an ordinary app: a five-minute job re-runs its exhibits and stores the results, and root's hourly check of the platform arrives in RC_FACTS/platform.json.

    how checks itself
    23

    Last night's backup works

    last worked

    Everything is backed up nightly, a copy leaves the server, and the backup is opened to prove it.

    See it working
    • backup, 172 MB, verified
    • read back1 guestbook signature(s), counted in the backup copy
    • offsite, 161.5 MB encrypted

    Add `backup_check = <db> <table>` to ratcloud.conf: after each nightly backup root counts that table in the copy and writes it to RC_FACTS/backup.json.

    how checks itself
    24

    The server can be replaced

    last worked

    The whole platform can be rebuilt on a fresh machine from the tools repo, one bundle of secrets, and one backup.

    See it working

    Last rehearsal : everything came back. Last one that passed: .

    Nothing refers to the machine: a rehearsal restores everything onto a scratch box and checks every app answers. Its verdict reaches each app in RC_FACTS/backup.json.