Run your own Host
The Host is the device-side of GemmaPod — proxies pods to a local Gemma 4 on Ollama, enforces signed-manifest tool allow-lists, and stores conversation memory in SQLite.
The Host is the device side of GemmaPod. It runs on your machine (Mac mini, NAS, VPS, Raspberry Pi — anything that runs Node 22 and can reach the signaling broker outbound). One Host process serves many pods. For each visitor that opens one of your pods, it:
- Accepts a WebRTC offer through the signaling broker.
- Exchanges signed
dartc.helloenvelopes with the visitor's session key. - Verifies the inlined signed manifest carried in
dartc.hello. - Intersects the manifest's signed tool allow-list with its local registry — only tools in both are exposed to the model.
- Streams
gemmapod.chat.deltaframes + signedgemmapod.ui.eventenvelopes as the model generates.
Prereqs
-
Node 22+
-
The unified CLI:
npm i -g gemmapod -
Ollama (or any OpenAI-compatible endpoint) with
gemma4:e4bpulled:ollama pull gemma4:e4b ollama serve -
A reachable signaling broker — either the default
wss://signal.gemmapod.com/signalor your self-hosted broker.
Start the Host
The simplest path: just gemmapod run a pod. If no Host is running,
one starts in the foreground automatically.
gemmapod run ./my-podIf you want a Host with no pods yet (e.g. so the CLI sees it on the
next gemmapod list), start it explicitly:
gemmapod start # foreground
gemmapod start --detach # background, returns immediately
gemmapod start --headless # no dashboard UIWhat you'll see (foreground):
[host] ready at http://localhost:57447
[host] dashboard at http://localhost:57447When a visitor opens one of your pods you'll see:
[host] offer session=… (1234 bytes)
[host] dartc.hello verified visitor=… manifest_ok=true tools=[…]
[host] chat.request conversation=… model=gemma4:e4b
[host] chat.done conversation=…The Host writes a discovery file at ~/.gemmapod/host.json containing
its port, PID, and an auth token. The CLI reads it to talk to the
running Host; gemmapod status will show whether it's healthy.
Configuration
Most configuration lives in each pod's pod.toml (signal URL, owner
pubkey, model). The Host itself takes a few process-level inputs:
| Env / flag | Default | Meaning |
|---|---|---|
--port <n> / GEMMAPOD_PORT | first free in 57447..57456 | Loopback port for the HTTP API + dashboard. |
OLLAMA_URL | http://localhost:11434 | Where the Host proxies model requests. |
OWNER_PUBKEY | unset | If set, reject any pod manifest not signed by this key. Recommended. |
GEMMAPOD_HOST_DB | ~/.gemmapod/host.sqlite | SQLite path for conversation memory. |
GEMMAPOD_CONTACT_JSON | unset | JSON returned by the built-in share_contact tool. |
Per-pod settings (signal URL, pod id, model) come from each pod's
pod.toml. See pod.toml reference.
Docker
docker run --rm \
-e OLLAMA_URL=http://host.docker.internal:11434 \
-e OWNER_PUBKEY=<your_pubkey_hex> \
-v ~/.gemmapod:/root/.gemmapod \
-v $(pwd)/my-pod:/pod \
-p 57447:57447 \
ghcr.io/gemmapod/host:latest /podhost.docker.internal only resolves on Docker Desktop. On Linux,
either run with --network host or expose Ollama on a routable
address.
Run under launchd / systemd / pm2
The Host is a long-lived foreground process. Reasonable runners:
- macOS — launchd: a plist with
KeepAlive=trueinvokinggemmapod start --headless. - Linux — systemd: a unit file with
Restart=alwaysandExecStart=/usr/bin/gemmapod start --headless. - Anywhere — pm2:
pm2 start gemmapod --name gemmapod-host -- start --headless.
Use --detach if you're scripting and want the CLI invocation to
return; use --headless to skip serving the dashboard UI.
Conversation memory
The Host keeps (podId, conversationId) rows in SQLite. A visitor's
browser refresh creates a new WebRTC peer and a new ephemeral DARTC
session key, but sends the same conversationId in the next
dartc.hello — so the Host reattaches to the existing thread.
To clear memory for a pod:
sqlite3 ~/.gemmapod/host.sqlite "DELETE FROM conversations WHERE pod_id = 'hello-pod';"Production hardening
- Set
OWNER_PUBKEYin each pod'spod.toml. Without it, anyone running a pod with the samepod_idcould request your tools. - No inbound HTTP. The Host is outbound-only — it dials the signaling broker via WSS. Don't forward ports.
- Single Ollama URL. The Host won't talk to arbitrary backends.
- Audit log. Stream the Host's stderr to a file or your log aggregator. Each tool call is logged.