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.
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.
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.
| Engine | Install | Notes |
|---|---|---|
| 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.
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.
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.
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.
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.
./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.
| Path | What |
|---|---|
portico.config.json | Port, passcode hash, engines, alert topic. Yours; never overwritten by an update. |
family.json | Who lives there, and their routing. |
portico.state.json | Paired devices. Secrets. |
ledger/ | The record. Hash-chained, append-only. |
agents/*.md | The pedagogy. Edit the markdown; no code change. |
The installer never touches the first four when updating.