A self-hosted fork of Relay, the CRDT-based multiplayer plugin for Obsidian, bundled with its control-plane backend so you can run the whole stack yourself.
This repo is a flat monorepo (both directories are in-tree, not submodules):
| Path | What it is | Upstream |
|---|---|---|
Relay/ |
Obsidian plugin (TypeScript → esbuild). | No-Instructions/Relay (pulled via subtree) |
relay-control-plane/ |
PocketBase admin + JS hook + relay-server (y-sweet Yjs sync), via Docker. |
Internal |
The plugin runs inside Obsidian and talks to the control plane; the control plane issues per-document tokens and proxies through relay-server.
git clone https://github.com/micahchoo/obsidian-relay-fork.git
cd obsidian-relay-forkNo submodule dance — both Relay/ and relay-control-plane/ are regular directories.
Every tagged release (v*) publishes a production build to GitHub Releases. Download main.js, manifest.json, and styles.css (or the relay-<tag>.zip bundle), then copy them into:
<your-vault>/.obsidian/plugins/system3-relay/
Enable Relay in Settings → Community plugins.
cd Relay
npm install
npm run dev # watch mode, rebuilds on save
# or
npm run release # one-shot production build -> main.js, styles.cssSymlink the built output into your vault's .obsidian/plugins/system3-relay/ directory for live testing.
Scripts exposed by Relay/package.json:
| Script | What it does |
|---|---|
npm run dev |
esbuild watch (dev build, source maps) |
npm run build |
Typecheck + esbuild develop target |
npm run release |
Typecheck + esbuild production target (minified) |
npm run beta |
esbuild debug target |
npm run staging |
esbuild staging target |
npm test |
Jest |
npm run lint |
ESLint |
Pushing a tag matching v* triggers .github/workflows/release.yml, which:
- Checks out the repo.
- Runs
npm ci && npm run releaseinsideRelay/. - Attaches
main.js,manifest.json,styles.css, andrelay-<tag>.zipto a GitHub Release.
Obsidian's plugin submission requirements (LICENSE, README.md, manifest.json with x.y.z version, and the three artifact files attached to the release) are all satisfied by this setup.
# bump Relay/manifest.json and Relay/manifest-beta.json to the new version first
git commit -am "Bump plugin version to vX.Y.Z"
git tag -a vX.Y.Z -m "vX.Y.Z — <summary>"
git push origin main
git push origin vX.Y.ZTwo services, both defined in relay-control-plane/docker-compose.yml:
| Service | Port | Purpose |
|---|---|---|
relay-server-sh |
8082 | Yjs sync server (CRDT updates). Image: docker.system3.md/relay-server. |
control-plane |
8090 | PocketBase — user accounts, relays, folders, token issuance. Built locally. |
cd relay-control-plane
cp .env.example .env # if an example exists; otherwise create .env with required vars
docker compose up -d
docker compose logs -fPocketBase admin UI: http://localhost:8090/_/
Relay-server health: http://localhost:8082/health
relay.toml—relay-serverconfiguration. The[server].urlfield must match the address Obsidian clients use to reach the sync server (e.g.http://<host-ip>:8082).pb_hooks/— PocketBase JS hooks. Mounted read-only; edit in place and restart the container.pb_migrations/— PocketBase schema migrations, applied at startup.data/— PocketBase database (persistent volume).relay-data/— relay-server document store (persistent volume).
In Obsidian → Settings → Relay → set the control-plane URL to http://<host-ip>:8090 and sign in. The plugin will fetch a self-hosted auth token and route sync traffic to the matching relay-server.
These are non-obvious failure modes that cost a lot to diagnose. Keep them in mind when bringing up a fresh deployment.
- Client sends the relay's
guid, not PocketBase's shortid. The token hook must usefindFirstRecordByFilter("relays", "guid = {:guid}", ...)— notfindRecordById. Getting this wrong returns 404 for every valid relay. provider.urlis host-facing;RELAY_SERVER_URLenv is container-facing. The pb_hook prefers env when set, falls back toproviders.urlonly if env is empty. Otherwise the hook tries to reachhttp://localhost:8082from inside the PB container and getsECONNREFUSED.- COSE
kidinRELAY_SERVER_AUTHmust be a byte-string (CBOR major type 2,0x4b-prefixed), not a text-string (0x6b). y-sweet's CWT validator rejects text-string kid with"Invalid token: The key ID did not match". The mint script in this README gets this right. docker restartdoes not reload.env. After editing env vars, usedocker compose up -d --force-recreate control-plane.allowed_token_typesinrelay.tomlmust include"server"explicitly. Upstream default is["document", "file"]and silently rejects server-token auth at/doc/:id/auth.- CWT audience validation is enabled when relay.toml has
[server].urlset; theaudclaim inRELAY_SERVER_AUTHmust match that URL byte-for-byte. - Legacy HMAC
[[auth]]block is required./doc/:id/authinternally callsgen_doc_token→sign(), which requires anAuthKeyMaterial::Legacykey. Without it you get 500CannotSignWithPublicKey.
Source/— local working copies / vendor drops. Gitignored.*HANDOFF*.md— session-handoff docs. Gitignored.docs/— plans, specs, and architecture notes (tracked)..mulch/,.seeds/— project expertise and issue tracking (tracked).
To pull upstream plugin changes into Relay/ (one-time remote setup, then one command):
# one-time
git remote add relay-upstream https://github.com/No-Instructions/Relay.git
# every time
git fetch relay-upstream
git subtree pull --prefix=Relay relay-upstream main --squashRelay/— seeRelay/LICENSE(upstream license).relay-control-plane/— provided as-is, no warranty.