Sanctuary

Security

Don't trust. Verify.

Sanctuary is a watch-only coordinator. It sees what your addresses see on-chain — and builds transactions for your hardware wallet to sign. That's all. The architecture below exists so you can inspect exactly what it holds, where it runs, and where trust lands.

What's architecturally guaranteed. The server never holds a Bitcoin private key, seed, or mnemonic — every signing path lives on the hardware wallet. Verify it yourself: grep the source for privateKey, seed, or mnemonic and you'll find zero matches in the server. AES-256-GCM at rest with scrypt-derived keys, JWT revocation, TOTP 2FA, per-route wallet RBAC, double-submit CSRF, HMAC-signed gateway calls, and an isolated AI proxy with no DB access.
Sensible defaults out of the box. Strong password policy, fail-closed Redis-backed rate limiting, Helmet CSP, HSTS, Dependabot + gitleaks + CodeQL as blocking CI gates, ~99% backend test coverage including auth and authorisation paths, and all SQL parameterised through Prisma.
Honest about where we are. Sanctuary is pre-1.0 and hasn't had an independent third-party audit yet. The whole codebase is open source and auditable by anyone — the test suite catches regressions on the security-sensitive paths, and we run our own internal review pass before each release. Start small while you get comfortable, and always verify every transaction on your hardware wallet. Full disclaimer.

What Sanctuary holds — and what it never holds

Stored on your server

  • › Extended public keys (xpubs) — watch-only
  • › Wallet metadata (names, descriptors, settings)
  • › Transaction and address history
  • › Address derivation state
  • › Labels and user-supplied annotations
  • › User accounts, preferences, encrypted 2FA secrets
  • › Audit logs of security-relevant events

Never stored, never transmitted

  • › Private keys
  • › Seed phrases
  • › Wallet passwords / passphrases
  • › Hardware-wallet PINs

These live only on your hardware wallet. Sanctuary has no way to recover funds if that device is lost without its seed backup.

The signing flow, end-to-end

  1. 1

    You build the transaction in Sanctuary

    Recipient, amount, fee rate, UTXO selection. Sanctuary assembles this into a PSBT — a standard format that describes the transaction without any signatures.

  2. 2

    The PSBT is handed to your hardware wallet

    Over USB (WebUSB / WebHID / WebSerial), over QR code, or over a file on MicroSD. Pick whichever gives you the isolation you want.

  3. 3

    You verify on the device screen

    This is the load-bearing step. The hardware wallet shows the recipient address, amount, and fee — rendered by the device itself, not by Sanctuary. If those match what you intended, press confirm. If they don't, refuse.

  4. 4

    The signed PSBT comes back

    The signature was computed inside the hardware wallet. The private key never leaves it. Sanctuary takes the signed PSBT, finalizes it, and broadcasts it to the network.

The rule: if Sanctuary is ever compromised, the worst an attacker can do is present a false transaction for your device to sign. The device's screen is your last line of verification — always read it before you press confirm.

Architecture

Sanctuary architecture diagram showing frontend, backend, worker, gateway, Redis, and PostgreSQL components
Component Port Purpose
Frontend:8443React UI served via nginx (HTTPS for WebUSB)
Backend:3001Node.js API — wallet logic and user requests
Worker:3002Electrum subscriptions, sync, notifications, confirmations
Gateway:4000Mobile API with JWT auth, route whitelisting, rate limiting
Redis:6379Job queue (BullMQ) and distributed cache
PostgreSQL:5432Primary database

All components run in Docker containers under your control. None of them reach out to a Sanctuary-owned service — there isn't one.

HTTPS & secure contexts

Browsers only expose WebUSB, WebHID, WebSerial, and camera APIs from a secure context — HTTPS or localhost. The default setup ships HTTPS on port 8443 with a self-signed certificate. That's why you see a certificate warning on first visit — it's expected and you should proceed past it on localhost.

Encryption at rest

Sensitive values — Electrum node passwords, 2FA shared secrets — are encrypted in the database using the ENCRYPTION_KEY / ENCRYPTION_SALT pair generated during install. User passwords are hashed with bcrypt. Electrum server TLS certificates are verified by default; self-signed certs are rejected unless explicitly allowed.

When restoring a backup onto a different instance (different encryption key), the data you can still decrypt is imported cleanly; anything tied to the old encryption boundary is cleared with a clear warning, never silently re-keyed. See Backup & restore for the full matrix.

Network exposure

By default Docker binds ports to 0.0.0.0, meaning Sanctuary is reachable from every device on your LAN. To restrict to localhost only:

# docker-compose.override.yml
services:
  frontend:
    ports:
      - "127.0.0.1:${HTTP_PORT:-80}:80"
      - "127.0.0.1:${HTTPS_PORT:-443}:443"

For remote access, put Sanctuary behind a reverse proxy (nginx / Caddy / Traefik) with a proper certificate, or reach it over a VPN.

What you're responsible for

Self-hosting moves real work onto you. The short list:

Reporting security issues

Found a vulnerability? Please don't post exploit details publicly. Open a brief Codeberg issue asking for a private disclosure channel.