Hard asset inventory for bullion, crypto, and other vaulted goods. Local-first: you run it on your machine. It is not a public internet service.
Passkeys (WebAuthn) are the sign-in method. Use http://localhost:5173 in development, or the container App URL — 127.0.0.1 will fail passkey registration.
Project site (static): site/index.html. After GitHub Pages is on: https://ty-haller.github.io/VaultBox/
- Docker Engine or Podman, or Python 3.11+ and Node.js 20+
- A browser that supports passkeys
Either engine can build and run the image. scripts/bootstrap.sh uses docker if docker info works, otherwise Podman.
| Docker Engine | Podman (Fedora/RHEL default) | |
|---|---|---|
| Install | docs.docker.com/engine/install | sudo dnf install -y podman podman-compose |
| Daemon | sudo systemctl enable --now docker; user in docker group |
Rootless; no daemon |
| CLI | docker / docker compose |
podman / podman compose (or DOCKER=podman ./scripts/bootstrap.sh) |
| HTTP bind | Script publishes 127.0.0.1:8000 (avoids IPv6 localhost resets on rootless stacks) |
Same |
| Ports 80/443 | Fine as root/docker group | Rootless cannot bind 80/443 unless you lower net.ipv4.ip_unprivileged_port_start; use 8080/8443 instead |
Optional shim if you want the docker command on a Podman host:
mkdir -p ~/.local/bin
printf '%s\n' '#!/bin/sh' 'exec podman "$@"' > ~/.local/bin/docker
chmod +x ~/.local/bin/dockerFrom a clone:
git clone https://github.com/Ty-Haller/VaultBox.git
cd VaultBox
chmod +x scripts/bootstrap.sh
./scripts/bootstrap.shThe script builds the image, writes .env if missing, and publishes http://localhost:8000. Open that URL (not 127.0.0.1) and Register Admin Passkey. Data lives in the volume vaultbox-data.
docker logs -f vaultbox # or: podman logs -f vaultbox
docker stop vaultboxCompose (same HTTP setup), after copying .env.example to .env:
docker compose up --build -d
# or: podman compose up --build -dCaddy sits in front of VaultBox, terminates TLS, and forwards to the app. Set VAULTBOX_HOSTNAME to the name in the browser (that is also the WebAuthn RP ID). After a hostname change, register a new admin passkey at the new App URL.
Stop the HTTP container first if it is already running (docker stop vaultbox or docker compose down).
LAN / localhost (Caddy local CA, no public DNS):
# .env should include VAULTBOX_HOSTNAME=localhost (or vault.home.arpa)
docker compose -f docker-compose.https.yml up --build -d
# Copy Caddy's local CA onto the host and trust it in the OS/browser (once):
docker compose -f docker-compose.https.yml cp \
caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-local-root.crtImport caddy-local-root.crt (Firefox/Chrome certificate settings, or sudo trust anchor ./caddy-local-root.crt on Fedora). Open https://localhost/login (or https://$VAULTBOX_HOSTNAME/login). Keep tls internal in deploy/Caddyfile.internal.
Public hostname (Let’s Encrypt): DNS for VAULTBOX_HOSTNAME must point here, and ports 80 + 443 must be reachable.
export VAULTBOX_HOSTNAME=vault.example.com
export CADDY_EMAIL=you@example.com
export CADDYFILE=./deploy/Caddyfile
docker compose -f docker-compose.https.yml up --build -dRootless Podman (cannot bind 80/443):
export CADDY_HTTP_PORT=8080
export CADDY_HTTPS_PORT=8443
export VAULTBOX_HTTP_PORT=8443
export VAULTBOX_HOSTNAME=localhost
docker compose -f docker-compose.https.yml up --build -dThen use https://localhost:8443/login.
Caddy files: deploy/Caddyfile.internal (LAN) and deploy/Caddyfile (Let’s Encrypt). Traefik or nginx would work the same way: TLS in front, proxy to vaultbox:8000, send Host and X-Forwarded-Proto.
git clone https://github.com/Ty-Haller/VaultBox.git
cd VaultBox
python3 -m pip install --user -r backend/requirements.txt
cd backend
python3 manage.py migrate
python3 manage.py seed_roles
python3 manage.py seed_admin
python3 manage.py seed_notifications
python3 manage.py seed_data # optional demo holdings
python3 manage.py runserver # http://127.0.0.1:8000npm install
npm run dev # http://localhost:5173First visit http://localhost:5173/login and Register Admin Passkey for user admin. There is no password login.
On first backend start Django writes a repo-root .env (SECRET_KEY, VAULTBOX_ENCRYPTION_KEY, mode 0600). Do not commit .env. See .env.example and SECURITY.md.
Reset demo inventory later from Admin → Danger zone (type RESET), or python3 manage.py seed_data --flush.
- Inventory — Site → Vault → Holding, including bullion, crypto, gems, watches
- Passkeys — WebAuthn; optional OIDC SSO
- Audits — Standard and advanced vault counts; cancelable sessions
- Reports — Portfolio, inventory, QR labels, purchase/sale, P/L (PDF + CSV)
- Live prices — Metals (and ticker crypto/stocks) with fallback warnings
- Notifications — Event engine with in-app, email (SMTP), and Apprise (Discord, Slack, Telegram, and other URL targets)
- Alerts — Spot/market % change and price above/below; vault audit due/overdue and capacity; holding gain/loss; insurance expiry; plus audit, admin, and inventory events
- QR labels — Per holding; lookup at
/lookup/:code - Backups — On-demand and scheduled SQLite + media archives (optional encryption + rclone)
- Secrets — Encrypted seed phrases and recovery data
- Admin — NetBox-style taxonomy, users, roles
Captured from the local demo at http://localhost:5173. Passkeys fail on 127.0.0.1.
Login — VaultBox, Hard Asset Inventory, passkey sign-in.
Dashboard — portfolio totals, charts, and the live price ticker.
Inventory — holdings table with filters and CSV export.
Vault — one vault: audit status, utilization, holdings.
Reports — PDFs plus purchases & sales (all-time).
Admin — taxonomy, users, backups, SSO.
Notification defaults — system event catalog and default channels (in-app, email, Apprise).
Role defaults — per-role subscriptions for the notification engine.
Notification preferences — which alerts you get and how they are delivered.
Apprise — pick Discord, Slack, Telegram, and other URL targets (no secrets in this shot).
Market alerts — % change and price above/below on an instrument; delivery is in user preferences.
SMTP delivery — admin relay for email notifications.
Passkey login is always on. SSO is optional under Admin → SSO / OAuth. Callback URL:
{backend-base}/api/auth/oauth/{provider-id}/callback/
Set the public hostname in .env (VAULTBOX_HOSTNAME, optional VAULTBOX_USE_HTTPS=true) or under Admin → Site & Hostname. Open the App URL shown on that page — passkeys fail if the browser host does not match the RP ID. Dev localhost uses ports 5173 / 8000; Docker HTTP uses http://localhost:8000; Docker+Caddy uses https:// plus that hostname. 127.0.0.1 is not valid for passkeys. After a hostname/RP ID change, open the new App URL and register a new admin passkey (login offers bootstrap because that host has no keys yet). Keep the old session open until that succeeds if you may need to revert. When the new host has a key, Full Admin can remove leftover old-host passkeys from Admin → Danger zone.
Admin → Backups
- Plain
.tar.gzor encrypted.vaultbox(AES-256-GCM). The archive password is not stored — lose it and the backup cannot be restored. - Scheduled retention (keep N) via
python3 manage.py run_scheduled_backups. - Optional rclone off-site copy (
rclonemust be onPATH).
Keep .env with the database. Changing VAULTBOX_ENCRYPTION_KEY makes existing Fernet ciphertext (holding secrets, wrapped schedule passwords) undecryptable.
Restart the app after a restore so Django reloads SQLite.
Authenticated JSON at /api/. UUID primary keys.
| Resource | Path |
|---|---|
| Sites / vaults / holdings | /api/sites/, /api/vaults/, /api/holdings/ |
| QR lookup | /api/lookup/{code}/ |
| PDFs | /api/reports/{portfolio|inventory|labels|purchase-sale|profit-loss}.pdf |
| Auth | /api/auth/ (passkeys, CSRF, SSO) |
| Admin | /api/admin/… |
| Demo seed (Full Admin) | POST /api/seed/ |
React 19 · TypeScript · Vite · Tailwind CSS v4 · Django 6 · SQLite · Docker (or Podman)
Also: Django REST Framework, Recharts, React Router, Pillow, ReportLab, qrcode, webauthn, cryptography, Apprise.
VaultBox is free, local-first software. If it is useful, you can fund development:
News and chat: X / @hallert
Apache License 2.0. See NOTICE. Contributions are under the same license (CONTRIBUTING.md).
See SECURITY.md. This tree is meant to stay on localhost or a private network — not on the public internet.












