Expand ↗
Page list (55)

Running a Team Server

zetl’s --collab flag turns zetl serve into a multi-user editing server: passkey login, real-time CRDT co-editing, per-user git commits, and scoped invitations. Everything is in-tree — there’s no feature flag to enable at install time. If you can run zetl serve, you can run a team server.

The shape of a collab vault

A collab vault is a normal Markdown vault plus a .zetl/collab/ directory:

  • .zetl/collab/server.key — the server’s Ed25519 signing key (for session cookies and invitations).
  • .zetl/collab/users.db — SQLite store for passkeys, sessions, and invitation nonces.
  • .zetl/collab/wal/ — write-ahead log for the CRDT engine. Survives crashes.

These files are created on first run. You never edit them by hand.

First run: bootstrap the owner

The very first time you start a collab server, you need to bootstrap an owner account. --init-owner creates one, prints a 12-word recovery phrase, and exits into the normal serve loop:

zetl -d ~/notes serve --collab --init-owner --owner-name Alice

The terminal will print something like:

Owner created: Alice
Recovery phrase (write this down — you will not see it again):

  palm  tornado  ladder  oyster  casual  umbrella
  desert  finger  enlist  brave  coconut  strong

Server listening on http://localhost:3000

Save that phrase. It’s your only way back into the account if you lose your passkey, and zetl will not show it to you a second time. Paper, password manager, encrypted notes file — pick one and commit.

Open http://localhost:3000 in your browser, log in as Alice, and register a passkey (Touch ID, YubiKey, or any WebAuthn authenticator). That’s the end of setup. See Passkeys and Accounts for the full dance.

Subsequent starts

After the owner exists, you start the server with just --collab:

zetl -d ~/notes serve --collab

The default port is 3000 and the default bind is localhost. Most of the plain-serve flags still work — --port, --theme, --public. Two extras are worth knowing:

FlagWhen you want it
--hostname mysite.fly.devPublic hostname for WebAuthn. Passkeys are bound to a hostname — if you intend to serve over a real domain, set it. Also env: ZETL_HOSTNAME.
--git-poll-interval 30sHow often zetl checks git for external commits (so an edit you push from another machine shows up in the UI). Set 0 to disable.

Containerised or ephemeral deployments

If your filesystem is ephemeral (Docker, Fly.io, Nomad, a VM you redeploy), the server key in .zetl/collab/server.key gets wiped on every redeploy — which invalidates every live session. Not ideal.

The fix is a deterministic server key, derived from a BIP39 mnemonic you control:

zetl -d /vault serve --collab \
  --server-key-seed "palm tornado ladder oyster casual umbrella \
                     desert finger enlist brave coconut strong"

# Or via environment, which is what you usually want in a container
export ZETL_SERVER_KEY_SEED="palm tornado ladder …"
zetl -d /vault serve --collab

The same mnemonic also derives the git SSH key (via zetl derive-ssh-key) and headless agent tokens (via zetl agent-token). One seed, three uses. See Passkeys and Accounts for the derivation paths and Co-editing for how git push fits in.

What’s next

Last changed by zetl · history

Backlinks