Deploy a hub
One self-hostable Yjs sync hub, deployable to Fly or any box that runs Docker. No SaaS, no Cloudflare account, no Yjs knowledge required.
The Maude hub is the only cross-machine collaboration path in v1.1. Phase 8 multi-tab collab is loopback-only — to share a canvas with someone on another network, you deploy a hub, they maude design link to it, and both .design/ folders mirror through it.
The hub is a pre-configured Hocuspocus server (production-tested, MIT, Node-native — DDR-052 explains why not PartyKit) with token auth and SQLite persistence. You never touch Yjs.
Two deploy targets ship in v1.1: Fly (one command) and Docker (everywhere else). Both run the same image.
Fly.io — the one-command path
Cheapest and least ops. ~$0.45/mo on the 256MB shared-cpu-1 tier. Auto-cert, auto-restart, persistent volume.
maude hub deploy fly --name maude-hub-acme --region fra
# writes fly.toml + Dockerfile, then prints the next commands:
fly launch --copy-config --no-deploy --name maude-hub-acme --region fra
fly volumes create maude_hub_data --region fra --size 3 --yes
fly deployAfter fly deploy, the boot logs print a single-use /admin bootstrap link (valid 24h). Open it → "Generate invite" → copy the maude design link … command → send it to your collaborator. See Linking peers.
maude hub deploy never runs fly for you — it emits the files and prints the commands so you can review fly.toml first.
Docker — everywhere else
The docker target emits a docker-compose.yml + Caddyfile. Caddy fetches a Let's Encrypt cert for your domain automatically and proxies WSS to the hub. This same stack runs on every provider below.
maude hub deploy docker --tag latest --out ./deploy
cd deploy
cat > .env <<EOF
HUB_SECRET=$(openssl rand -hex 32)
PUBLIC_DOMAIN=maude-hub.example.com
ACME_EMAIL=you@example.com
EOF
docker compose up -d
docker compose logs hub # copy the single-use /admin bootstrap linkPoint a DNS A/AAAA record at the box first, then docker compose up -d. The image is ghcr.io/1agh/maude-hub (multi-arch amd64 + arm64, published on every release tag).
AWS Lightsail
t4g.nano (arm) + a 10 GB block-storage disk + Lightsail's static IP. ~$4–6/mo.
- Attach the static IP, point your DNS at it.
- Install Docker, drop the emitted
docker-compose.yml+Caddyfile,docker compose up -d. - Route 53 (or any DNS) for
PUBLIC_DOMAIN.
AWS EC2 + ALB
For teams already on AWS. t4g.nano EC2 + a gp3 EBS volume mounted at /data + an ALB with an ACM cert (ALB upgrades WebSockets natively, so you can skip Caddy and let the ALB terminate TLS). ~$15–20/mo.
Avoid EFS for
/data— Yjs persistence is small-write-heavy and EFS per-op latency hurts. Use gp3 EBS unless you genuinely need multi-AZ HA.
Hetzner / DigitalOcean / Vultr / Linode
The universal path. Any $4–6/mo VPS (Hetzner CX11, a DO droplet, etc.): install Docker, drop the compose stack + Caddyfile, point DNS, docker compose up -d. Caddy handles TLS.
Coolify
If you want a self-hosted PaaS layer on a $5 VPS, Coolify gives you deploy/restart/TLS as a UI. The Maude hub is just a Docker app — point Coolify at ghcr.io/1agh/maude-hub, set HUB_SECRET + HUB_PUBLIC_URL, mount a volume at /data.
Cloudflare Tunnel (home server, no public IP)
Advanced — only if you can't get a public IP. Run the hub locally via Docker, then expose it with cloudflared tunnel (Cloudflare terminates TLS upstream). This is the only mention of tunnels in v1.1, and it's user-driven, not Maude-provided.
docker compose up -d # hub on localhost, no Caddy needed
cloudflared tunnel --url http://localhost:1234Set HUB_PUBLIC_URL to the tunnel's https URL so the admin bootstrap link is correct.
Behind an existing reverse proxy (sub-path mount)
Already run an nginx/Caddy in front of a box — a shared dev/PR-preview server, say — and want the hub on a path like https://example.com/hub instead of its own subdomain? The admin UI serves mount-relative assets and API calls, so the hub runs correctly under any path prefix as long as the proxy strips that prefix before forwarding.
Set HUB_PUBLIC_URL to the full public URL including the path so the single-use bootstrap link and every maude design link … invite command carry the prefix:
HUB_PUBLIC_URL=https://example.com/hubnginx — strip the prefix, forward to the hub, and pass the WebSocket upgrade (the same shape as a typical path-based PR-preview route):
location /hub/ {
rewrite ^/hub/(.*)$ /$1 break; # strip /hub before forwarding
proxy_pass http://127.0.0.1:1234;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # Yjs WebSocket sync
proxy_set_header Connection "upgrade";
proxy_read_timeout 7d; # long-lived collab sockets
}Caddy does the same with handle_path, which strips the matched prefix automatically: handle_path /hub/* { reverse_proxy 127.0.0.1:1234 }.
The hub itself always sees root paths (/admin, /admin/api/*, /health) — the proxy strips the prefix — so nothing in the hub config changes. Open https://example.com/hub/admin and the bootstrap flow works exactly as on a root-mounted deploy.
Sub-path serving landed after v0.24.0. The published
ghcr.io/1agh/maude-hub:latestonly picks it up on the next release — if you need it before then, build the image from source:docker build -t maude-hub apps/hub.
Transport hardening
- WSS is mandatory. The hub refuses to boot over plaintext
http://to a non-loopback host. TLS terminates at your proxy (Fly / Caddy / ALB / Cloudflare, or any upstream TLS terminator), so the hub itself seeshttp://internally —HUB_PUBLIC_URLmust declare thehttps://URL. Local dev setsHUB_INSECURE_HTTP=1to opt out. - Token auth on every connection. Tokens are stored HMAC-SHA256-hashed at rest (the raw value never lands on disk). Per-token rate limit: 100 auths / 60s.
- Bootstrap key is single-use and not reissued after consumption — fall back to
HUB_SECRETif you miss the window.
Self-host checklist
maude hub deploy <fly|docker>→ review the emitted files.- Run the printed deploy command.
- Open the single-use
/adminbootstrap link from the logs. - "Generate invite" → copy the
maude design link …command. - Send it to each collaborator → they paste it in their repo. See Linking peers.
For local contributor testing without a cloud account, see apps/hub/CONTRIBUTING.md.
The hub
Self-hostable Yjs sync hub for cross-machine canvas collaboration. Deploy once, link your peers, share canvases over the internet — no SaaS, no Cloudflare account, no Yjs knowledge.
Linking peers
maude design link / unlink / status / adopt — pair a local repo with a hub, mirror .design/ bidirectionally, and survive the hub going offline.