Setup
Installing
Windows and Linux have prebuilt installers on the latest release. Download the one for your platform and run it.
macOS builds from source. A notarized download needs a paid Apple Developer
membership, and this is a free open-source project. An unnotarized .app
downloaded from the internet would be blocked by Gatekeeper. Building on your own
Mac sidesteps that completely because locally compiled apps are never
quarantined, so the result opens like any other app. See below.
Building on macOS
You do not need to know Rust. The script checks for everything and offers to install anything missing.
- Download the source (Code → Download ZIP on GitHub, or
git clone). - Open the
scriptsfolder. - Double-click Build subScribe (macOS).command.
It verifies Xcode Command Line Tools, Node, and Rust, installs whatever is absent (asking first), builds the app, and offers to move it to Applications. The first build takes several minutes because Rust compiles from scratch. Later builds are much quicker.
If Finder refuses to run the script because it came from a ZIP download, either
run xattr -dr com.apple.quarantine on the folder, or start it from Terminal:
./scripts/"Build subScribe (macOS).command"
Microphone or camera prompt never appears
macOS records permission decisions per app identity. If a previous build was ever denied (or a stale record exists), the built app is refused instantly with no prompt, and locally built apps sometimes don't even appear in System Settings → Privacy & Security. Clear the stale records and relaunch:
tccutil reset Microphone com.writform.desktop
tccutil reset Camera com.writform.desktop
The next time subScribe asks, the real system prompt appears.
Host on this computer (easiest)
- Install subScribe and open it. You're at Home, and can already write on this device.
- Choose Host on this computer (on Home, or from the account button at
the foot of the rail), pick a server name and port (default
7311), and press Start hosting. - Create the first account. The first registration becomes the server admin.
- Open Settings → Server for the addresses to share with friends.
Your server starts automatically whenever the app runs. Data lives in the
app's data directory under server/.
Join someone's server
- Choose Join a server and enter the address they gave you
(e.g.
192.168.1.20:7311). - Compare the identity fingerprint the app shows with the one your host sees. This is the trust-on-first-use check.
- Create your account on that server.
Standalone server (optional)
For an always-on machine, run the bare server instead of the app:
writform-server --data-dir /var/lib/writform --port 7311
# or
docker run -d --name writform --restart unless-stopped \
-p 7311:7311 -v writform:/data ghcr.io/hullabaloo-vincent/writform-server
A systemd unit ships in the repo under deploy/. For upgrading either
flavor, see Updating a server below.
Each setting is an environment variable, or a flag of the same name
(--port, --registration and so on):
| Variable | Default | What it does |
|---|---|---|
WRITFORM_DATA_DIR |
./data |
Where the database, attachments and identity key live |
WRITFORM_BIND |
0.0.0.0 |
The address to listen on |
WRITFORM_PORT |
7311 |
The port to listen on |
WRITFORM_SERVER_NAME |
subScribe Server | The name people see when they connect |
WRITFORM_WEB_DIR |
none | Serve the browser client from this folder |
WRITFORM_REGISTRATION |
none | Who may make an account: open, invite or closed. Unset, an admin chooses in the app |
WRITFORM_TLS |
on |
off serves plain HTTP for a proxy or tunnel in front that speaks TLS |
WRITFORM_TRUST_PROXY |
none | Behind a proxy, read each person's address from cloudflare (CF-Connecting-IP) or forwarded (X-Forwarded-For) |
WRITFORM_LINK_PREVIEWS |
on |
off stops the server fetching the links members post |
WRITFORM_FONT_PROXY |
on |
off stops the server fetching fonts for its members |
To keep the server's address private behind Cloudflare, see Networking.
Who can make an account
The first account on a new server is always allowed: it becomes the server
admin. After that, an admin chooses in Settings ▸ Admin ▸ New accounts:
anyone who can reach the server, only someone with an invite code, or no
one. Invite codes are made there too, each for a number of accounts and a
number of days, and can be revoked. WRITFORM_REGISTRATION fixes the
choice for the server, and the app then shows it without letting it be
changed.
Using subScribe in a browser (phones included)
The server can serve the app itself: open https://your-server:7311/ in
any browser (including on a phone), log in, and you get the same chat,
documents, canvas, sessions, and voice in a mobile-friendly layout. The
Docker image ships with this enabled. For a bare-metal server, build the
client once (npm run build in apps/desktop) and start the server with
--web-dir apps/desktop/dist (or set WRITFORM_WEB_DIR).
Fonts. Documents can use families from an online library of open
fonts. Your server fetches each font file the first time anyone needs it
(from the library's CDN, jsDelivr) and keeps it under fonts/ in its data
folder, so everyone else (and every browser) gets it from you. To keep
the server from making that request, set WRITFORM_FONT_PROXY=off. Fonts
already fetched keep working, and the desktop apps can still download
directly if their own switch (Settings ▸ Fonts) is on.
Notes to know: your browser will warn about the self-signed certificate the first time. Proceed after checking the fingerprint with the host (the desktop app's pinned-identity check doesn't exist in browsers, which is why the desktop app remains the most secure way to connect). Desktop-only features (the notes vault, on-device documents, the portable profile, hosting, plugins) don't appear in the browser.
Updates
The desktop app updates itself from GitHub Releases (Settings → Application → Check for updates). Packages are signature-verified before installing.
Updating a server
Everything that makes your server your server (the identity key behind the fingerprint, the TLS certificate, the account database, and uploaded attachments) lives in the data directory, not in the binary or the container. As long as you keep that directory, you can swap the server underneath it freely: the fingerprint stays the same, nobody has to re-pair or re-register, and database migrations run automatically the first time the new version starts.
Docker
Containers are disposable. The named volume (writform below) is what
carries your data. An update is: pull the new image, replace the container,
keep the volume.
Updates that change the database (their release notes say so, for example the one adding Recently Deleted and full-text search to documents) migrate it on first start. A copy of the volume beforehand is cheap insurance:
docker run --rm -v writform:/data -v "$PWD":/backup alpine tar czf /backup/writform-data.tgz -C /data .
docker pull ghcr.io/hullabaloo-vincent/writform-server
docker stop writform && docker rm writform
docker run -d --name writform --restart unless-stopped \
-p 7311:7311 -v writform:/data ghcr.io/hullabaloo-vincent/writform-server
(If you started the old container without --name, find its generated name
with docker ps first.)
docker rm deletes only the container layer. The volume is untouched. The
only commands that would reset your server's identity are
docker volume rm writform or a docker run without the -v writform:/data
mount, which starts from a blank data directory. Neither is ever part of an
update.
Bare binary
Replace writform-server with the new release's binary and restart it (or
systemctl restart writform-server if you use the shipped unit), keeping
the same --data-dir.
Verifying
Connect from the app. Connecting cleanly is the fingerprint check, because clients pin the server identity and warn loudly if it changed. Where to check versions: the server prints its version at startup, and a client that's newer than the server will tell you when it needs something the server doesn't have yet ("the server is running an older subScribe version…"). That message is the cue to run the update above.
Update the server before the apps start using new formatting. Text color, highlight, fonts and sizes on part of a paragraph, indents, borders, checklists, tables, footnotes, a table of contents, sections and suggested changes (from version 1.1.0), and a table cell's own borders and a picture's outline and rounded corners (from 1.1.2), are stored in a newer document format. A server that hasn't been updated can't hold it: the apps simply don't offer those controls for its documents, and a document from a device that already uses them can only be published there as a simplified copy. Once the server is updated the controls appear (within a few minutes, or the next time a document is opened). After a document uses them, people on an older app are told to update before they can open it.
Permission denied on an old volume: volumes first created by releases up
through v0.5.0 are owned by root, so after an update the (unprivileged)
server can exit with permission denied (os error 13). Fix the ownership
once and it stays fixed:
docker run --rm --user root --entrypoint chown \
-v writform:/data ghcr.io/hullabaloo-vincent/writform-server -R writform /data
Deleting a server
Deleting a server erases everything on it for every member (accounts, groups, messages, documents, uploads) and retires its identity, so the fingerprint can never be reused. There is no undo. If you might want the server back later, take a copy of its data directory first (that copy is the server: restore it and the same fingerprint comes back).
- Hosted in the app: stop hosting, then delete the
server/folder inside the app's data directory (that folder is the entire server: identity, database, attachments). - Docker: remove the container, then the volume (this is the step that actually destroys the data):
docker stop writform && docker rm writform
docker volume rm writform
- Bare binary: stop the process (
systemctl disable --now writform-serverif you use the unit) and delete its--data-dir.
Friends who had the server saved can tidy it off their list of servers with the Forget this server button on its card.