Self-hosting
One project, your infrastructure, your identity provider. The hub is the self-hosted product — not a cut-down copy of the cloud, and not a lesser path.
Maude Cloud and self-hosting are two different products that share one image.
The cloud is multi-tenant: it provisions a cell per project, bills you, and runs a control plane that owns accounts across all of them. Self-hosting is the hub. One project per hub, operated by you, with no account anywhere and nothing phoning home. That is a deliberate decision rather than a gap — a portable copy of the control plane would be a second thing to keep correct, and the half nobody on the cloud side ever runs is the half that rots.
So the self-hosted story is smaller on purpose, and complete on its own terms.
Which shape do you want
| Plain hub | Workspace | |
|---|---|---|
| Who owns the project | each peer, in their own clone | the hub |
| Autosave becomes | a message to other peers | an append-only git commit |
| Media | in git | an S3-compatible bucket |
| Signing in | a token pasted into a terminal | an email and a password — or your own IdP |
| In a browser | no | yes, the real studio |
If everyone on the team has a clone and is comfortable with git, a plain hub is smaller and there is less to operate — Deploy a hub. If somebody should never have to think about git, you want a workspace — Workspace mode.
Everything below assumes a workspace.
The shortest honest path
maude hub workspace-up --local --dev-minio \
--domain localhost --admin-email you@example.comThat runs the whole stack on your laptop — no domain, no certificate, no account anywhere — and then runs the same verification a public deployment gets. It is the cheapest way to find out what you are signing up for. It is testing only: local mode serves plain HTTP, so a sign-in password would travel in the clear.
When you are ready for something real, /design:hub-workspace walks you through it and collects exactly the credentials your choices need.
Exports from the browser: the render sidecar
The hub image carries no browser — that is a security invariant (no tenant-authored code is ever evaluated next to your hub's secrets), not an omission. Since v1.0.7 that matters less than it used to: PNG and SVG of the artboard you're looking at, and the PPTX deck, export straight from the member's own browser — the canvas already renders there, so those clicks are instant and need no sidecar. What still needs a real Chromium on a server is video (MP4/WebM/GIF), PDF, and multi-artboard PNG/SVG scopes.
Two honest options:
- Do nothing. PNG/SVG of the active artboard, the PPTX deck, ZIP and AI handoff all work in the hosted studio; video/PDF show up disabled in the Export sheet with the reason, and the desktop app exports every format. Many teams never need more.
- Add
--rendertoworkspace-up. It deploys themaude-rendersidecar — a separate container that holds the Chromium and nothing else: no hub secret, no volume, no public port (the hub reaches it over the compose network only), and a boot-time refusal if any known secret variable is present in its environment. Renders authenticate back to the hub with the same short-lived read-only capability a member's own browser holds.
maude hub workspace-up … --renderRe-running with the flag is also how you add it to an existing workspace — like every re-run, existing secrets in .env are reused. The verification suite gains a render-health step; a sidecar that is up but misconfigured reports as failed, never as fine.
Pin the tag, and know your architecture. MAUDE_IMAGE_TAG covers both containers, and the sidecar is published for release tags only — unlike the hub it has no :latest, so --render without --image-tag vX.Y.Z names an image that does not exist. Both images are published for linux/amd64 and linux/arm64; releases up to and including v1.0.3 published the sidecar for amd64 only, so on an arm64 host pin a later tag. workspace-up checks the published architectures before it writes anything and refuses with the reason — Docker itself would pull the image happily and only fail at exec format error, ~3 GB later.
The sidecar is a ~3 GB image running Chromium, so give it room: a 2 GB instance is tight for the hub plus a render, and swap or a larger instance is the cheaper answer than a killed container mid-export.
Upgrading to 2.0.0: the image converts your project
The first time a workspace hub starts on the stable 2.0.0 image, or any later stable one, it converts its project to Maude 2 by itself: the file format goes to 2 and saving moves to accepted revisions. Maude Cloud does the same at the same release. Nobody has to press "Update project".
- A backup comes first. One full copy (the documents, the project store and the checkout) goes to your configured backup target under
v2-convert/, or to<data dir>/v2-convert/when you have not set one. The regular backup schedule never prunes it. If there is nowhere durable to put it (no backup target and a disposable data disk), the project is not converted and stays as it was. - Only a stable image converts. A release candidate (
2.0.0-rc.N) and a 1.x image never do, and a second start converts nothing. - The kill switch. Put
CELL_V2_CONVERT=0in the hub's.envand restart to hold the conversion back. Nothing converts while it is set. Remove it, or set it to1, and the next start converts. Any other value also holds it back, so a typo never converts by accident. - How to check.
/healthreports it asv2Convert:
curl -s https://design.acme.com/health | jq .v2Convert
# {"state":"done","converted":1,"skipped":0,"failed":0,"at":"…"} first 2.0.0 start
# {"state":"done","converted":0,"skipped":1,"failed":0,"at":"…"} every start after that
# {"state":"disabled","by":"CELL_V2_CONVERT=0"} the kill switch is on
# {"state":"gated","reason":"prerelease"} an rc or 1.x imageThe hub's log carries the same counts on one line, [v2-convert] converted=1 skipped=0 failed=0, and says [v2-convert] disabled by CELL_V2_CONVERT=0 while the switch is on. failed=1 leaves the project exactly as it was; the line before it names the reason. Fix that and restart, and the next start converts it.
What you are taking on
The command scaffolds and verifies once. It does not operate the deployment, and it prints that list on every successful run rather than implying otherwise. In short:
- Durability is the page to read before you rely on this. Not the backup — the restore.
- Rotate
HUB_SECRETwhen someone leaves. It is the operator credential. - Pin the image tag.
latestmeans an unplanned upgrade on the next restart. - Never expire the
assets/prefix. A canvas in git history can reference media no current canvas does, so "unreferenced" never means "unreachable". - The bill is yours. This runs on your infrastructure.
Read next
- On AWS — EC2 + EBS + S3, the IAM policy, and the two AWS defaults that quietly break this.
- Durability — what survives a restart, a lost volume, and a second hub.
- People — accounts, invite links, and what "removed" actually means.
- Identity — keep the built-in sign-in, or bring Auth0 / Google.
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.
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.