figcraft/docs/server.md

13 KiB

figcraft-server

figcraft-server keeps FigCraft files for a team and serves their live sessions. A file published to it stays there when everyone has left; people open it with a share link that lets them edit it or only view it. Edits are kept as a snapshot plus a log of what changed, so a restart (or a crash) loses nothing that was confirmed, and apps reconnect where they were. It is headless, native only (no web build), and runs on the same platforms as the desktop app.

There are no user accounts yet: a share link is the access to one file, and the server key manages files (publishing, links, removal). Accounts and per-person permissions come later. A file can also hold a team library that other files use through one of its links (see Team libraries).

The protocol and the design behind it are in collab-protocol.md.

Running it

cargo build --release -p figcraft-server
target/release/figcraft-server --listen 0.0.0.0:47900 --data /srv/figcraft
  • --listen defaults to 127.0.0.1:47900: only this machine can connect. 0.0.0.0:47900 (or an interface's address) lets the network in; put it behind a TLS reverse proxy before you expose it beyond a trusted network (see below).
  • --data is the data directory (default ./figcraft-data), made if missing. One server at a time uses a directory: a second one refuses to start.
  • Logs go to standard output (RUST_LOG=debug for more). They name files, link ids and addresses, never keys or tokens.
  • Stop it like any service (Ctrl+C, systemctl stop, kill). Every change it accepted is on disk before anyone is told about it, so nothing confirmed is lost; a file's log is folded into a new snapshot the next time everyone leaves it. Apps that were in a file keep editing and reconnect once the server is back (for about a minute and a half), as the same participants.

Publishing a file

figcraft-server --data /srv/figcraft key

prints the address to publish to, ws://<host>:47900/?key=<server key>. In the desktop app, Share ▸ Publish to server… (or File ▸ Publish to server…) asks for that address and the file's name. The open file is uploaded as a new file and the tab keeps syncing through the server; until the upload is done the tab is read-only ("Publishing…" in the toolbar). The Share popover then shows an edit link and a view link to copy. Agents do the same with collab.publish {server, name?} (mcp.md).

Whoever has the server key can publish files and manage every file's links: give it only to people you trust.

Joining

Share links look like the desktop app's invite links and join the same way (File ▸ Join live session…, opening the link, or collab.join {link}):

  • figcraft://join?host=<host:port>&file=<id>&token=<token> for a plain ws:// server;
  • figcraft://join?server=wss://figcraft.example.com/&file=<id>&token=<token> for a server behind a TLS reverse proxy or under a path;
  • ws://<host:port>/?file=<id>&token=<token> or wss://… (the same link as a WebSocket URL).

The file opens as a new tab. With a view link you see everyone's changes and presence, and others see your cursor and selection, but you can't change anything: the toolbar shows a View only badge with only the Move and Hand tools, edits are refused with a message (and the server refuses them too). Leave session keeps the file open as your own copy, editable again.

Team libraries

A file on the server can hold a team library: its published components, styles and variables, which other files use without anyone copying the library file around.

  • Publishing. In the library file, opened from the server with an edit link (or published to it), Assets ▸ Libraries ▸ Publish to server publishes the chosen assets as the library's next version (library.publishToServer; with the server's address and key a file not on the server yet is kept there first). The server keeps the latest version of each file's library. Each asset keeps the version it last changed in, so files using the library update only what changed; the server's version is the one that counts, so when someone published since, the next publish follows it. Publishing needs an edit link: a view link's publish is refused.
  • Using. In another file, paste one of the library file's links (a view link is enough) into Assets ▸ Libraries (library.add {path: <link>}). The library is listed in the Assets panel like a library file: placing a component or applying a style copies it in as a read-only linked copy, as for library files. While the Assets panel shows, the app asks the server for a newer version at once and then every 30 seconds (a few bytes when nothing changed) and shows Library updates available; accepting keeps instance overrides. While the server can't be reached, the last version fetched stays in use. The file keeps the link, token included: sharing that file shares read access to the library.
  • Usage. Each time a file's app asks for the library it reports which of its components the file uses and how often. Show usage in the library file (library.usage) lists the files and the instances of each component. Files are told apart by their names; a file's report changes only when its usage does.
  • Swapping. library.swap (the swap button by an enabled library) moves a file's instances, styles and variables from one library to another's of the same names, keeping overrides.

The browser build can't use team libraries yet (it has no library support of its own).

Command What it does
figcraft-server key [--rotate] The address to publish to, with the key; --rotate makes a new key (the server must be stopped)
figcraft-server files [list] The files: id, name, people in it now, last change, size
figcraft-server files remove FILE Remove a file and its links for good (whoever is in it is disconnected)
figcraft-server links list FILE A file's links: id, access, created, label
figcraft-server links create FILE --access edit|view [--label TEXT] A new link, printed once
figcraft-server links revoke FILE LINK Revoke a link: it no longer joins, and whoever joined with it is disconnected

Each takes --data (or --config). When a server is running on the directory, the command asks it over the same port with the server key (so a revoked link's users are disconnected at once); otherwise it changes the directory itself. A new link is shown only when it is made: the server keeps only a hash of its token, so a lost link can't be recovered (revoke it and make another). Publishing makes an edit link and a view link labelled Published (edit) and Published (view).

Configuration

Every flag is also a key of a TOML file given with --config server.toml (flags win; unknown keys are errors):

Key Default Meaning
listen 127.0.0.1:47900 Where to listen
data figcraft-data The data directory
public_url (the listen address) How links name the server, when people reach it another way (a reverse proxy: wss://figcraft.example.com/)
max_files 1000 Most files kept
max_file_mb 64 Largest file (its snapshot), in MiB; a welcome carries the whole file, and messages are at most 64 MiB
max_links 100 Most share links per file
max_connections 512 Most open connections
max_people 256 Most people in one file at once
compact_after 1000 Compact a file's log into a new snapshot after this many entries…
compact_mb 16 …or once its log is this large (MiB)

With listen on every interface (0.0.0.0) and no public_url, links name this machine's network address.

The data directory

<data>/LOCK                    held while the server (or a command) uses the directory
<data>/server.json             where the running server listens (read by the commands)
<data>/server.key              the server key
<data>/files/<id>/meta.json    the file's name, when it was created and last changed
<data>/files/<id>/links.json   its links: id, access, label, created, SHA-256 of the token
<data>/files/<id>/snapshot.bin its state at the last compaction (with a checksum)
<data>/files/<id>/log.bin      what changed since, one checksummed record per change
<data>/files/<id>/library.bin  its team library, when one was published (a .figcraft archive)
<data>/files/<id>/usage.json   which files use that library's components, as their apps report it
  • Writes are atomic: a snapshot, a description or a link list is written to a temporary file beside it, flushed, then renamed over the old one. The log is only ever appended to, and synced before the server tells anyone about the change.
  • Compaction writes the file's state as the next snapshot and starts an empty log, after compact_after entries or compact_mb of log, when the last person leaves, and when the server stops. A log left over from a compaction cut short is recognised and ignored (the snapshot holds it all).
  • Damage is skipped and reported, never deleted. At startup a folder whose description, links or snapshot can't be read is left out and logged; a log that ends in a cut-short or damaged record loses only that end (logged when the file opens). Leftover temporary files are removed.
  • The server key is stored as plain text (on Unix readable by the owner only): whoever can read the data directory can read every file anyway. Link tokens are stored only as hashes.

Backups

Copy the data directory while the server is stopped, or take an atomic snapshot of the filesystem it lives on (ZFS, Btrfs, LVM, a VSS shadow copy). A plain copy of a running server's directory can catch a file between its snapshot and its log; it restores, but may lose that file's changes since its last compaction. To restore, stop the server, put the directory back and start it: apps that were in a file catch up with a fresh copy (their unsent edits are kept when the server still knows them, else they are told what couldn't be kept).

Behind a reverse proxy (TLS)

The server speaks plain WebSocket (ws://); TLS comes from a reverse proxy in front of it, which gives browsers the wss:// they need on an https:// page.

  • Run the server on the loopback interface (listen = "127.0.0.1:47900"), and set public_url to the address people use, for example wss://figcraft.example.com/. Links and the publish address then name the proxy.
  • Caddy: a site block for your host name whose only directive is a reverse_proxy to 127.0.0.1:47900. Caddy obtains the certificate and passes WebSocket upgrades through on its own.
  • nginx: a server block listening on 443 with your certificate, and a location that passes requests to http://127.0.0.1:47900 over HTTP/1.1 with the Upgrade and Connection request headers forwarded (that is what lets the WebSocket upgrade through). Raise the proxy's read timeout above the server's 45-second idle limit or leave its default (60 s): apps ping every 15 seconds.
  • Under a path: the server answers on any path, so the proxy can forward a prefix such as /figcraft/ unchanged; set public_url = "wss://example.com/figcraft/".
  • Tokens never travel in the WebSocket request (the client sends them inside its first message), so the proxy's access logs don't record them.

The desktop app has no TLS of its own yet: it can't open wss:// links. Inside a trusted network or a VPN, let it reach the server's ws:// address directly (keep the port closed to the internet), and use wss:// links from the browser build.

Security

  • Access is a share link (128-bit random token) per file and access level, or the server key (128-bit) for managing files. Tokens are stored as SHA-256 hashes and every comparison runs in constant time; revoking a link disconnects whoever joined with it. A view link's batches are refused by the server whatever the client does.
  • Transport: plain ws:// on the server's port; anyone on the network path can read traffic (presence, changes, tokens) unless TLS terminates in front of it. Use a reverse proxy for anything beyond a trusted network.
  • Limits: the transport's (message sizes, 100 messages a second per connection, idle and handshake timeouts, max_connections) and the collaboration core's (layers, changes per batch, max_people), plus max_files, max_file_mb and max_links. Every message is decoded defensively, and fuzzed in the tests. A library is read in full before it is kept (as files using it read it: size limits on every part, at most max_file_mb), must be the file's library at a newer version, and is checked on the server's main thread; usage reports keep at most 2,000 components per file and 500 files per library.
  • Team libraries: any link of a file reads its library and its usage reports (file names and counts); only an edit link publishes it.
  • Not there yet: user accounts, per-person permissions, rate limits per address, audit logs.