Geode syncs Obsidian vaults between your devices through a server you run yourself. Each device encrypts your notes before sending them, so the server stores only encrypted data and never sees file names or note contents. Edits usually reach your other devices within a few seconds.
flowchart TD
base["Base version<br/>the note both devices started from"]
base --> laptop["Laptop edit<br/>changes one line"]
base --> phone["Phone edit<br/>adds a new line"]
laptop --> merged["Merged note<br/>both edits, no conflict"]
phone --> merged
laptop --> same["Both changed the same line"]
phone --> same
same --> file["Conflict file<br/>both versions kept"]
file --> review["Review<br/>Keep original, Keep conflict, Keep both"]

Taken on plugin v0.41.7.
Geode backup folder inside the vault, instead of uploading them over your notes.
Taken on plugin v0.41.7.

Taken on plugin v0.41.7.

Taken on plugin v0.41.7.
.env, rename or leave the vault, and
change the server address.
Taken on plugin v0.41.7.
Every other screen, and the command that reshoots them all: docs/screenshots/README.md.
.env. You need it to add devices.Use one sync tool per vault. Don't sync a Geode vault with iCloud or Google Drive at the same time.
Geode runs from a published image, so there is nothing to clone and nothing to build. It needs a machine with Docker and Compose v2, and a directory of its own.
compose.yaml pulls ghcr.io/hrt-htk/geode:latest: the release that has been promoted to the
community directory. To follow every release instead, including the betas BRAT installs, put
ghcr.io/hrt-htk/geode:beta in its place.
At your DNS provider, add an A record that points your domain at the VPS IP address, and delete a leftover AAAA record for the same name. Caddy, which Geode starts for HTTPS, gets its certificate for the address DNS hands out.
On the server, make the directory and fetch the two files:
mkdir geode && cd geode
curl -fsSLO https://raw.githubusercontent.com/Hrt-Htk/geode/main/compose.yaml
curl -fsSL https://raw.githubusercontent.com/Hrt-Htk/geode/main/.env.docker.example -o .env
Open .env and fill in two things: the GEODE_ADMIN_TOKEN value, and the VPS block below it
(uncomment COMPOSE_PROFILES=https and GEODE_TRUST_PROXY, set GEODE_DOMAIN). Generate the
token with:
openssl rand -base64 36 | tr -d '\n/+=' | cut -c1-40
Create the data folder for the container's user, then start Geode. It is 0700 because the database holds device tokens:
mkdir -p data && sudo chown 1993:1993 data && sudo chmod 700 data
docker compose up -d
Open https://geode.example.com/version in a browser. It shows the server version. If it fails
with a TLS error, Caddy is still getting the certificate: open it again after 15 seconds.
Check that the server answers as an admin, not just that it is up. This is the request the plugin
makes when it lists vaults, so it also proves the admin token and the database work. [] is the
right answer on a fresh server: the database holds no vaults yet. After the setup wizard it lists
your vault instead.
curl -s -H "Authorization: Bearer <the token from .env>" \
https://geode.example.com/vaults
Use wss://geode.example.com as the server address. geode.example.com works too: an address with
no scheme gets https, unless it is a private or local one.
Geode and Caddy both come back after a reboot.
The same two files and steps, with the home block in .env instead: uncomment
COMPOSE_PROFILES=forward and GEODE_PUBLISH=0.0.0.0:8080, and leave the domain lines alone.
On your home network, use http://192.168.1.20:8080 as the server address, with your server's LAN
IP in place of 192.168.1.20. Plain http is fine inside your own network. Typed without a scheme, a
private or local address (localhost, 10.x, 192.168.x, 172.16 to 31.x, 100.64 to 127.x) also defaults
to http; any other address defaults to https. Open http://192.168.1.20:8080/version in a browser
to check the server.
To reach the server from outside your home, use Tailscale. Install it on
the server and on every device, then use http://TAILSCALE_IP:8080 as the server address.
Tailscale encrypts the traffic between your devices.
Phones connecting to a plain http address have not been tested yet. If your phone can't connect,
use the domain setup with HTTPS.
Geode used to install as a systemd service, and that path is gone. To move the same server to Docker:
Do steps 2 and 3 above, with your existing admin token as GEODE_ADMIN_TOKEN: it is in
/etc/geode/geode.env. If the old script set up Caddy for your domain, keep it and use the own
proxy block in .env (COMPOSE_PROFILES=forward, GEODE_PUBLISH=127.0.0.1:8080,
GEODE_TRUST_PROXY=1): that Caddy already forwards to 127.0.0.1:8080. Without a domain, use
the home block.
Instead of step 4, stop the old server, copy its data and start Geode:
sudo systemctl disable --now geode
sudo cp -a /opt/geode/data ./data
sudo chown -R 1993:1993 data
sudo chmod 700 data
docker compose up -d
Your devices reconnect by themselves: their tokens are in the copied data. The old install is left
where it is; delete /opt/geode, /etc/geode and /etc/systemd/system/geode.service once the new
one works.
Obsidian then updates Geode with your other community plugins.
BRAT installs plugins straight from a GitHub repository, so it gets every release here, including the betas, before they are promoted.
https://github.com/Hrt-Htk/geode and click Add.main.js, manifest.json and styles.css from the
latest release.<vault>/.obsidian/plugins/geode/.Phone file apps hide the .obsidian folder, so on a phone install from the community directory
instead.
The passphrase encrypts your notes and needs at least 12 characters. It is never sent to the server, so nobody can reset or recover it. If you lose it, the data on the server can't be decrypted. Notes already on your devices stay readable.
Install the plugin fresh on each device. Don't copy the vault folder from another device:
.obsidian/plugins/geode/ holds the device identity, and two devices sharing one identity cause
replay loops and false conflicts.
The admin token is long. On a phone, paste it instead of typing it, for example from a password manager.
Rejoining over an old copy. A device that still holds this vault and its Geode data (a revoked device, a device that was offline for months) joins the same way, and Geode makes it match the server instead of pushing the old folder back:
Geode backup folder inside the vault instead of uploaded. That folder never
syncs, on any device.The setup wizard warns you before the join runs and reports how many files it kept. This needs the device's Geode data: a recovery that wiped it (a fresh plugin folder) leaves nothing to compare, and that join uploads the folder as new notes, so remove the old files first.
If you already copied a vault folder and see conflicts or replay loops, run the setup wizard on that device and join again. It gets a new identity, and your passphrase still works.
The plugin and server don't need to run the same version. When versions differ, sync keeps working and Geode's settings show Plugin update available or Server update available. Sync stops only when the wire protocol changed or the server requires a newer plugin. The settings then show Update required or Server update required. Update the side the message names, then reload Obsidian.
Server: Geode's settings have a Server update row. With the update job installed (optional,
docs/architecture/server.md §11) its Update button pulls the new
image, waits for Geode to report healthy, and puts the old image back if it does not come up. Without
the job, the row's copy button gives the command to run in the compose directory:
docker compose pull geode && docker compose up -d
Either way your data and admin token stay, and Caddy keeps serving. A new image only exists when a
release is cut, and /version reports the one that is running. The tag in compose.yaml decides
which releases you get: latest moves only when a release is promoted to the community directory,
beta moves on every release.
Plugin: from the community directory, click Check for updates under Settings → Community plugins; there you get the versions that were promoted. BRAT installs every release, including pre-releases, so a new build reaches you as soon as it is cut: click Update next to Geode in BRAT's settings, or replace the three files with the ones from the latest release.
Everything the server keeps is in data/ next to compose.yaml: a SQLite database and the encrypted
blobs. The data is encrypted, so you can store backups anywhere, such as another computer, a USB drive
or object storage. The database itself is not: it holds vault names and device tokens, so keep a
backup about as private as the server.
Back up with a consistent copy of the database plus the blobs, from the compose directory:
sudo apt install -y sqlite3 rsync # once
sudo mkdir -p /path/to/backup
sudo sqlite3 data/geode.db ".backup /path/to/backup/geode.db"
sudo rsync -a --exclude 'geode.db*' --exclude 'before-migration.sqlite*' \
data/ /path/to/backup/
The two excludes drop the live database, which sqlite3 .backup already copied consistently, and
before-migration.sqlite: the copy the server writes before a pending migration (see
docs/architecture/server.md §3). That one is a stopgap for going back
straight after an update, and stale as soon as the server runs again, so it does not belong in a
backup.
To restore, stop Geode, copy the backup over data/ and start it again:
docker compose down
sudo rsync -a --delete /path/to/backup/ data/
sudo chown -R 1993:1993 data
docker compose up -d
--delete is what makes the restore restore. A running server keeps its newest changes in
geode.db-wal beside the database, and SQLite replays that file over whatever database it finds, so a
restore that leaves the old -wal in place comes back holding the newer state even though the
restored geode.db is the backup's. --delete removes geode.db-wal and geode.db-shm, and with
them the blobs of files and vaults created after the backup: the server then serves exactly the
backup's state.
Then check what the server serves. [] is correct when the backup held no vaults; otherwise it lists
the vaults the backup contained:
curl -s -H "Authorization: Bearer <the token from .env>" \
https://geode.example.com/vaults
Try a restore at least once so you know the backup works. Full procedure:
docs/testing/strategy.md §6.
The admin token lets you add devices and manage the server. Keep it private, like a password.
GEODE_ADMIN_TOKEN in the .env next to compose.yaml. That file holds the whole
token, so keep it private (chmod 600 .env), and copy the token from there when the setup wizard
asks for it..env, then docker compose up -d. The old token stops working.
Existing devices keep syncing; use the new token to join devices and for admin actions.A device that has joined the vault can read and change your notes until you revoke it.
The device can no longer sync from that moment. If you get it back, join it again like a new device.
Server: from the compose directory:
docker compose down -v
cd .. && rm -rf geode
-v removes Caddy's certificate volume as well. Geode's own data is a bind mount, not a volume: it
goes with the directory. The image stays on disk until you remove it:
docker image rm ghcr.io/hrt-htk/geode:latest.
Plugin: disable Geode under Settings → Community plugins, then delete the
<vault>/.obsidian/plugins/geode/ folder.
| Problem | What to do |
|---|---|
| Can't reach the server | docker compose ps shows what runs and docker compose logs geode what the server says. Then query it: curl -v https://geode.example.com/version (home server: curl -v http://192.168.1.20:8080/version). It answers with JSON that includes the server version. If it does, check that an admin request works: curl -s -H "Authorization: Bearer <the token from .env>" https://geode.example.com/vaults answers [] or your vaults. |
| "That admin token isn't valid" | Copy it again from GEODE_ADMIN_TOKEN in .env and paste it exactly. |
| Wrong passphrase when joining | Geode rejects it right away. Passphrases are case sensitive. |
| Notes don't appear after joining | Give the first sync a moment. If nothing arrives, run Resync from scratch from the command palette. |
| Replay loops or repeated conflicts after copying a vault | Two devices share one identity. See Add another device. |
| "Update required" or "Server update required" | See Update. |
Geode has one remote service: the Geode server you run yourself. The setup wizard asks for its address, and every request the plugin makes goes to that address and nowhere else.
/vaults/<id>/sync and reports each change
there. Encrypted chunks go over HTTP to /blobs on the same host, and the reads behind history and
catch-up come back the same way. A server with a domain gets wss:// and https://; a server on
your home network gets ws:// and http://.The server receives:
It never receives note contents or file names in the clear.
There is no telemetry, no analytics, no crash reporting and no third-party service: no CDN, no font or icon host, no update endpoint of its own. Those are the only network calls the plugin makes.
plugins/geode/). Geode never syncs that folder, but it must
stay on the device: another sync tool that copies your config directory would carry the token to its
server. Keep one sync tool per vault.Found a security problem? Report it privately, not in a public issue: SECURITY.md.
507, a commit
or restore storage_full). Deleting frees space only once compaction and blob garbage collection
have run, so the quick way back is a bigger cap: set GEODE_VAULT_MAX_BYTES (bytes) in .env and
run docker compose up -d. The server logs the cap it actually uses at startup. A commit is also
bounded per file: 10 000
chunks, 64 KB of encrypted metadata and 8 KB of encrypted path. At the chunker's 256 KB average that
caps a single file at about 2.5 GB (640 MB if every chunk were the 64 KB minimum, 10 GB if every
chunk hit the 1 MB maximum), and a larger file cannot be committed.node-diff3 for
merging.main.js is about 3.5 MB because it is the whole plugin in one file: the sync engine and every
library it runs on, bundled by deno task build:plugin (scripts/plugin-build.ts, esbuild). More
than half of the size is libsodium, which arrives as WebAssembly inlined as base64. An Electron
window never finishes fetching a separate .wasm file, so the build extracts the WebAssembly module
from libsodium's own JavaScript, inlines it, and patches libsodium to start from those bytes
synchronously. Nothing is loaded from the network when the plugin runs.
Architecture and build order: docs/architecture/overview.md.
Index of all documents: docs/README.md.
Contribution rules: CONTRIBUTING.md.
Apache-2.0. See LICENSE.