Atrium: the sync server for Cella PDF (IT guide)
Cella PDF's Sync menu keeps a project's drawings and everyone's markups in step across the office: people draw on the same sheet and see each other's markups arrive live, with every change kept as history. Sheet changes travel too: a sheet turned, moved, inserted, deleted or renamed on one computer changes on everyone's, and so do the custom columns, sequences, revision schedule and bookmarks, and each sheet's scales, scale regions, hyperlinks and Spaces. The server behind it is Atrium: a shared hub for the firm's drawings. It can also let an AI assistant that runs elsewhere (a claude.ai or Claude Desktop connector, say) read the firm's projects, read-only (AI assistants); that is optional and off until you give Atrium its public address. This guide is for whoever sets it up.
There are two ways to have one:
| Self-hosted | Hosted by Sorted Solution | |
|---|---|---|
| Who runs it | your IT, on your server or cloud | Sorted Solution, one private instance per firm |
| Address | yours, e.g. https://sync.yourfirm.com | https://yourfirm.cellapdf.com |
| Cost | free with Cella PDF | a subscription: plans, seats and storage are in sync/hosted/README.md ▸ The offer, or ask Sorted Solution |
| Backups | yours (built-in nightly backup helps) | nightly, offsite, kept 30 days |
| Leaving | it's yours | you get the whole data folder to run yourself |
Hosted firms can skip to Inviting people. Everything else here is for self-hosting. (Developer and protocol detail: sync/README.md; security: sync/SECURITY.md; how Sorted Solution runs the hosted servers: sync/hosted/README.md.)
Install: Docker · one binary · systemd · TLS. Run: Inviting people · AI assistants · Backups. Maintain: Upgrades · Looking after it · Monitoring · Troubleshooting. Reference: What is stored · Firewall and ports · Sizing.
AI assistants (below) are built, pass Atrium's own tests and have been run end to end against a local server with test clients, but not yet on a deployed server with a real assistant; treat the first setup as a trial and tell Sorted Solution how it goes.
Status for 1.0: the server, its binaries and its tests are run and passing. The Docker image, compose file and systemd unit were written and checked by hand but not yet run on a Docker host or a Linux server; please tell Sorted Solution how the first install goes.
Self-host in 10 minutes with Docker
You need: a Linux machine (a small VM is plenty) with Docker Engine and the compose plugin 2.20 or newer, a DNS name for it (sync.yourfirm.com), and ports 80 and 443 reachable from wherever people work. Port 80 is only for the certificate.
The image is published as ghcr.io/sortedsol/atrium (Linux amd64 and arm64); the three files that run it come as a small download:
curl -fsSLO https://www.sortedsolution.com/software/cella/updates/atrium-docker-1.0.0.zip
unzip atrium-docker-1.0.0.zip && cd atrium-docker-1.0.0
cp .env.example .env
nano .env # ATRIUM_DOMAIN=sync.yourfirm.com, ATRIUM_NAME=Your Firm
docker compose pull # ghcr.io/sortedsol/atrium:1.0.0
docker compose run --rm atrium init # prints the admin token ONCE: put it in your password manager
docker compose up -d
Open https://sync.yourfirm.com/admin and sign in with the token. That's it: Caddy fetched a certificate, Atrium is running as a non-root user with its data in the atrium-data volume, and a backup runs every night at 03:00 UTC into the atrium-backups volume.
Useful afterwards:
docker compose ps # both "healthy"
docker compose logs -f atrium
docker compose exec atrium atrium admin rotate-token # a new admin token
docker compose exec atrium atrium version
One binary (Linux, Windows, macOS)
Each release has a self-contained atrium for linux-x64, linux-arm64, win-x64 and osx-arm64: no .NET or anything else to install, as atrium-1.0.0-<platform>.tar.gz (.zip on Windows) from Sorted Solution. (Built with sync/publish.sh.)
tar -xzf atrium-1.0.0-linux-x64.tar.gz && cd atrium-1.0.0-linux-x64
./atrium init --data /srv/atrium # prints the admin token once
./atrium serve --data /srv/atrium --name "Your Firm"
On Windows: atrium.exe init --data D:\Atrium then atrium.exe serve --data D:\Atrium. To keep it running without a logged-in user, wrap it as a service with a tool such as NSSM (nssm install Atrium C:\Atrium\atrium.exe serve --data D:\Atrium); a native Windows service mode isn't in 1.0. On a Mac mini, a LaunchDaemon (/Library/LaunchDaemons/com.sortedsolution.atrium.plist with ProgramArguments atrium serve --data /Library/Atrium, RunAtLoad and KeepAlive true, run as a dedicated user) does the same; a binary built elsewhere needs codesign -s - atrium first.
Linux with systemd
deploy/atrium.service (also in each Linux archive) runs Atrium as its own user with a locked-down sandbox. Settings go in /etc/atrium/atrium.env.
sudo useradd --system --home-dir /var/lib/atrium --shell /usr/sbin/nologin atrium
sudo install -m 0755 atrium /usr/local/bin/atrium
sudo install -d -m 0750 -g atrium /etc/atrium
sudo install -m 0640 -g atrium atrium.env.example /etc/atrium/atrium.env # edit: ATRIUM_NAME, …
sudo install -d -m 0700 -o atrium -g atrium /var/lib/atrium /var/cache/atrium /var/backups/atrium
sudo -u atrium atrium init --data /var/lib/atrium # the admin token, once
sudo install -m 0644 atrium.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now atrium
journalctl -u atrium -f
The example settings bind Atrium to 127.0.0.1:8787 for a reverse proxy on the same machine (next section) and turn on nightly backups into /var/backups/atrium.
TLS
Every request carries a person's token, so use HTTPS for anything beyond a trusted office LAN.
- Docker: done; Caddy gets and renews a Let's Encrypt certificate (and adds HSTS).
- Office-only name with no public DNS (
sync.yourfirm.local): in.envsetATRIUM_CADDY_TLS=tls internal(Caddy's own certificate authority: install its root, from thecaddy-datavolume, on each computer) or mount your own certificate into Caddy and setATRIUM_CADDY_TLS=tls /certs/fullchain.pem /certs/privkey.pem. - Caddy on the same machine as the binary:
sync.yourfirm.com { reverse_proxy 127.0.0.1:8787 { flush_interval -1 } }with
ATRIUM_BIND=127.0.0.1andATRIUM_TRUST_PROXY=127.0.0.1. - nginx: turn buffering off for the live event stream, allow long reads and big uploads, and pass the client address:
location / { proxy_pass http://127.0.0.1:8787; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_buffering off; proxy_read_timeout 1h; client_max_body_size 2g; } - Atrium itself:
ATRIUM_TLS_CERT=/etc/atrium/fullchain.pemandATRIUM_TLS_KEY=…/privkey.pem(PEM). Restart it after renewing the certificate.
Inviting people
- Sign in at
https://<your address>/adminwith the admin token. - Projects ▸ Create: name it like the job (
2417 Riverside Clinic). - Invites ▸ Create invite for the project, with a note (
Sam's laptop). Copy the code (J6GV-0WDS-ZJ3B); it is shown once, works once, and lasts 14 days. - Send each person the address and their code. In Cella PDF (the Mac, Windows and Linux apps have the same Sync menu): Sync ▸ Join Server…, enter both, then Sync ▸ Connect Document to Project… on a sheet.
Each device that joins gets its own token (kept in the Mac's Keychain, Windows' DPAPI or Linux's secret service). Members ▸ Remove takes someone out of a project. A lost laptop: atrium admin devices, then atrium admin revoke-device <id>; its token stops working everywhere. Headless setups can do it all from the command line: atrium admin create-project, atrium admin invite <project> --note ….
Also on the admin page. Under Projects, each project's tools and client ids disclosure shows its Project Tools (how many, who changed them last) with Clear Project Tools, and the app install ids its members hold with Release (for a reinstalled or replaced machine whose id someone else's device should take over). Streams shows live documents per device against the cap and who is present on which document; Database the schema version, size, last backup and the atrium.db.before-vN copies an upgrade left; the Audit log lists token rotations, revoked devices, cleared Project Tools, released ids and AI assistant sign-ins, revocations and changes to the list of assistants. /admin/metrics carries the stream and presence counts too.
Project Tools. Everyone in a project can share tools from their Tool Chest with the whole project (Tool Chest ▸ Manage Tool Chest… ▸ Project Tools ▸ Share with Project…); they appear under Tool Chest ▸ Project Tools on every member's computer while a document of the project is open. Atrium has no roles inside a project (only the admin token and members), so any member can change or remove them; each change records who made it. Last change wins; someone whose change was replaced at the same moment is told.
AI assistants
An AI assistant that runs elsewhere (a claude.ai or Claude Desktop connector, for instance) can read your firm's projects through Atrium: their markups, statuses, replies and history, with six read tools (list_projects, list_documents, list_markups, get_markup, search_markups, activity_since). It cannot change anything, and it sees no drawing pictures or sheet text; Atrium holds neither. It is off until you give Atrium its public address. (An assistant on someone's own computer, such as Claude Code or Claude Desktop reading the documents open in Cella PDF, does not go through Atrium at all: that is Settings ▸ AI Assistants in the app, Options on Windows, and needs no server.)
- Set
ATRIUM_PUBLIC_URLto the address people reach the server at (https://sync.yourfirm.com, no trailing slash) inatrium.envor your service file (with Docker, asATRIUM_PUBLIC_URL: https://sync.yourfirm.comunder theatriumservice'senvironment:indocker-compose.yml; the compose file passes on only the settings it names), and restart. Hosted servers from Sorted Solution get it from their own setup. Atrium must be reachable over HTTPS at that address; assistants live on the internet and will not sign in to a plain-HTTP or LAN-only server. - Give the assistant's connector the address
https://sync.yourfirm.com/mcp. It starts an OAuth sign-in and shows an 8-character code (it works once and lasts 10 minutes). The person opens Cella PDF on a Mac, Sync ▸ Approve AI Assistant Sign-in…, and enters it; the app approves it with their own sync token, so the assistant sees the projects that person belongs to and no others. A page in a browser alone can't grant access. The Windows and Linux apps do not have the approval sheet yet, so each person who signs an assistant in needs a Mac for that step. - Admin page ▸ AI assistants lists every sign-in (person, assistant, created, last used) with Revoke, and the assistants that registered (under Clients, with Remove, and a form to add one by hand). Revoke when someone leaves or a connector is no longer wanted; the person's own sync token is untouched. Allow AI assistants there switches the whole feature off and on.
ATRIUM_MCP=offturns it off for good on a server that has a public address (set it, andATRIUM_MCP_RATE, the way step 1 sets the address);ATRIUM_MCP_RATE(default 120) is the requests a minute each sign-in may make. WithoutATRIUM_PUBLIC_URLthe sign-in and/mcpaddresses answer "not configured".
Upgrading from an older Atrium migrates the database (schema 6, with an atrium.db.before-v6 copy) and builds the current state of each document from its log. Markups that were never synced as edits (a file brought them with it) appear after someone saves the document in Cella PDF (the apps send each document's markups when they connect and after each save). When an assistant signs in, Atrium fetches the client's published metadata document if it identifies itself by a web address (https only, no redirects, 5 seconds, public addresses only); that is the one outgoing request Atrium makes. Assistants' tokens last an hour and renew for 30 days. The design and the full list of what an assistant can ask for are in sync/README.md.
Backups
- Built in:
ATRIUM_BACKUP_DIR(on in the Docker setup) takes a consistent backup every night atATRIUM_BACKUP_HOUR(UTC) and keepsATRIUM_BACKUP_KEEP(14) dated database snapshots; the PDFs in it are shared between them, so it costs little more than one copy. The admin page shows when it last ran. - By hand or from cron:
atrium backup /backups/atrium --keep 14(safe while running). - Offsite: copy the backup folder off the machine every night (restic, rclone, your NAS's backup). A backup on the same disk is not a backup. A backup is the whole server (every PDF and the database): Atrium creates a new backup folder readable by its own user only; keep it that way.
- Restore: stop Atrium, then
atrium restore /backups/atrium --force(the latest) or--snapshot 2026-10-12(that day) and start it. On a new machine runatrium inittoo (the admin token isn't in backups). With Docker:docker compose stop atrium && docker compose run --rm -v /path/to/backup:/backup atrium restore /backup --force && docker compose start atrium. Apps that synced after the backup was taken re-send what the server is missing by themselves.
Upgrades
Back up first, then replace the binary (or, with Docker, change the image: tag in docker-compose.yml to the new version, docker compose pull && docker compose up -d) and start it. A release that changes the database migrates it on first start, after copying it to atrium.db.before-v<N> in the data folder. An older Atrium refuses to open a newer database; to go back, restore that copy or the backup. Clients keep working throughout: the protocol is versioned. 1.0 speaks protocols 1 and 2: version 2 carries sheet changes and document settings, and a Cella PDF that only knows version 1 keeps syncing markups (it doesn't see sheet changes; update it to get them). A sheet's scales, scale regions, hyperlinks and Spaces need this release's Atrium on top: with an Atrium 1.0 from before 2026-10-15 they stay on each computer, as before, and nothing else changes. Going from a server that only knew version 1 to 1.0 converts the database once (schema 1 to 2: the existing markup history is kept and tagged with the sheet each markup was on); a copy from before stays in atrium.db.before-v2. The 1.0 release also adds schema 3 (which member each app install's id belongs to, filled from the existing history) and schema 4 (Project Tools, a project's shared Tool Chest group, below; the copy from before is atrium.db.before-v4): an older Atrium doesn't offer Project Tools and Cella PDF shows none. Schema 5 adds the admin audit log, and schema 6 (this release, with an atrium.db.before-v6 copy) the document snapshots and the AI assistants' sign-in tables (AI assistants); the current version is atrium version. The apps send snapshots only to a server whose /api/v1/info offers them (mcp), so an older Atrium simply never gets one and syncing is unchanged.
What is stored
Everything is in one data folder (/var/lib/atrium, or the atrium-data volume):
atrium.db: projects; members (name, email, when they joined, last seen, a hash of each device's token); invite codes (hashed); every markup change (the markup's own data: type, position, text, author, status, replies) and every sheet change (turned, moved, inserted, deleted, renamed) and change to a document's custom columns, sequences, revision schedule or bookmarks or to a sheet's scales, hyperlinks or Spaces, and each project's shared tool set (Project Tools: the tools' names, styles and any stamp pictures, at most 500 tools and 2 MB), each with who made it and when. Nothing is ever deleted from this history.- the current state of each document's markups, as the apps last sent it (on connecting and after each save), so AI assistants (below) can read it: the same markup data as above, held for lookup; and the assistants' side: the clients that registered and each sign-in's grant (only hashes of its tokens), with the on/off switch.
blobs/: every version of every PDF uploaded to a project, and pages inserted into a synced document.admin-token.sha256: a hash of the admin token.
No passwords, no payment details, no telemetry: Atrium contacts no one on its own (with AI assistants on, it fetches a signing-in assistant's published client document, once, as above). Presence (who has which sheet open, their pointer) is kept in memory only. Atrium doesn't encrypt its files; keep the folder and its backups on an encrypted disk.
On each person's computer, beside a synced PDF, Cella PDF keeps a hidden .cella folder: the link to the project with the changes not yet sent (<file>.sync.json), and the pages inserted while the server couldn't be reached (<file>.sync-queue/, up to 64 MB) until they upload, so quitting Cella PDF offline loses nothing. The queue empties and the pages go once the server has them; the member token is in the system keychain, never there.
Firewall and ports
| Port | From | For |
|---|---|---|
| 443/tcp (and 443/udp for HTTP/3) | wherever people work | the apps and the admin page (Caddy or your proxy) |
| 80/tcp | the internet | Let's Encrypt's challenge only (Docker setup); redirects to HTTPS |
| 8787/tcp | the proxy only (localhost or the compose network) | Atrium itself; don't expose it |
Atrium makes no outgoing connections, except that with AI assistants on it may fetch a signing-in assistant's published client document over https. Caddy contacts Let's Encrypt to renew certificates. If you use AI assistants, 443 must also be reachable from the assistant's service on the internet (/mcp and /oauth/*).
Sizing
Small. A firm of 5 to 50 people should run comfortably on 1 to 2 CPUs and 1 to 2 GB of memory (an estimate; the work is relaying small JSON messages and serving PDFs). Disk is the drawings: every uploaded version of every PDF is kept, so plan for roughly twice the size of the firm's current drawing sets plus growth, and the same again for local backups. One live session is one open connection per person per open sheet.
Looking after it
Atrium needs little: it backs itself up, it sends nothing anywhere, and it has nothing to tune. What it does need is the machine kept patched, the backups checked, and people removed when they leave.
| When | What |
|---|---|
| Weekly (5 minutes) | the admin page's Database card: the last backup is from last night; df -h on the data disk. |
| When someone leaves the firm | Members ▸ Remove them from each project; revoke their devices (atrium admin devices, atrium admin revoke-device <id>). |
| When a computer is lost or replaced | revoke that device; on a replacement, Release its old client id on the admin page (Projects ▸ tools and client ids) if the new one should take it over. |
| Monthly | operating system updates (Ubuntu: unattended-upgrades does them; reboot when /var/run/reboot-required exists, out of hours); with Docker, docker compose pull caddy && docker compose up -d for Caddy's patches; check for a new Atrium release and read its notes. |
| Every three months | a restore test (below); check the offsite copy is still arriving; check the admin token in the password manager still signs in. |
| When the disk passes 70 percent | grow it. Atrium keeps every uploaded PDF version, so the disk only grows (Sizing). |
A restore test (Docker; the binary is the same with atrium restore --data <scratch folder>):
# atrium_atrium-backups is the compose file's backup volume ("atrium" is the compose project name)
docker run --rm -v atrium_atrium-backups:/backup:ro -v atrium-drill:/var/lib/atrium ghcr.io/sortedsol/atrium:1.0.0 restore /backup
docker run --rm -d --name atrium-drill -p 127.0.0.1:8799:8787 -v atrium-drill:/var/lib/atrium ghcr.io/sortedsol/atrium:1.0.0
curl -s http://127.0.0.1:8799/api/v1/info # the server's name: the backup opens
docker stop atrium-drill && docker volume rm atrium-drill
Moving to a new machine: install Atrium on the new one (same version or newer), stop the old one, take a last backup (atrium backup <folder>, or copy the atrium-backups volume), restore it on the new machine (Backups ▸ Restore), run atrium init there for a new admin token, then point the DNS name at the new machine. The address doesn't change, so nobody has to rejoin; their apps re-send anything the old server took after the backup.
Changing the address (a new DNS name): people have to join again with the new address. Keep the old name pointing at the server until everyone has.
Hosted and self-hosted, either way: a hosted firm that leaves gets its whole data folder (it restores with atrium restore as above, with no license). A self-hosting firm that wants Sorted Solution to host it sends a backup folder through a secure link; it is restored onto the firm's hosted server. Either way the address changes, so people join once more.
Removing Atrium: take a final backup and keep it as long as the firm keeps project records; then docker compose down -v (that deletes the data volumes: be sure), or stop the service and delete /var/lib/atrium. Each computer keeps its PDFs with every markup saved into them.
Monitoring
GET /healthanswers{ "ok": true }(no token; Docker's health check uses it viaatrium health). An uptime monitor (UptimeRobot, Better Stack, healthchecks.io) onhttps://<your address>/healthcatches the server, the certificate and DNS at once.GET /admin/metrics(Prometheus text) with the admin token, or withATRIUM_METRICS_TOKEN, a token that reads metrics and nothing else: projects, documents, ops, connected clients, storage bytes, seats, last backup time and success. With Prometheus, these are the alerts worth having (Sorted Solution's own set issync/hosted/monitoring/atrium.rules.yml):- alert: AtriumDown expr: up{job="atrium"} == 0 for: 5m - alert: AtriumBackupFailed expr: atrium_last_backup_success == 0 or time() - atrium_last_backup_timestamp_seconds > 26 * 3600 - alert: AtriumDiskGrowing # set to 70 percent of your data disk expr: atrium_blob_bytes > 350e9
Troubleshooting
- The app says it can't reach the server:
curl https://<address>/api/v1/infofrom that computer should print the server's name,"protocol":1and"protocols":[1,2]. - "Sheets can't be changed on this sync server": the server is an older Atrium that keeps only markups. Upgrade it (above); the apps pick it up on their next connect.
- "Your sheet change wasn't sent": someone uploaded a newer version of that document; in Cella PDF, Sync ▸ Update Sheets from Project, then make the change again. Nothing of the refused change reaches the project: a refused sheet delete doesn't take that sheet's markups away for anyone else.
- "Can't undo: that sheet was deleted by …": the step you undid (or redid) was for a sheet someone else deleted meanwhile; it is skipped and taken off your Undo history. Undo / Redo of sheet changes otherwise work while synced: each sends the inverse change, so everyone's copy follows.
- "A sheet you chose was deleted … while the dialog was open": someone else deleted a sheet while your Delete, Move To, Insert Pages or Extract Sheets dialog was up, so nothing was changed; choose the sheets again. (Sheets someone else only moved meanwhile are found where they are now.)
- "Not updated: “A-201”…" after Update Sheets: that sheet was deleted or renumbered in the project while the confirm was up; the other sheets were updated.
- Live updates arrive late or in bursts: a proxy is buffering; see the nginx settings above (Caddy needs
flush_interval -1). - An assistant's sign-in page or
/mcpsays "not configured" (503):ATRIUM_PUBLIC_URLis not set, orATRIUM_MCP=off, or the admin page's Allow AI assistants is off./api/v1/infoshowsmcpasnullwithATRIUM_MCP=off, and itsurlasnullwithout a public address. - Lost the admin token:
atrium admin rotate-tokenon the server (docker compose exec atrium atrium admin rotate-token). - "read-only" banner: only on Sorted Solution-hosted instances whose plan has ended; renew with Sorted Solution.
- Uploads fail over a certain size: raise
ATRIUM_MAX_UPLOAD_MBand the proxy's body limit. - "This device has 32 live documents open on this server already": one computer has more synced documents open at once than
ATRIUM_MAX_STREAMS_PER_DEVICE(32) allows; close some, or raise it. - Someone removed from a project still sees changes for a moment: an open document stops getting them within the keep-alive interval (
ATRIUM_KEEPALIVE_SECONDS, 20 s), and it can't reconnect. A lost computer:atrium admin revoke-device ID(atrium admin deviceslists them). - All settings:
sync/README.md▸ Settings, oratrium help.