Nodus is a hybrid, offline-first peer-to-peer storage system. Files live primarily on a storage node you control, sync directly between your own devices over the local network, and use the Internet only as an enhancement — never a hard dependency.
🚀 Status: active implementation. Core storage, sync, and transit components are built and tested; the web client has a working UI ported from the design prototype. See Current status below.
Most cloud storage treats the Internet — and a central server — as the source of truth. Nodus flips that:
- Local-network-first. Your devices talk directly to your Storage Node over Wi-Fi or wired LAN, as long as they're on the same network. No Internet required for day-to-day sync.
- Internet as enhancement, not dependency. A Relay server provides signaling, cross-network sync, and temporary buffering when devices aren't on the same network — but it never holds plaintext file contents or keys.
- Recoverable by design. If the Relay's database is lost, it can be fully rebuilt from snapshots held by your Storage Node(s).
- Convergent, not "last write wins." Independently made offline changes reconcile without an arbitrary winner overwriting the other.
Four components, one protocol:
| Component | Directory | Stack | Status |
|---|---|---|---|
| Web client | apps/web |
Next.js + packages/ui (Tailwind v4) |
UI ported; session auth wired; live sync TBD |
| Mobile client | apps/mobile |
React Native / Expo | Phase 15–17 complete — native build, session auth, pairing, mDNS, A→B→C→D transfers, upload/download, folders, devices/nodes, recovery, background drain |
| Storage Node | services/storage-node |
Rust, SQLite | Sync + object store implemented |
| Relay | services/relay |
Go, PostgreSQL, Redis | Control plane + buffer implemented |
INTERNET
|
v
+----------------------+
| Go Relay / API |
| PostgreSQL · Redis |
| Temporary Buffer |
+----------+-----------+
|
Sync Protocol
|
Wi-Fi / LAN
|
+----------v-----------+
| Rust Storage Node |
| SQLite · Object Store|
+----------+-----------+
|
Local WebRTC
|
+----------v-----------+
| Web / Mobile Client|
+----------------------+
Files are split into 8 MB shards, encrypted client-side with AES-256-GCM (unique nonce per shard), and integrity-checked with BLAKE3. The Relay only ever sees encrypted bytes and encrypted key envelopes — never plaintext file keys or shard contents.
Nodus picks the best available path automatically, falling back as needed:
- Path A — Direct Local P2P. Local signaling + WebRTC, no Relay involved.
- Path B — Relay-Mediated Signaling. Relay carries SDP/ICE only; file data still flows peer-to-peer.
- Path C — Buffer-and-Relay. Encrypted shard is buffered on the Relay and delivered asynchronously when the Storage Node comes back online.
- Path D — Local Queue. If both the Storage Node and Relay are unreachable, changes queue locally until one becomes available.
Nodus is a single monorepo — one repo, multiple languages:
nodus/
├─ apps/
│ ├─ web/ # Next.js web client (ported design UI)
│ └─ mobile/ # React Native / Expo mobile client (scaffold)
├─ packages/
│ ├─ core/ # Domain logic: sharding, crypto abstractions
│ ├─ protocol/ # Canonical protocol schemas/types (zod, JSON Schema)
│ ├─ relay-client/ # WebSocket client (web + mobile)
│ ├─ webrtc-transport/ # WebRTC abstraction
│ ├─ transfer-manager/ # Cross-path transfer orchestration + repair
│ ├─ ui/ # Shared design system (Tailwind v4, ported prototype)
│ └─ config/ # Shared TypeScript tooling (eslint/ts configs)
├─ services/
│ ├─ relay/ # Go Relay (control plane + buffer)
│ └─ storage-node/ # Rust Storage Node (data plane)
├─ docs/
│ ├─ architecture/
│ ├─ protocol/
│ ├─ security/
│ ├─ decisions/ # ADRs (key hierarchy, recovery, GC, ...)
│ └─ design-port-plan.md # Prototype → implementation mapping
├─ pnpm-workspace.yaml
└─ turbo.json
The TypeScript apps and packages are managed by Turborepo + pnpm. The Rust
Storage Node and Go Relay live in the same repository under services/ but
sit outside the pnpm/Turborepo workspace, with their own native tooling.
Implemented and tested (see CHANGELOG.md for detail):
- Design foundations — five ADRs locked in
docs/decisions/: key hierarchy, recovery mechanism, conflict-resolution UX, mobile local discovery, and GC policy. - Protocol (
packages/protocol) — canonical wire schemas with runtime (zod) validation and generated JSON Schema, versioned and documented. - Storage Node (
services/storage-node, Rust) — SQLite schema, run-time configurable data directory, content-addressed object store with atomic writes, crash recovery, reconciliation, and automatic GC per ADR-0005. - Relay (
services/relay, Go) — REST API (Argon2id password auth, opaque server-side sessions via HttpOnly cookie), WebSocket hub with presence, and a transient encrypted shard buffer with TTL sweep. - Transfer sync (
services/relay↔services/storage-node) — incremental sync, full snapshot / relay rebuild (Path C), buffer-and-relay transfers, and the repair (data-return) path. - Transfer Manager (
packages/transfer-manager) — path selection, fan-out, and repair orchestration for the four transfer paths. - Web client (
apps/web) — the Figma prototype (nodus-design/) ported to a Tailwind v4 shared design system (packages/ui); six dashboard routes plus a real auth wizard (Phase 7a §3), accessible (axe-clean) in light and dark themes./authsigns in / creates accounts throughapp/api/auth/*route handlers that proxy the Relay and set the HttpOnly session cookie;/and the dashboard group require a valid session./pair(Phase 7a §4) is server-guarded and pairs Storage Nodes through session-authenticated proxies (/api/nodes,/api/pairing/*) — no client-side Bearer/JWT.
Full build order and the phase-by-phase checklist:
nodus_implementation_plan.md— the complete architecture and design plan (protocol, schemas, key envelopes, state machines, failure guarantees, etc.)Todo.md— the phase-by-phase implementation checklist derived from the plan
Internet is an enhancement, not a dependency.
Internet available: Client <-> Relay <-> Storage Node
Internet unavailable: Client <-> Storage Node
Storage Node offline: Client -> Relay Buffer -> Storage Node
Relay corrupted: Storage Node -> Snapshot/Rebuild -> New Relay DB
| Tool | Version | Used for |
|---|---|---|
| Node.js + pnpm | Node ≥ 20, pnpm 11 | Web client, TypeScript packages |
| Rust | stable toolchain + cargo |
Storage Node |
| Go | ≥ 1.22 | Relay |
| Docker | Compose v2 | Postgres + Redis for the Relay |
make install-hooks # gofmt/rustfmt/clippy pre-push hooks
pnpm install # TypeScript workspace (apps + packages)cd apps/web
pnpm dev # http://localhost:3000The dashboard, overview, and settings routes render with mock data; /auth
is the real session-cookie wizard. pnpm build && pnpm start for a
production-style build. To run the web client against the Relay locally, start
the Relay with SESSION_COOKIE_SECURE=false (plain HTTP) and point the web
app at it via RELAY_URL (defaults to http://localhost:8080).
cd services/relay
docker compose up -d # Postgres 17 + Redis 7 (health-checked)
go run .The relay listens on :8080 by default. Configuration via env vars:
| Env | Default |
|---|---|
PORT |
8080 |
DATABASE_URL |
postgres://nodus:nodus_password@localhost:5432/nodus_relay?sslmode=disable |
REDIS_URL |
redis://localhost:6379/0 |
Migrations run automatically on startup.
cd services/storage-node
cargo run # interactive CLI
cargo run -- --data-dir ~/NodusBackup # or export NODUS_DATA_DIR=~/NodusBackupOn a terminal, a bare nodus opens the interactive CLI: on first run it asks
for the Relay URL and pairing code (mint one in the web app under
Devices → “+ Add Storage Node”), then presents a menu — run the node, view the
storage summary, list files/folders, show status, or pair/re-pair. The file and
folder listings read this node's local SQLite and show ids and sizes only, since
names are end-to-end encrypted and the node never holds a file key.
Non-interactive invocations (systemd, pipes) skip the menu and boot the daemon
directly; nodus node start always boots the daemon and nodus node pair --relay <url> --code <code> pairs without prompting. Without --data-dir the
first run prompts for the data directory and defaults to ~/NodusBackup.
The self-hosted unit packages the Next.js web app, the Go Relay, PostgreSQL, Redis, and Caddy (TLS + routing) behind one public origin:
cp deploy/.env.example deploy/.env
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up --build -d
curl -fsS http://localhost/healthSet SITE_ADDRESS/PUBLIC_RELAY_URL/ALLOWED_ORIGINS to the real origin for
production. See deploy/README.md for routing, TLS, and
operations details.
Both resets are destructive and require typing the exact phrase
purge everything, then force everything to be paired again.
Storage Node — in the interactive CLI choose Factory reset (delete everything), or from the running shell:
nodus> factory-reset
This deletes the node's catalogue, objects, identity, and config. The node gets
a new node_id, so it must be paired with the Relay again.
Relay — stop the running Relay first, then:
cd services/relay
go run . --factory-reset # prompts: type "purge everything"
# or non-interactively:
echo 'purge everything' | go run . --factory-resetThis drops and recreates the database schema (rebuilt from migrations on the next start), flushes the Relay's Redis database, and clears the shard buffer. Every account, device, and Storage Node must register and pair again.
pnpm test # all JS/TS, Relay (Go), and Storage Node (Rust) testsThis requires Node/pnpm, Go, and Cargo. Relay tests that require external
services are skipped by default. To include them, start the Relay Compose
dependencies and provide TEST_DATABASE_URL (and TEST_REDIS_URL where
needed):
docker compose -f services/relay/docker-compose.yml up -d
TEST_DATABASE_URL='postgres://nodus:nodus_password@localhost:5432/nodus_relay?sslmode=disable' \
TEST_REDIS_URL='redis://localhost:6379/0' \
pnpm testMIT