Self-hosting¶
A ZippChat instance is one public host name serving three things:
| Path | What |
|---|---|
/ |
this website (optional for a private instance) |
/app/ |
the web client |
/api/…, /gateway, /media/… |
the backend |
Voice signaling runs on livekit.<host>. Voice media needs real ports at
your edge — 7881/tcp and 7882/udp — because WebRTC cannot cross an
HTTP-only proxy or tunnel.
The web client has no domain compiled in: it talks to whatever origin served it, so the same image runs on your host and on the main service. The Android app can be pointed at any instance from its login screen.
Before you start, read the hosting model. On your own box, you are the operator who could be compelled to hand over anything the server can read — persistent channels, media in them, and all metadata.
What you run¶
| Component | Version in the reference deployment | Notes |
|---|---|---|
| PostgreSQL | 16 | migrations run from the api image |
| Valkey (Redis protocol) | 8 | REDIS_URL must use the redis:// scheme |
| NATS with JetStream | 2.10 | event bus between services |
| Garage (S3-compatible) | 1.0 | sealed attachments; the variables are still named MINIO_* |
| Vault | 1.17 | channel and media KEKs — must be persistent, not -dev |
| LiveKit | — | voice SFU; media is end-to-end encrypted, it only forwards |
| api, keyserver, media, voice, worker | — | the five Go services |
| web client, site | — | static nginx images |
| reverse proxy | — | routes the paths above; most specific first |
The proxy has to send /api/keys/ to the keyserver, /api/voice/ to voice,
/api/media/ and /media/ to media, and the rest of /api/ plus /gateway
(a WebSocket, with a long read timeout) to the api — before falling through to
/app/ and /.
Vault must persist
If Vault loses its data, every channel KEK goes with it: all persistent
channel history and every stored attachment become permanently
undecryptable — while the API keeps answering 200 with empty channels.
The development compose files run Vault in -dev mode (in memory) and are
not deploy targets.
The supported path¶
The reference deployment is Kubernetes (k3s) on a single node, described in
k3s deployment. The public host is set in exactly
one place, Server/k8s/base/public-host.env:
Kustomize writes it into the Ingress (the main host and livekit.<host>),
CORS_ALLOWED_ORIGINS and LIVEKIT_PUBLIC_URL. Nothing else names a domain.
Things a self-hoster has to provide that the manifests cannot:
LIVEKIT_NODE_IP— the public IP your edge advertises for voice media;- Vault initialised and unsealed (it does not auto-unseal; that is deliberate);
- the secrets listed in the deployment guide (JWT secret, database password, LiveKit key pair, Garage RPC secret);
- optionally,
FCM_PROJECT_IDandFCM_CREDENTIALS_JSONfor Firebase push. UnifiedPush works without any of it.
Accounts and other instances¶
An instance has its own accounts. Signing in to someone else's instance with a main-service account, and joining servers across instances, is designed (see the hosting model) but not built.