Client-side encryption
Argon2id, HKDF and XChaCha20-Poly1305 protect data before it reaches the API.
A self-hosted password manager with browser-side end-to-end encryption, YubiKey/FIDO2 and no server access to plaintext secrets.
N3X Vault is an independent password manager with its own protocol and browser-side end-to-end encryption. The server never receives the master password or an unwrapped account key.
Release status: the security alpha is suitable for protocol review, development and controlled testing. Do not store production secrets yet.
Application interface
A real product interface — open any preview to inspect it at full size.
1 / 3
Logins, secure notes and TOTP secrets are encrypted in the browser before reaching your own server.
Managed hosting by N3X
N3X can deploy and maintain this application, from initial setup and SSL to updates, monitoring and backups.
Argon2id, HKDF and XChaCha20-Poly1305 protect data before it reaches the API.
WebAuthn PRF unlock, hardware-key inventory and a two-device readiness policy.
Encrypted export and resumable restore into an empty installation without a hidden universal key.
The Rust API and PostgreSQL store ciphertext, protocol parameters and minimal operational metadata.
Fifteen-minute browser sessions remain memory-only and are revocable server-side.
A private database network, dropped capabilities, read-only filesystems and volume-backed runtime secrets.
DOCKER CONFIGURATOR
Set the final hostname before enrolling a YubiKey. Changing the RP ID later invalidates existing hardware credentials.
Use the final reverse-proxy domain, for example vault.company.com.
name: n3xvault
x-api-security: &api-security
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:size=32m,mode=1777
x-web-security: &web-security
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:size=32m,mode=1777
- /app/.next/cache:size=64m,mode=0700,uid=1001,gid=1001
services:
bootstrap:
image: alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
restart: "no"
command:
- /bin/sh
- -ec
- |
umask 077
if [ ! -s /run/n3xvault/database_password ]; then
od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > /run/n3xvault/database_password
fi
if [ ! -s /run/n3xvault/bootstrap_token ]; then
od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > /run/n3xvault/bootstrap_token
fi
chmod 0444 /run/n3xvault/database_password /run/n3xvault/bootstrap_token
volumes:
- runtime_secrets:/run/n3xvault
networks:
- backend
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
postgres:
image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
restart: unless-stopped
depends_on:
bootstrap:
condition: service_completed_successfully
environment:
POSTGRES_DB: n3xvault
POSTGRES_USER: n3xvault
POSTGRES_PASSWORD_FILE: /run/n3xvault/database_password
volumes:
- postgres_data:/var/lib/postgresql/data
- runtime_secrets:/run/n3xvault:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n3xvault -d n3xvault"]
interval: 5s
timeout: 5s
retries: 12
networks:
- backend
api:
image: ghcr.io/nexitpl/n3xvault-api:alpha
restart: unless-stopped
depends_on:
bootstrap:
condition: service_completed_successfully
postgres:
condition: service_healthy
environment:
N3XVAULT_BIND: 0.0.0.0:8080
N3XVAULT_DATABASE_HOST: postgres
N3XVAULT_DATABASE_NAME: n3xvault
N3XVAULT_DATABASE_USER: n3xvault
N3XVAULT_DATABASE_PASSWORD_FILE: /run/n3xvault/database_password
N3XVAULT_BOOTSTRAP_TOKEN_FILE: /run/n3xvault/bootstrap_token
N3XVAULT_ENVIRONMENT: ${N3XVAULT_ENVIRONMENT:-development}
N3XVAULT_PUBLIC_ORIGIN: https://vault.example.com
N3XVAULT_WEBAUTHN_RP_ID: vault.example.com
N3XVAULT_SESSION_TTL_SECONDS: ${N3XVAULT_SESSION_TTL_SECONDS:-900}
N3XVAULT_WEBAUTHN_FLOW_TTL_SECONDS: ${N3XVAULT_WEBAUTHN_FLOW_TTL_SECONDS:-300}
N3XVAULT_DEVICE_PAIRING_TTL_SECONDS: ${N3XVAULT_DEVICE_PAIRING_TTL_SECONDS:-300}
RUST_LOG: ${N3XVAULT_LOG_LEVEL:-n3xvault_api=info,tower_http=info}
volumes:
- runtime_secrets:/run/n3xvault:ro
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/health/ready"]
interval: 15s
timeout: 3s
retries: 5
start_period: 15s
networks:
- backend
<<: *api-security
web:
image: ghcr.io/nexitpl/n3xvault-web:alpha
restart: unless-stopped
depends_on:
api:
condition: service_healthy
environment:
N3XVAULT_API_INTERNAL_URL: http://api:8080
ports:
- "127.0.0.1:8793:3000"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/health"]
interval: 20s
timeout: 5s
retries: 5
start_period: 20s
networks:
- frontend
- backend
<<: *web-security
networks:
frontend:
backend:
internal: true
volumes:
postgres_data:
runtime_secrets:
mkdir -p n3xvault
cd n3xvault
# Save the configuration as docker-compose.yml
docker compose up -d
docker compose ps
docker compose run --rm api print-bootstrap-token
curl -fsS http://127.0.0.1:8793/api/healthUSER GUIDE
Installation, getting started, settings and troubleshooting.
N3X Vault is a self-hosted password manager with client-side encryption.
Security alpha: use this version only for controlled testing. It is not approved for production secrets.
You need a server with Docker Compose, a browser and the final HTTPS domain. PostgreSQL is included. Hardware unlock requires a compatible browser and a FIDO2 key supporting WebAuthn PRF; password unlock remains available.
docker-compose.yml in a dedicated server directory.docker compose up -d
docker compose ps
docker compose run --rm api print-bootstrap-token
The public application URL and WebAuthn RP ID must match the final domain before enrolling keys. Changing the RP ID later prevents existing hardware credentials from working.
Treat the bootstrap token as a password. Do not store it in public logs or the stack definition. Owner registration closes after the first account is created.
Portainer may not expose the Compose working directory. The token already exists in the persistent volume; read it through the running API container.
docker ps \
--filter label=com.docker.compose.service=api \
--format 'table {{.Names}}\t{{.Label "com.docker.compose.project"}}'
Select the API container belonging to the correct stack and replace PORTAINER_API_CONTAINER with its name:
docker exec PORTAINER_API_CONTAINER \
/usr/local/bin/n3xvault-api print-bootstrap-token
Alternatively, open Portainer → Containers → API container → Console → /bin/sh and run:
/usr/local/bin/n3xvault-api print-bootstrap-token
If the API is unavailable, find the correct stack's bootstrap container. Replace PORTAINER_BOOTSTRAP_CONTAINER with its name; the temporary container reads only the existing volume:
docker ps -a \
--filter label=com.docker.compose.service=bootstrap \
--format 'table {{.Names}}\t{{.Label "com.docker.compose.project"}}'
docker run --rm \
--volumes-from PORTAINER_BOOTSTRAP_CONTAINER:ro \
alpine:3.22 \
cat /run/n3xvault/bootstrap_token
Use the Alpine image reference pinned in the downloaded Compose file if alpine:3.22 is not available locally. Do not remove runtime_secrets or generate a new token to troubleshoot this issue.
After unlocking with your password, open Security, enroll a compatible FIDO2 key and complete the second touch required to configure unlock. Enroll two keys before relying on this method.
With the vault unlocked, select Recovery kit. Store the encrypted .n3xvault file on protected offline media.
Restoration requires an empty installation, the recovery-kit file, the destination's bootstrap token and either the original master password or an enrolled key with PRF. Hardware-key restoration requires the same RP ID. When changing domains, restore using the password and enroll keys again. Test restoration on an isolated empty installation.
Take consistent backups of postgres_data and runtime_secrets, and keep a separate recovery kit. Follow the release notes when updating, preserving volumes and the existing domain:
docker compose pull
docker compose up -d
docker compose ps
For startup failures, check container status and docker compose logs --tail=100 api web. If hardware unlock fails, check HTTPS, the domain, RP ID and browser PRF support. Switching between localhost and 127.0.0.1 also changes the credential context.
Encryption and decryption happen in the client. A deployment or session token alone cannot decrypt vault contents.