A self-hosted, zero-knowledge password and secrets manager. The browser does all the encryption; the server stores ciphertext, ids, item types and timestamps, and never sees a master password, a key or an item's contents.
v2 adds typed items (logins, API keys, SSH hosts, databases, servers, VPNs, cloud credentials, webhooks, cards, secure notes, identities, Wi-Fi, licences), collections, version history, trash, and a one-time in-browser upgrade for v1 vaults. The full build contract is docs/V2-SPEC.md.
browser ──HTTP──> client container (nginx:1.27, port 80)
├── / built React SPA (Vite)
└── /api/ ──> backend-api container (Node 20 + Express, port 5000)
└── PostgreSQL (external)
- client/ — React + Vite SPA, served by nginx with a strict CSP. nginx proxies
/api/to the API, so the app is same-origin. - server/ — Express API.
app.jsbuilds the app (createApp(pool)),index.jsboots it,db.jsholds the additive migrations and the cold-boot retry loop (30 tries × 5 s, then exit 1 so Docker restarts it). - Sessions — an httpOnly,
SameSite=Strictcookie holding a 12-hour HS256 JWT. Every non-GET request must sendX-WV-Client: 1(CSRF guard). Login attempts are rate limited and recorded inlogin_logs. - Registration is open only while the server has no users, or when
ALLOW_REGISTRATION=true.
- Master password → Argon2id (64 MiB, t=3, p=1, 16-byte salt) → master key.
- HKDF-SHA256 splits it into an encryption key and an auth key. Only
base64(authKey)is sent to the server, which stores a bcrypt hash of it. - A random vault key is sealed with the encryption key; each collection has a random key sealed with the vault key; each item is sealed with its collection key.
- Sealing is AES-256-GCM with a fresh 12-byte nonce and associated data that binds the ciphertext to its item id and type.
- It works over plain HTTP on a LAN (no
crypto.subtleneeded):@noble/ciphers,@noble/hashes,hash-wasm,crypto.getRandomValues.
v1 → v2 upgrade. v1 encrypted each entry with CryptoJS AES using the raw master
password. On first v2 sign-in, the browser decrypts the v1 entries, re-encrypts them as v2
items, verifies every one, makes the person download a backup, and only then calls
POST /api/auth/upgrade. The server checks the upgrade covers every v1 entry exactly once.
The v1 rows and the v1 password hash are never modified, so rolling back to v1 keeps working.
Requires Docker and Node 20+.
# Postgres 16 (host port 55432) + API + client (http://localhost:8088)
docker compose -f docker-compose.dev.yml -p wvdev up -d --build
# Optional: a v1 user to try the upgrade with (alice / "correct horse battery staple")
cd server && npm install
DATABASE_URL=postgres://wreckvault:wreckvault-dev-only@localhost:55432/wreckvault npm run seed:legacyThe dev stack's credentials are throwaway and only for that local stack. To run the API
outside Docker, copy server/.env.example to server/.env and point DATABASE_URL at a
database; the server also reads plain environment variables, and .env is optional.
| Variable | Meaning |
|---|---|
DATABASE_URL |
Postgres connection string (required) |
PORT |
API port, default 5000 |
ALLOW_REGISTRATION |
true keeps account creation open after the first user |
Server tests use node:test and supertest against a real Postgres. Each test file creates
its own throwaway database next to the one in DATABASE_URL and drops it afterwards, so
the role needs CREATEDB:
cd server
DATABASE_URL=postgres://wreckvault:wreckvault-dev-only@localhost:55432/wreckvault npm testClient lint, unit tests and build: npm run lint, npm test, npm run build in client/.
Production uses docker-compose.yml: the API image bakes in the source and reads
DATABASE_URL from server/.env on the host (env_file); the client image is a multi-stage
build served by nginx. Ports: client 3011, API 3012. The API joins the external
postgresql_default network to reach Postgres by container name.
GitHub Actions (.github/workflows/deploy.yml) runs client lint, tests and build plus the
server tests against a postgres:16 service on every push to main, then a self-hosted
runner deploys with docker compose -p wreckvault up -d --build.
client/ React SPA, Dockerfile, nginx.conf
server/
app.js createApp(pool): middleware + routes
index.js boot: migrate (with retry), then listen
db.js pool, additive migrations, retry loop
routes/ auth.js (auth, upgrade, legacy), vault.js (collections, items)
lib/ session, validation, row mapping, helpers
scripts/ seed-legacy.js (dev only)
test/ node:test suites
docs/V2-SPEC.md the v2 contract
MIT