mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 00:39:44 -06:00
M9: standalone collaboration server with share links and permissions (#112)
figcraft-server hosts many files over the same hub the app uses for sessions, persists each as a checksummed snapshot plus an append-only log (synced before replies, compacted, damaged files skipped and reported) so restarts lose nothing confirmed, and gives per-file share links with edit or view access (hashed 128-bit tokens, revocable). The app can publish a file to a server and join server-hosted files; view-only tabs refuse edits. Protocol version 2.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# FigCraft architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (added the `plugin` and `collab` crates from #101 and #103) · **Target:** Figma Design
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the shared hub in `collab-net` and the `figcraft-server` app) · **Target:** Figma Design
|
||||
|
||||
How FigCraft is built today. Engine first, a thin swappable UI, everything a command; a Figma-shaped
|
||||
data model (a scene graph of frames with auto layout, components and variables). ~108,800 lines of
|
||||
@@ -29,8 +29,8 @@ replaced.
|
||||
| L5 | `figcraft-engine` | ~39.3k | Sessions, undo history (`Arc` snapshots), the command registry (`cmd/*`, `spec!`), pointer tools and vector edit mode, snapping and measuring, components sync, libraries, prototype runtime, Dev Mode code generation, the menu catalog (`catalog.rs`) and the keymap (`keymap.rs`), a panic guard |
|
||||
| L6 | `figcraft-ui-egui` | ~31.9k | The UI3-style frontend: canvas (render worker), toolbar, layers, Design/Prototype/Inspect panels, colour picker, menus, dialogs, presenter, comments, accessibility (AccessKit), the control channel server |
|
||||
| L6 | `figcraft-mcp` | ~2.4k | MCP server (JSON-RPC over stdio), headless or bridged to a running app |
|
||||
| L6 | `figcraft-collab-net` | ~2.4k | Live sessions over the network: the WebSocket transport (tungstenite, a reader and a writer thread per connection), the session host the app embeds (runs the collaboration core's server), invite links, presence relay and limits; the only crate allowed networking dependencies |
|
||||
| apps | `figcraft`, `figcraft-cli`, `figcraft-web` | ~1.9k | Desktop app (eframe + wgpu), command line, browser build (WASM, WebGPU/WebGL2) |
|
||||
| L6 | `figcraft-collab-net` | ~3.0k | Live sessions over the network: the WebSocket transport (tungstenite, a reader and a writer thread per connection), the hub that runs a collaboration core server per open file for the session host the app embeds and for `figcraft-server` (behind a `Backend`), the joining and managing clients (publishing), invite links, presence relay and limits; the browser client; the only crate allowed networking dependencies |
|
||||
| apps | `figcraft`, `figcraft-cli`, `figcraft-web`, `figcraft-server` | ~3.2k | Desktop app (eframe + wgpu), command line, browser build (WASM, WebGPU/WebGL2), collaboration server (files, share links, persistence; [`server.md`](server.md)) |
|
||||
| plugins | `plugins/` (own workspace) | | Rust plugin SDK and two example plugins, built for wasm32 by `cargo xtask plugins` ([`plugins.md`](plugins.md)) |
|
||||
| tools | `xtask` | | `ci`, `layers`, `assets`, `parity`, `version`, `icon`, `wasm`, `plugins`, `bench` |
|
||||
|
||||
@@ -80,5 +80,6 @@ input.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | `collab-net` hosts through one hub for the app's sessions and the standalone server; added the `figcraft-server` app |
|
||||
| 2026-10-10 | minor | Merged main after #101–#103: WASM plugins (plugins area 0% → 35% breadth, 20% ready; ecosystem 5% → 20%) and the multiplayer core without networking (multiplayer 5% → 15% breadth, 5% ready; collaboration 8% → 12%); menu breadth 97.5% → 98.5% (200/203); ready for real work ~46% → ~47%; full parity 420–790 h → 390–745 h; beta 210–380 h → 200–365 h. Fixed the stated weight sums (areas 1–16 sum to 94, total 113; percentages were already computed with those) |
|
||||
| 2026-10-10 | major | Created from the code as of PR #99 |
|
||||
|
||||
+6
-1
@@ -6,6 +6,10 @@ commands, serve MCP and measure performance. Build it with
|
||||
|
||||
Bad arguments print usage and exit with code 2; failures exit with 1. The CLI never panics on input.
|
||||
|
||||
The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p
|
||||
figcraft-server`): it keeps files with share links and serves their live sessions, and its
|
||||
`key`, `files` and `links` subcommands manage them. See [server.md](server.md).
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `info <file> [--json]`
|
||||
@@ -99,7 +103,8 @@ machines.
|
||||
### `mcp [--attach PORT]`
|
||||
|
||||
Serve MCP on stdio: headless by default, or attached to a running app started with
|
||||
`figcraft --control PORT`. See [mcp.md](mcp.md).
|
||||
`figcraft --control PORT`. See [mcp.md](mcp.md). The headless session can host, join and publish
|
||||
live sessions (`collab.*`); the one-shot `run` can't (no network link).
|
||||
|
||||
### `perf [--layers N[,N...]]`
|
||||
|
||||
|
||||
+108
-23
@@ -10,8 +10,11 @@ handling and offline queueing with reconnect (`crates/collab`, wired into the en
|
||||
session and the transport). **Step 4 is implemented**: the browser build joins sessions a desktop
|
||||
app hosts, and `figcraft://` invite links open the app and join. Styles, variables, collections,
|
||||
modes and comments created in a session take ids from their creator's id space, and others' text
|
||||
carets show on the canvas. The rest is the plan the next
|
||||
implementation PRs follow and update; sections say what is built.
|
||||
carets show on the canvas. **Step 5 is implemented**: the
|
||||
standalone `figcraft-server` (`apps/figcraft-server`, running it: [`server.md`](server.md)) keeps
|
||||
many files with persistence and compaction, and share links that let people edit or only view;
|
||||
the app publishes to it. The rest is the plan the next implementation PRs follow and update;
|
||||
sections say what is built.
|
||||
|
||||
Goal: several people edit the same `.figcraft` file at the same time and see each other's edits,
|
||||
cursors and selections live, on the desktop app and in the browser, without a central cloud
|
||||
@@ -136,7 +139,7 @@ As built:
|
||||
hash, the encoded bytes as base64). The page order is the pages' positions.
|
||||
- **Next**: this file's published library (it embeds a snapshot document; it moves with an asset
|
||||
channel, together with images, which then stop travelling inside batches); the version history
|
||||
(kept beside the document, not in it) with the standalone server; finer entries for modes,
|
||||
(kept beside the document, not in it) on the standalone server; finer entries for modes,
|
||||
per-mode variable values and comment replies (see Identifiers).
|
||||
- **Changes** (`Change`): `Create {id, kind, parent, position, props}` (only the properties that
|
||||
differ from a new node of that kind), `Set {id, prop, value}`, `Delete {id}` (the subtree; a
|
||||
@@ -152,8 +155,9 @@ As built:
|
||||
|
||||
## Messages
|
||||
|
||||
*Built* (`figcraft_collab::message`): the types below and both encodings. The transport and the
|
||||
presence UI come in step 2.
|
||||
*Built* (`figcraft_collab::message`): the types below and both encodings. Protocol version 2 (the
|
||||
`Hello`'s `protocol`): the welcome says what the client may do, and a connection can manage a
|
||||
server's files instead of joining one; a client of another version is refused with the reason.
|
||||
|
||||
**Encoding (decided):** a binary message is a 1-byte wire version followed by **postcard** (serde),
|
||||
which is compact and needs no schema files. Property values inside are compact JSON text: the
|
||||
@@ -174,17 +178,20 @@ Client → server:
|
||||
| `Presence {cursor?, selection?, viewport?, editing_text? {node, caret, selection?}}` | Ephemeral; throttled to ~30 Hz, latest wins. |
|
||||
| `Ack {seq}` | Highest sequence applied (lets the server trim per-client buffers). |
|
||||
| `Ping` | Keep-alive. |
|
||||
| `Manage {protocol, key, client_version}` | Instead of `Hello`: manage a server's files with its key. |
|
||||
| `Admin {publish {name, snapshot} \| files \| links {file} \| createLink {file, access, label} \| revoke {file, link} \| remove {file}}` | A request on a managing connection; each gets one `Admin` reply, in order. |
|
||||
|
||||
Server → client:
|
||||
|
||||
| Message | Meaning |
|
||||
|---|---|
|
||||
| `Welcome {session, client_id, seq, catch_up: Snapshot or Delta, processed, peers}` | Join accepted: the full document, or for a reconnecting client the batches it missed; what the server did with that client's batches (`processed {last_batch, refused}`). |
|
||||
| `Welcome {session, client_id, seq, catch_up: Snapshot or Delta, processed, peers, access}` | Join accepted: the full document, or for a reconnecting client the batches it missed; what the server did with that client's batches (`processed {last_batch, refused}`); `access` is `edit` or `view` (a view link: every batch is refused). |
|
||||
| `Changes {seq, author, batch_id, changes}` | Accepted changes, in order, to everyone (the author gets its own back as confirmation). |
|
||||
| `Reject {batch_id, reason, fixes: [Change]}` | A batch (or part of it) was invalid: the current values to roll back to. |
|
||||
| `Presence {client_id, …}` / `PeerJoined` / `PeerLeft` | Other people's cursors, selections and arrivals. |
|
||||
| `Refused {reason}` | The connection is refused (protocol version, token, session full, a message out of place). |
|
||||
| `Pong` | Keep-alive. |
|
||||
| `Admin {ready \| published {file, edit, view} \| files \| links \| link \| done \| failed {reason}}` | A managing connection's answer: the key is right, a published file with its first two links (tokens shown once), listings, a new link, done, or why not. |
|
||||
|
||||
`Change` is one of: `Create {id, type, props}`, `Set {id, prop, value}`, `Delete {id}` (deleting a
|
||||
node deletes its subtree; ids are never allocated twice), plus the document-level `SetDoc`, `Put`
|
||||
@@ -352,11 +359,12 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
`ws://`/`wss://` URL). Others on the same network (or through a forwarded port / VPN such as a
|
||||
mesh VPN) join with "File ▸ Join live session" or by opening the link. When the host leaves, the
|
||||
session ends; everyone keeps a local copy.
|
||||
- **Self-hosted server** (next step): the same server as a `figcraft-server` binary that keeps many
|
||||
files, persists them (a snapshot plus an append-only log of accepted batches, compacted into new
|
||||
snapshots), and survives restarts. TLS comes from a reverse proxy (`wss://`).
|
||||
- **Auth**: an unguessable per-session token in the invite link (first step); accounts and per-file
|
||||
permissions (view, edit) come with the standalone server.
|
||||
- **Self-hosted server**: the same server as a `figcraft-server` binary that keeps many files,
|
||||
persists them (a snapshot plus an append-only log of accepted batches, compacted into new
|
||||
snapshots), and survives restarts. TLS comes from a reverse proxy (`wss://`). *Built*, below.
|
||||
- **Auth**: an unguessable per-session token in the invite link (hosting from the app); per-file
|
||||
share links that let people edit or only view, and a server key that manages files (the
|
||||
standalone server). User accounts come later.
|
||||
|
||||
*Built* (hosting from the app and joining):
|
||||
|
||||
@@ -426,8 +434,56 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
page refuses other hosts up front with a message instead of failing obscurely. To join from
|
||||
other machines, serve the web build over `http://` on a trusted network (locally:
|
||||
`python3 -m http.server 8767` in the built site, then `http://localhost:8767/` or the machine's
|
||||
LAN address); `wss://` comes with the standalone server behind TLS. Some browsers also ask
|
||||
before a public site may reach private network addresses.
|
||||
LAN address), or use `wss://` links of a `figcraft-server` behind a TLS reverse proxy (the
|
||||
browser opens them; see *Built (the standalone server)*). Some browsers also ask before a public
|
||||
site may reach private network addresses.
|
||||
|
||||
*Built* (the standalone server, `apps/figcraft-server`; running it: [`server.md`](server.md)):
|
||||
|
||||
- **One hub for both hosts.** The app's session host and the server run the same hub
|
||||
(`crates/collab-net/src/hub.rs`): the listener, a reader and a writer thread per connection,
|
||||
and one thread owning a core `Server` per open file. What differs sits behind a `Backend`: which
|
||||
hellos it admits and with what access, where a file that isn't open comes from, what it keeps
|
||||
of the changes, and the managing connections. The app's backend has one file and its session
|
||||
token; the server's is its data directory. Hellos carry the token to the backend, which checks
|
||||
it; the sessions themselves keep no secret.
|
||||
- **Files and share links.** Each file has links, each with an access (`edit` or `view`), a label
|
||||
and a 128-bit token stored only as its SHA-256 hash (compared in constant time). A hello names
|
||||
the file and a link's token; the server joins it with that link's access (`Server::join_as`). A
|
||||
viewer gets every change and everyone's presence and sends its own presence; each batch it
|
||||
sends is refused (`Reject`, "you can only view this file") and rolled back. Revoking a link
|
||||
(or removing a file) disconnects whoever joined with it, with the reason; the link no longer
|
||||
joins.
|
||||
- **Links** name the server's endpoint: `figcraft://join?host=<host:port>&file=…&token=…` for a
|
||||
plain `ws://` endpoint at `/`, `figcraft://join?server=<ws(s)://host[:port][/path]>&file=…&token=…`
|
||||
for any endpoint (TLS, a path behind a proxy), or `ws(s)://…?file=…&token=…`. The desktop app
|
||||
can't open `wss://` yet (no TLS of its own); the browser can.
|
||||
- **Managing files.** A connection that opens with `Manage {key}` instead of `Hello` sends
|
||||
`Admin` requests: publish a document (its snapshot) as a new file, which returns an edit link
|
||||
and a view link; list files and links; make and revoke links; remove a file. The server key is
|
||||
128 bits in the data directory. The server's command line (`figcraft-server files|links|key`)
|
||||
uses the same requests when a server is running on the directory, else it changes the directory
|
||||
itself (one process holds it at a time: an OS file lock).
|
||||
- **Publishing from the app.** `collab.publish {server, name?}` (Share ▸ Publish to server…)
|
||||
uploads the open document over a managing connection, then joins the new file with its edit
|
||||
link on the same connection thread; the tab becomes the session's synced document (read-only
|
||||
until the welcome arrives, its undo history per person after) and `collab.peers` reports
|
||||
`published {server, file, name, edit, view}`.
|
||||
- **Persistence.** Each open file's server keeps a journal of what changes its lasting state
|
||||
(`Server::journaling`: client ids handed out, accepted batches, refused batch ids); the store
|
||||
appends it to the file's log and syncs it once per burst of messages, before the hub sends what
|
||||
the server said, so a confirmed edit is never lost to a crash. A restart restores the last
|
||||
snapshot (`Server::restore`) and replays the log (`Server::replay`; entries already in the
|
||||
snapshot change nothing, a log that stops making sense is cut there). Compaction writes the
|
||||
next generation's snapshot (`Server::state`) and an empty log of that generation (a log of an
|
||||
older generation is from a compaction cut short and is ignored), after 1,000 entries or 16 MiB
|
||||
of log, when everyone leaves (the file is then closed) and when the server stops. Snapshots and
|
||||
logs carry CRC-32 checksums; writes go to a temporary file that is flushed and renamed.
|
||||
Damaged files are skipped and reported at startup, never deleted; a damaged log end is dropped.
|
||||
- **Restarts.** A stopping server closes connections with code 1012 (service restart), which
|
||||
clients take as a dropped connection: they reconnect with their usual backoff and resume as the
|
||||
same clients (the session id, client ids and log come back with the file), sending what they
|
||||
made meanwhile. The app's session ending (code 1001) stays final.
|
||||
|
||||
*Built* (opening invite links):
|
||||
|
||||
@@ -487,7 +543,8 @@ Decisions, as built:
|
||||
- **Session state.** `Server::state` returns what a server needs to carry on after a restart
|
||||
(`SessionState`: snapshot, sequence number, session id, client-id counter, the clients that can
|
||||
reconnect and what was done with their batches, deleted ids, the log), as one serde value; the
|
||||
standalone server will store it.
|
||||
standalone server stores it as each file's snapshot, with a journal of the changes since
|
||||
(`Server::journaling`, `Server::replay`).
|
||||
|
||||
Tests: random sessions where clients drop and come back mid-session converge, with the default log
|
||||
and with logs short enough that long absences fall back to snapshots; targeted tests cover a lost
|
||||
@@ -496,9 +553,9 @@ saved state.
|
||||
|
||||
## Agents, MCP and the CLI
|
||||
|
||||
Everything stays a command: `collab.host`, `collab.join`, `collab.leave`, `collab.peers`,
|
||||
`collab.follow` are ordinary commands, so the CLI, the control channel and MCP can host and join
|
||||
sessions; an agent can be a participant with its own cursor.
|
||||
Everything stays a command: `collab.host`, `collab.join`, `collab.publish`, `collab.leave`,
|
||||
`collab.peers`, `collab.follow` are ordinary commands, so the CLI, the control channel and MCP can
|
||||
host, publish and join sessions; an agent can be a participant with its own cursor.
|
||||
|
||||
*Built*: the desktop app (control channel) and the MCP server's headless session (`figcraft-cli
|
||||
mcp`) both have the network link installed; `collab.join` returns at once with status
|
||||
@@ -522,12 +579,12 @@ chooses to share on the network; the default is the invite link with a random to
|
||||
popover says who can join.
|
||||
- **The token.** 128 bits from the operating system's random number generator, URL-safe base64,
|
||||
new for every session (stopping and starting again invalidates old links). It is compared in
|
||||
constant time (the core's `Server`). It is in the invite link by design, so whoever holds the link
|
||||
constant time (the hub's backend). It is in the invite link by design, so whoever holds the link
|
||||
can join until the session stops: share it like a password. It never travels in the WebSocket
|
||||
request (the path is `/`), so proxies don't log it; FigCraft never logs it (logs name addresses
|
||||
and ports only), and errors about links never echo the link. Traffic is not encrypted (`ws://`):
|
||||
on a shared network, others on the path could read presence and the token. TLS (`wss://`) comes
|
||||
with the standalone server behind a reverse proxy; until then, use a trusted LAN or a VPN.
|
||||
on a shared network, others on the path could read presence and the token. Use a trusted LAN
|
||||
or a VPN; TLS (`wss://`) comes from a reverse proxy in front of a `figcraft-server`.
|
||||
- **Limits.** At most 64 open sockets (joined or not) and the core's 256 people; a connection must
|
||||
finish the WebSocket handshake and say hello within 10 s, and one silent for 45 s is dropped
|
||||
(clients ping every 15 s); client messages are at most 16 MiB (what the host sends keeps the
|
||||
@@ -536,8 +593,32 @@ chooses to share on the network; the default is the invite link with a random to
|
||||
outgoing queue fills (it can't keep up); names are cut to 64 characters without control
|
||||
characters; presence numbers must be finite and selections are cut to 10,000 ids.
|
||||
- **Malformed input.** Anything that isn't a binary message of the current wire version that
|
||||
decodes in full is answered with `Refused` and a close; the first message must be `Hello`, for
|
||||
this file. The invite parser and the host's reader are fuzzed (`crates/collab-net`).
|
||||
decodes in full is answered with `Refused` and a close; the first message must be `Hello` (for
|
||||
this file) or, on a server, `Manage`. The invite parser and the host's reader are fuzzed
|
||||
(`crates/collab-net`), and so are the server's readers of its own files and its port
|
||||
(`apps/figcraft-server`).
|
||||
|
||||
*Built* (the standalone server), and what it adds to the threat model:
|
||||
|
||||
- **Who can connect.** Whoever can reach the server's port (127.0.0.1 by default; the operator
|
||||
chooses an interface or a reverse proxy). Joining a file needs one of its link tokens; managing
|
||||
files needs the server key. There are no accounts: a link is a bearer secret for one file and
|
||||
one access level, so share it like a password and revoke it when it leaks; the server key is
|
||||
the administrator's and should stay with trusted people.
|
||||
- **Secrets at rest.** Link tokens are kept only as SHA-256 hashes, so a copy of the data
|
||||
directory can't be used to join (the documents themselves are in it, unencrypted). The server
|
||||
key is plain text in the data directory (owner-only on Unix): reading the directory already
|
||||
gives every file. A lost link can't be shown again; revoke it and make a new one.
|
||||
- **View access is enforced by the server**, not by the app: a viewer's batches are refused
|
||||
whatever client sends them; the app's read-only state is for clarity.
|
||||
- **Abuse limits.** On top of the transport's: at most `max_files` files, `max_file_mb` per file
|
||||
(a publish over it is refused), `max_links` links per file, `max_people` per file and
|
||||
`max_connections` sockets.
|
||||
- **TLS.** None in the server: a reverse proxy terminates it. Tokens and keys are never in the
|
||||
WebSocket request (only in the first message), so proxies don't log them, and the server logs
|
||||
file and link ids, never secrets.
|
||||
- **Not yet**: accounts and per-person permissions, per-address rate limits, audit logs,
|
||||
encryption at rest.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
@@ -558,4 +639,8 @@ chooses to share on the network; the default is the invite link with a random to
|
||||
carets on the canvas.
|
||||
4. **Browser client** (WebSocket through `web-sys`). **Done** (the web build joins
|
||||
desktop-hosted sessions; `figcraft://` links open the desktop app and join).
|
||||
5. **Standalone `figcraft-server`** with persistence, compaction and accounts.
|
||||
5. **Standalone `figcraft-server`** with persistence, compaction, share links and permissions.
|
||||
**Done** (`apps/figcraft-server` on the shared hub; edit and view links, the server key,
|
||||
publishing from the app; see [`server.md`](server.md)).
|
||||
6. **Accounts**: people signing in, per-person permissions, teams and projects; TLS in the
|
||||
desktop app.
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
# Collaboration parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (wording: live sessions, the browser joins them, invite links open the app; collaboration polish: collision-free ids, remote text carets; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Figma is multiplayer first; FigCraft is local first. **Ready ~12% (estimated):** comments and version
|
||||
history work inside one file; the real-time collaboration core landed in #103 (sync model,
|
||||
server ordering, client reconciliation, convergence tests), and live sessions (preview) let
|
||||
several apps co-edit a file hosted by one desktop app, joined from the desktop app or the browser,
|
||||
with others' cursors, selections and text carets shown live ([`collab-protocol.md`](collab-protocol.md)). Remaining: 55–110 h
|
||||
several apps co-edit a file hosted by one desktop app or kept on a self-hosted `figcraft-server`
|
||||
with share links to edit or view, joined from the desktop app or the browser, with others'
|
||||
cursors, selections and text carets shown live ([`collab-protocol.md`](collab-protocol.md), [`server.md`](server.md)). Remaining: 55–110 h
|
||||
(multiplayer 45–90 h, shared libraries 10–20 h), plus branching 8–15 h.
|
||||
|
||||
| Feature | Figma | FigCraft | Hours |
|
||||
|---|---|---|---|
|
||||
| Real-time co-editing | yes | **core built, no networking** (#103): client-scoped ids, per-property change sets with fractional positions, server ordering and validation, optimistic clients, binary messages; randomized multi-client convergence tests over real commands. Live sessions over the network landed (`crates/collab-net`): the WebSocket transport (keep-alive, size and rate limits, reconnect with backoff), hosting from the app (this machine or the local network) with invite links, live cursors with name tags, selections, avatars and follow, `collab.*` commands. Document sync between apps works over them (the host's server orders every batch; guests open the host's file as a new tab). Undo is per person (only your own steps, others' later writes left alone), refused edits roll back with a toast, and a reconnect resumes as the same client with what it missed (or a snapshot) and sends what was made offline. The browser build joins (same messages through the browser's WebSocket API); `figcraft://` invite links open the desktop app and join. Styles, variables, collections, modes and comments made in a session take ids from their creator's space (the server checks it), so concurrent creations all survive; modes, per-mode values and replies inside one entry still resolve last-writer-wins. Left: standalone server with TLS | 25–55 |
|
||||
| Real-time co-editing | yes | **core built, no networking** (#103): client-scoped ids, per-property change sets with fractional positions, server ordering and validation, optimistic clients, binary messages; randomized multi-client convergence tests over real commands. Live sessions over the network landed (`crates/collab-net`): the WebSocket transport (keep-alive, size and rate limits, reconnect with backoff), hosting from the app (this machine or the local network) with invite links, live cursors with name tags, selections, avatars and follow, `collab.*` commands. Document sync between apps works over them (the host's server orders every batch; guests open the host's file as a new tab). Undo is per person (only your own steps, others' later writes left alone), refused edits roll back with a toast, and a reconnect resumes as the same client with what it missed (or a snapshot) and sends what was made offline. The browser build joins (same messages through the browser's WebSocket API); `figcraft://` invite links open the desktop app and join. Styles, variables, collections, modes and comments made in a session take ids from their creator's space (the server checks it), so concurrent creations all survive; modes, per-mode values and replies inside one entry still resolve last-writer-wins. A standalone `figcraft-server` keeps many files, persists them (a snapshot plus a log, compacted), loses nothing confirmed across a restart (apps resume as the same participants) and takes files published from the app (Share ▸ Publish to server…). Left: TLS in the desktop app (browsers use `wss://` through a reverse proxy), packaging the server | 25–55 |
|
||||
| Live cursors, selections, viewports; follow / observation mode; spotlight | yes | **partial, in live sessions**: named cursors, selection outlines, avatars, follow (click an avatar), others' text carets and selected characters with their names, a note when two people type in one layer; no spotlight | 8–15 |
|
||||
| Cursor chat, audio, high-fives | yes | no | 4–8 |
|
||||
| Share links, permissions (view / edit), teams and projects | yes | no | 8–17 |
|
||||
| Share links, permissions (view / edit), teams and projects | yes | **partial**: `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. No accounts, teams or projects | 8–17 |
|
||||
| Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences | (shared once multiplayer lands) |
|
||||
| Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file | — |
|
||||
| Branching and merging | yes (paid plans) | no | 8–15 |
|
||||
@@ -25,6 +26,7 @@ with others' cursors, selections and text carets shown live ([`collab-protocol.m
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged |
|
||||
| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-10 | minor | Merged main after #101–#103: WASM plugins (plugins area 0% → 35% breadth, 20% ready; ecosystem 5% → 20%) and the multiplayer core without networking (multiplayer 5% → 15% breadth, 5% ready; collaboration 8% → 12%); menu breadth 97.5% → 98.5% (200/203); ready for real work ~46% → ~47%; full parity 420–790 h → 390–745 h; beta 210–380 h → 200–365 h. Fixed the stated weight sums (areas 1–16 sum to 94, total 113; percentages were already computed with those) |
|
||||
|
||||
@@ -90,10 +90,12 @@ pointer press, so the focus ring shows) and the name of the `control` with the k
|
||||
screen reader announces; empty when nothing has it). `reduceMotion` is true when the preference or
|
||||
the system's accessibility setting turns the interface's animations off. `live` is the live
|
||||
session as the window shows it: `status`, `role`, the number of `peers`, who you `follow`, the
|
||||
peers whose cursors are drawn on the canvas (`cursorsShown`) and whether the Join dialog is open
|
||||
(`joinDialog`; Escape closes it); the session itself is `engine.execute {command: "collab.peers"}`
|
||||
(hosting and joining are `collab.host` and `collab.join`, opening invite links
|
||||
`collab.registerLinks`, see [mcp.md](mcp.md#live-sessions)). The app also joins the invite link it
|
||||
peers whose cursors are drawn on the canvas (`cursorsShown`), whether the Join and Publish to
|
||||
server dialogs are open (`joinDialog`, `publishDialog`; Escape closes them) and why the active
|
||||
document can't be edited now (`readOnly`: the "View only" or "Publishing" reason, else null); the
|
||||
session itself is `engine.execute {command: "collab.peers"}` (hosting, joining and publishing are
|
||||
`collab.host`, `collab.join` and `collab.publish`, opening invite links `collab.registerLinks`,
|
||||
see [mcp.md](mcp.md#live-sessions)). The app also joins the invite link it
|
||||
was started with (`figcraft "figcraft://join?…"`).
|
||||
|
||||
### `ui.pointer {kind, x, y | sx, sy, to?, steps?, shift?, alt?, cmd?, ctrl?}`
|
||||
|
||||
+15
-9
@@ -1,6 +1,6 @@
|
||||
# Where FigCraft falls short of Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (live boolean editing and the vector tail: feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the standalone server with share links to edit or view, wording only; live boolean editing and the vector tail: feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
|
||||
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
|
||||
@@ -15,7 +15,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
| # | Gap | Kind | Hours | Doc |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `.fig` files can't be opened (**beta blocker**) | File format | 30–60 | [file-format-parity](file-format-parity.md) |
|
||||
| 2 | No real-time multiplayer or sharing (**beta blocker**) | Feature | 45–90 | [collaboration-parity](collaboration-parity.md) |
|
||||
| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 45–90 | [collaboration-parity](collaboration-parity.md) |
|
||||
| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) |
|
||||
| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) |
|
||||
| 5 | No localization layer; English only | Localization | 60–100 | [localization-parity](localization-parity.md) |
|
||||
@@ -54,21 +54,26 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
model), with a corpus of synthetic `.fig` files saved from the installed Figma. Export to `.fig`
|
||||
is a further 15–30 h and lower priority.
|
||||
|
||||
### 2. No real-time multiplayer or sharing
|
||||
- **Missing:** co-editing, live cursors and selections, observation mode, cursor chat, spotlight,
|
||||
audio, share links and permissions. Comments and versions are local to the file.
|
||||
### 2. Multiplayer and sharing are a preview, without accounts
|
||||
- **Missing:** accounts, teams and projects, per-person permissions, spotlight, cursor chat, audio,
|
||||
shared comments and version history, TLS in the desktop app. Comments and versions are local to
|
||||
the file.
|
||||
- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server
|
||||
ordering and validation, client reconciliation, binary messages, randomized convergence tests over
|
||||
real commands), and live sessions run over the network (`crates/collab-net`: hosting from the
|
||||
app, invite links that open the app, document sync between apps, live cursors, selections,
|
||||
text carets, avatars and follow, and the browser build joins) with per-person undo, refused
|
||||
edits rolled back with a reason, reconnects that resume and send what was made offline, and
|
||||
styles, variables and comments that several people create at once all kept; but sessions are
|
||||
plain `ws://` (no TLS, so an https page can only join on the same computer), and there are no
|
||||
share links or permissions (`docs/collab-protocol.md`).
|
||||
styles, variables and comments that several people create at once all kept; a standalone
|
||||
`figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing
|
||||
confirmed), takes files published from the app and hands out share links that let people edit
|
||||
or only view (`docs/server.md`). But there are no accounts (a link is the access), and the
|
||||
desktop app speaks only plain `ws://` (TLS comes from a reverse proxy, which only the browser
|
||||
can use so far) (`docs/collab-protocol.md`).
|
||||
- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work
|
||||
without it.
|
||||
- **Estimate:** 45–90 h: sharing and permissions and a standalone server with TLS.
|
||||
- **Estimate:** 45–90 h: accounts, teams and per-person permissions, TLS in the desktop app,
|
||||
shared comments and versions (numbers from before the server landed; re-measured next).
|
||||
|
||||
### 3. Nothing checked against real Figma files
|
||||
- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and
|
||||
@@ -225,6 +230,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h |
|
||||
| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged |
|
||||
| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure |
|
||||
|
||||
+14
-2
@@ -529,12 +529,24 @@ joined file opens as a new document).
|
||||
`collab.peers` does, with `link` (`figcraft://join?host=…&file=…&token=…`) and `url`
|
||||
(`ws://…/?file=…&token=…`): both carry the session's secret, so share them only with people you
|
||||
invite. `collab.stop {}` ends it.
|
||||
- `collab.join {link}` (either form) returns at once with `status: "connecting"`; poll
|
||||
- `collab.join {link}` (any form: an app's invite, a `figcraft-server` share link, or the
|
||||
`ws(s)://…?file=…&token=…` URL) returns at once with `status: "connecting"`; poll
|
||||
`collab.peers` until `status` is `"live"`, or `"failed"` with a `reason` (a wrong token, a full
|
||||
session, an unreachable host). `collab.leave {}` leaves.
|
||||
session, a revoked link, an unreachable host). `collab.leave {}` leaves; the synced document
|
||||
stays open as your copy.
|
||||
- `collab.publish {server, name?, key?}` puts the active document on a `figcraft-server`
|
||||
([`server.md`](server.md)) and syncs it from there: `server` is the address
|
||||
`figcraft-server key` prints (`ws://host:47900/?key=…`; or give `key` separately), `name` the
|
||||
file's name (default: the document's). It returns at once with `status: "connecting"`; the
|
||||
document is read-only until the upload is done, then `collab.peers` has `published: {server,
|
||||
file, name, edit, view}` with the file's first edit link and view link (they carry their
|
||||
tokens: share them like passwords). Someone joining with the view link gets `access: "view"`:
|
||||
every edit on the synced document is refused with "View only: …" (the server refuses them too).
|
||||
- `collab.peers {}` → `{available, canHost (false in the web build), status
|
||||
(off|connecting|live|reconnecting|failed), reason?,
|
||||
attempt?, role (host|guest), me, name, color, fileName, pages, link, url, address, network,
|
||||
access (edit|view), readOnly (the active document can't be edited now: view only, or being
|
||||
published), published (a file you published: {server, file, name, edit, view}),
|
||||
follow, syncing (the document arrived), document (the index of the synced open document),
|
||||
editing (the text layer you are typing in: {node, layer, caret, selection}, or null), peers: [{id,
|
||||
name, color, editing (the text layer they are typing in, same shape, or null), presence: {cursor:
|
||||
|
||||
@@ -70,6 +70,14 @@ is in the workspace: `packaging/env.sh` sets `BINS`/`HAS_CLI` from that, and `pa
|
||||
`figcraft --version` prints the version and, in CI builds, the commit (`FIGCRAFT_BUILD_SHA`,
|
||||
read by `apps/figcraft/build.rs`).
|
||||
|
||||
**The collaboration server** (`apps/figcraft-server`, binary `figcraft-server`, see
|
||||
[`server.md`](server.md)) is in the workspace but not packaged yet: releases don't ship it, and
|
||||
operators build it with `cargo build --release -p figcraft-server`. Shipping it means a
|
||||
`figcraft-server-<v>-<platform>-<arch>` archive per desktop platform (a headless binary: no
|
||||
bundle, installer entry or desktop file), signed like `figcraft-cli` on macOS and Windows; that
|
||||
is a change to `release.yml` and `packaging/` of its own, not part of `BINS` (which every
|
||||
desktop package installs).
|
||||
|
||||
**Fonts.** Every build job checks out [craft-fonts](https://github.com/storytold/craft-fonts) at
|
||||
`CRAFT_FONTS_REF` (top of `release.yml`) and builds with `CRAFT_FONTS_DIR` and
|
||||
`CRAFT_FONTS_REQUIRED=1`, so releases embed its fonts and fail rather than ship without them.
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
# FigCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (live boolean editing and the vector tail: feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the standalone server with share links to edit or view, wording only; live boolean editing and the vector tail: feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
|
||||
@@ -17,7 +17,7 @@ are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint cr
|
||||
40–60 h, parallel by tool.
|
||||
4. **Localization layer**, then Figma's own languages first (ja, ko, es, pt-BR, fr, de): 15–25 h
|
||||
for the layer, 3–5 h per language plus native review.
|
||||
5. **Multiplayer M9**: a standalone server with TLS and sharing on top of the live sessions (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, and joining from the browser landed), per [`collab-protocol.md`](collab-protocol.md): 45–90 h.
|
||||
5. **Multiplayer M9**: accounts, teams and TLS in the desktop app on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h.
|
||||
|
||||
## Alpha gate
|
||||
|
||||
@@ -31,7 +31,7 @@ builders' side-by-side checks with synthetic files; nothing has been run on real
|
||||
| Build and use a design system: components, variants, properties, variables and modes, libraries | yes | `components_scale_tests.rs` (300 components, 2,000 instances), `library_tests.rs`, `library_variables_tests.rs`; libraries are local files, not shared | — |
|
||||
| Prototype and present: interactions, flows, overlays, smart animate, variables | yes | `proto_*_tests.rs`, `present_*_tests.rs` | — |
|
||||
| Hand off to developers: Dev Mode inspect, code, measurements, asset export, ready for dev | yes | `devinspect_tests.rs`, `component_codegen_tests.rs`, `devcompare_tests.rs`, `annotations_tests.rs` | — |
|
||||
| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app, joined from the app or the browser) but no share links or per-person undo yet ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 |
|
||||
| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app or a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, undo per person); no accounts or teams ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 |
|
||||
| Continue existing Figma work (open `.fig`) | no (beta gap) | no `.fig` reader; REST JSON import works for files fetched through Figma's API ([file-format-parity](file-format-parity.md)) | beta gap: 30–60 |
|
||||
|
||||
**Passes, so FigCraft stays alpha:** every daily design workflow works end to end on FigCraft's
|
||||
@@ -55,7 +55,7 @@ blockers.
|
||||
| M6 Interop & Dev Mode | ~55% | `.fig` import, video/HEIC/TIFF, CMYK intent, Code Connect | 40–80 |
|
||||
| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 |
|
||||
| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 |
|
||||
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme) | standalone server with TLS, sharing | 45–90 |
|
||||
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable) | accounts, teams, TLS in the desktop app, packaging the server | 45–90 |
|
||||
| M10 Localization (new) | not started | layer + 11 languages | 60–100 |
|
||||
| M11 Plugins | runtime, API, menu, manager and SDK done (#101) | run-time plugin UI, settings storage, distribution, widgets; Figma plugin compatibility is an owner decision | 25–55 |
|
||||
| M12 AI features (new, owner decision) | not started | local models | 30–60 |
|
||||
@@ -71,6 +71,7 @@ blockers.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h |
|
||||
| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged |
|
||||
| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure |
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
# 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.
|
||||
|
||||
The protocol and the design behind it are in [`collab-protocol.md`](collab-protocol.md).
|
||||
|
||||
## Running it
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
```sh
|
||||
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`](mcp.md#live-sessions)).
|
||||
|
||||
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.
|
||||
|
||||
## Managing files and links
|
||||
|
||||
| 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
|
||||
|
||||
```text
|
||||
<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
|
||||
```
|
||||
|
||||
- **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.
|
||||
- **Not there yet**: user accounts, per-person permissions, rate limits per address, audit logs.
|
||||
@@ -1,6 +1,6 @@
|
||||
# FigCraft parity with Figma
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (live boolean editing and the vector tail: feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (the standalone server with share links to edit or view, wording only; live boolean editing and the vector tail: feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
|
||||
|
||||
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
|
||||
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
|
||||
@@ -110,7 +110,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|
||||
| 14 | Import/export | 6 | 65% | 35% | **`.fig` import** (the blocking gap), `.fig` export, HEIC/TIFF/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 30–60 |
|
||||
| 15 | Dev Mode / inspect | 2 | 80% | 62% | Code Connect, plugins in Dev Mode, editor integrations ([dev-mode-parity](dev-mode-parity.md)) | 4–8 |
|
||||
| 16 | Comments, versions, find, misc | 3 | 80% | 58% | Comments are local only; branching; Open recent | 4–8 |
|
||||
| 17 | Multiplayer & sharing | 8 | 15% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow. Left: per-person undo, offline resume, cursor chat, share links and permissions ([collaboration-parity](collaboration-parity.md)) | 45–90 |
|
||||
| 17 | Multiplayer & sharing | 8 | 15% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view. Left: accounts and teams, cursor chat, TLS in the desktop app ([collaboration-parity](collaboration-parity.md)) | 45–90 |
|
||||
| 18 | Plugins & widgets | 4 | 35% | 20% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK, 2 examples). Left: plugin UI at run time, settings storage, distribution, widgets; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 25–55 |
|
||||
| 19 | Team libraries & organization | 2 | 40% | 25% | Local library files exist; no shared/cloud library, no library analytics | 10–20 |
|
||||
| 20 | Figma Draw extras | 2 | 45% | 30% | Have: variable width, lasso, shape builder, noise/texture. Missing: brushes (stretch/scatter), pressure, text on a path, pattern fills, multi-edit | 10–18 |
|
||||
@@ -125,7 +125,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|
||||
| Features (depth) | 35 | 57% | 71–135 (areas 1–13, 15, 16, 20) | The table above; core editor ~67% | this file |
|
||||
| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (cursors, modifiers while dragging, hover states, popovers) not yet compared; accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) |
|
||||
| File formats | 12 | 40% | 30–60 | No `.fig` read or write; REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid | [file-format-parity](file-format-parity.md) |
|
||||
| Collaboration | 10 | 12% | 55–110 | Local comments and version history; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app, joined from the app or the browser (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) |
|
||||
| Collaboration | 10 | 12% | 55–110 | Local comments and version history; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) |
|
||||
| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) |
|
||||
| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) |
|
||||
| Hardware | 3 | 50% | 15–30 | Trackpad pinch and HiDPI yes; no pen pressure, no GPU canvas, no P3 on screen, one window | [hardware-parity](hardware-parity.md) |
|
||||
@@ -577,6 +577,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h |
|
||||
| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure |
|
||||
| 2026-10-10 | minor | Merged main after #101–#103: WASM plugins (plugins area 0% → 35% breadth, 20% ready; ecosystem 5% → 20%) and the multiplayer core without networking (multiplayer 5% → 15% breadth, 5% ready; collaboration 8% → 12%); menu breadth 97.5% → 98.5% (200/203); ready for real work ~46% → ~47%; full parity 420–790 h → 390–745 h; beta 210–380 h → 200–365 h. Fixed the stated weight sums (areas 1–16 sum to 94, total 113; percentages were already computed with those) |
|
||||
|
||||
Reference in New Issue
Block a user