๐Ÿ›๏ธ

Setting up Portico

Your own box. Node.js and nothing else.

Portico runs on hardware you already own โ€” a Raspberry Pi, a NAS, an old laptop. It is one Node process with no dependencies and no database. Nothing about it phones home, because there is no home to phone.

1. Install

curl -fsSL https://porticofamily.com/install.sh | bash

This downloads the server, verifies its checksum, and hands off to the package's own setup.sh. That script creates your config, generates a private alert topic, reports which helpers it found, and runs the safety tests in front of you. It starts nothing.

Prefer to read first? install.sh is fifty lines. The source is on GitHub.

2. Say who lives there

node server.js add-kid ethan Ethan 5
node server.js add-kid owen Owen 3
node server.js set-passcode <your parent passcode>

Then open family.json and set the parents' names, each child's emoji, and โ€” worth doing โ€” their pronouns. Left unset, Portico uses they/them and never guesses from a name.

There are no accounts. A child's iPad is bound once with a six-letter code you read out; that's the whole identity system.

3. Decide where the thinking happens

EngineInstallNotes
Claude env -u ANTHROPIC_API_KEY claude login Runs on your subscription. Unset the key first or you're billed per call.
Local ollama pull qwen3.5:2b Also powers the second safety layer. Set OLLAMA_MAX_LOADED_MODELS=1.
Hugging Face hf auth login Picked up automatically; no second place to store the token.

Set the routing per child and per helper in family.json. The default for tutoring is Claude, deliberately: a 2B model leaks the answer within a few turns and cannot hold the Socratic line. Local is an honest fallback and the child is told when it's in use.

Keep the small model resident

Cold-loading a 2B costs about a minute on a Pi. Portico's second safety layer runs on every turn, so without a warm copy it times out and that layer is, in practice, not there. Portico warms it at boot and again when a session starts, and says so in the log if it couldn't.

4. Get the alerts to a real phone

When a child says something that needs a parent, Portico refuses to answer and pages you. Without somewhere to page, that goes nowhere โ€” so this step is not optional in the way it looks.

Portico posts to a self-hosted ntfy on the same box. Message content stays there; only a content-free wake-up reaches Apple's push service. Install ntfy, then in the ntfy app add your server and subscribe to the topic setup.sh printed.

Send yourself a test before you rely on it.

5. Run it as a service

cp portico.service.example ~/.config/systemd/user/portico.service
loginctl enable-linger "$USER"
systemctl --user daemon-reload
systemctl --user enable --now portico

Then open http://<your-box>.local:4300/parents โ€” for example http://pi-nas.local:4300/parents.

6. Reach it from the iPad

At home, the app uses the .local name and finds the box on your network. From anywhere else, put the box and the phone on a tailnet and use that name instead โ€” Portico stays off the public internet either way.

The app has a connection test on every entry screen. It distinguishes not on the home Wi-Fi from server not running from wrong port, because those look identical otherwise and send you to the wrong place.

7. Back up the record

./backup-ledger.sh

Replicates the ledger to another machine. Because it's hash-chained, the copy can be verified independently at the far end โ€” a replica that fails verify-ledger proves the transfer is at fault, not the history. There is deliberately no --delete: the record only grows.

Where things live

PathWhat
portico.config.jsonPort, passcode hash, engines, alert topic. Yours; never overwritten by an update.
family.jsonWho lives there, and their routing.
portico.state.jsonPaired devices. Secrets.
ledger/The record. Hash-chained, append-only.
agents/*.mdThe pedagogy. Edit the markdown; no code change.

The installer never touches the first four when updating.