Deploying Hezo#
The repo ships a working Dockerfile. Two paths covered here: Railway
(simplest) and any host that runs containers + mounts a volume.
Current production deployment#
This repo is already linked to a Railway project. From the repo root:
railway status # → Project: hezo / Service: centrosome / Env: production
railway up # uploads the local tree, builds the Dockerfile, deploys
Live at https://hezo.app. The service is named centrosome (v1
codename — the rename never happened, the URL did). Volume is mounted
at /data. HEZO_EMAIL_HMAC_KEY is set on Railway; back it up to your
password manager so you can re-set the same value if the volume is
ever lost. Releases are manual — no CI, no GitHub Actions.
Watch a deploy:
railway logs --build # build output
railway logs # runtime / migrations / startup
Setting up a fresh instance#
The rest of this doc covers an instance that isn't already linked.
Required environment#
| Var | Default | Notes |
|---|---|---|
PORT |
3001 |
HTTP listen port. Railway sets this automatically. |
HOST |
127.0.0.1 |
The Dockerfile overrides to 0.0.0.0. |
HEZO_DATA |
./data (dev), /data (Docker) |
Where global.sqlite + per-project DBs + blob dirs live. Must be on a persistent volume. |
HEZO_EMAIL_HMAC_KEY |
(auto) | Optional. Base64url-encoded ≥32 bytes used to HMAC user emails into a blind index. If unset, the server generates one on first boot and persists it to <HEZO_DATA>/email_hmac.key. Back this file up alongside the SQLite DB — losing it orphans every account. |
No other config required. The access model is invite-only by construction (see below).
Railway#
railway init # interactive — pick a project name
railway add --service hezo
railway volume add --mount-path /data
railway up # builds the Dockerfile, deploys
railway domain # generates a public URL
The VOLUME Dockerfile directive isn't supported by Railway — use
railway volume add instead.
Access model#
Hezo is invite-only by default. There's no global allow-list to configure; the gates are:
- Bootstrap login. The very first
/api/auth/loginrequest (when the users table is empty) is always allowed. That email becomes the founding user. Sign in once after deploy to claim ownership of the instance. - Existing-user login. After bootstrap, direct login only works for emails already in the users table. Returning users can sign in normally from any browser.
- Invite-grant login. Anyone with a valid invite URL can sign in, regardless of allow-list status. The invite URL is the access grant — sharing it grants both the right to register on the instance and membership in a project. Tokens are one-shot.
So the flow is:
- Bootstrap user (you) deploys, opens the URL, signs in (any email), creates a project, generates an invite, copies the URL, sends it to a collaborator out of band.
- Collaborator opens the URL, sees "You've been invited," signs in with their email + API key. The same call accepts the invite and mints their session. They're in.
Trust model in prototype mode#
Still no email verification — anyone with an invite URL can claim any email on the instance. Fine for a small trusted group where invite URLs are shared privately; not safe if invite URLs ever leak (chat history, forwarded mail, etc.). Future hardening: magic-link email proof, expiring invite tokens.
Bringing your existing key#
After deploy, sign in once via the web UI to bind your email to the
instance. Then use npm run import-keychain -- you@example.com locally
to push your macOS Keychain API key into the local browser's IndexedDB.
The key is never sent to the server — the import URL is a client-only
fragment.
If you're signing in on a fresh machine without that Keychain entry, just paste the key into the login form directly.