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, SQLite, Redis | Control plane + buffer implemented |
INTERNET
|
v
+----------------------+
| Go Relay / API |
| SQLite · 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)
│ └─ docs/ # Documentation site (Next.js + Fumadocs)
├─ 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.
The official documentation site lives in apps/docs (Next.js + Fumadocs) and
covers everything end to end — user guides, self-hosting, and the full
CLI/API/protocol reference:
pnpm --filter docs dev # http://localhost:3001Content is authored as MDX under apps/docs/content/docs/. The engineering
docs in docs/ (ADRs, protocol, architecture) remain canonical and are mirrored
into the site's Reference and ADR sections.
For end-user install steps — Android APK, Storage Node binary, and the
self-hosted server — see docs/install.md.
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 | 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 # Redis 7 (health-checked); the database is a local SQLite file
go run .The relay listens on :8080 by default. Configuration via env vars:
| Env | Default |
|---|---|
PORT |
8080 |
DB_PATH |
${TMPDIR}/nodus-relay/relay.db |
REDIS_URL |
redis://localhost:6379/0 |
Migrations run automatically on startup.
Install the latest prebuilt binary (Linux x86_64):
curl -fsSL https://raw.githubusercontent.com/TalibMushtaq/nodus/main/scripts/install-node.sh | sh
# installs nodus to ~/.local/bin; pin a version with `sh -s -- --version 0.1.0`Then run nodus (interactive) or nodus node start (service). To build from
source instead:
cd services/storage-node
cargo build --release
./target/release/nodus # interactive CLI
./target/release/nodus --data-dir ~/NodusBackupcargo run works too for local development.
On 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, the docs site, SQLite, Redis, and Caddy behind three public origins (web, relay, docs):
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 WEB_HOST/RELAY_HOST/DOCS_HOST, PUBLIC_RELAY_URL,
NEXT_PUBLIC_RELAY_URL, and ALLOWED_ORIGINS 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 deletes the SQLite database file and its -wal/-shm sidecars (rebuilt
from the baseline migration 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.
The prompt prints the exact targets it is about to destroy — the database file,
Redis host and index, and the buffer path — so a DB_PATH or REDIS_URL
pointed at the wrong place is visible before anything is deleted. Two guards
also apply:
- The reset refuses to run while the Relay is still running, because it holds an
exclusive lock on the database file. Stop it first, or pass
--factory-reset-force. BUFFER_DIRis validated before anything is deleted. A relative path, the filesystem root, a system directory (/etc,/usr,/tmp, …), your home directory, the working directory, or a path that resolves through a symlink into a system tree is refused outright —os.RemoveAllis recursive, so those are the values that would delete far more than the shard buffer. Use a dedicated subdirectory such as/var/lib/nodus/buffer.
pnpm test # all JS/TS, Relay (Go), and Storage Node (Rust) testsThis requires Node/pnpm, Go, and Cargo. Relay tests that need Redis are skipped
by default. To include them, start the Redis dependency and provide
TEST_REDIS_URL. The Relay's database is a per-test SQLite file, so no database
service is needed:
docker compose -f services/relay/docker-compose.yml up -d
TEST_REDIS_URL='redis://localhost:6379/0' pnpm testMIT