Extras · Self-hosted API bank
API bank & secret vault
This site carries its own secret manager: per-user encrypted vault, Bearer API keys, a host-allowlisted proxy so third-party keys never touch client machines, a no-dependency PC CLI, and an admin board — all behind better-auth sign-in with free two-factor. This page documents the whole system.
You're reading the static mirror.
Sign-in, the vault, keys, and the admin board need the live backend — they run on the main site, not on this GitHub Pages copy.
The tour
How the pieces fit
Six stops from first sign-in to automated calls from your own computer. Secrets are encrypted at rest and only ever unmasked on demand or inside the server-side proxy.
01
Sign in
Email + password, GitHub, or Google. Turn on two-factor with any authenticator app after sign-up; sign-ins after that ask for a one-time code. Backup codes cover a lost phone.
02
Store secrets
The /vault page saves name, value, allowed hosts, and how the secret is injected (header or body). The list only shows a masked preview — values are revealed on demand, in memory only.
03
Mint an API key
The /keys page creates Bearer keys for scripts and services. The raw key is shown exactly once, at creation — after that only metadata is listed.
04
Call through the proxy
POST /api/proxy/:id with a target URL. The server checks the host allowlist, injects the secret itself, and returns the upstream response — clients never see the value.
05
Drive it from a PC
The site-vault CLI (node cli/vault.mjs, no dependencies) wraps the same API: login, list, get, push, push-env-file, delete, proxy. Credentials live at ~/.config/site-vault/config.json, mode 600.
06
Administer
The admin board manages users and roles. The first admin is seeded server-side with scripts/seed-admin.mjs, reading VAULT_ADMIN_* strictly from the environment.
Sign-in & accounts
Auth with free two-factor
Email + password plus GitHub and Google OAuth, with TOTP two-factor that costs nothing — any authenticator app works.
Methods
- Email + password, or one click with GitHub / Google.
- Enroll two-factor after sign-up: scan the QR code with any authenticator app and save the backup codes somewhere safe.
- Later sign-ins ask for the six-digit code — an OTP challenge before the session is granted.
- Lost phone? A backup code gets you back in; then re-enroll.
Admin board
Admins get a board for user and role management. The very first admin is seeded server-side (see the seeding section below) — never through the UI, never with credentials in the repo.
The vault
Secrets, masked by default
The /vault page stores each secret with the metadata the proxy needs. The list never prints values — only a masked preview — until you explicitly reveal one.
Creating a secret
- Name — e.g.
OPENAI_API_KEY. - Value — encrypted before it rests in the database.
- Allowed hosts — comma-separated (api.openai.com); required for proxying, and the proxy refuses anything else.
- Inject as —
headersends an Authorization Bearer header,bodyadds the secret to a JSON body field instead. - Description — optional plaintext note saying what the key is for (never the value itself, max 500 chars).
Living with secrets
- Masked list — every row shows a preview, never the value; filter it client-side by name, description, or host.
- Reveal on demand — one click fetches the plaintext into memory only; hide it when done.
- Proxy test — each row has a built-in panel that fires a real request through
POST /api/proxy/:idso you can verify a key without ever seeing it.
Open the vault on the main site
Duplicates are handled on demand only — nothing is checked on write. The /vault page's Find duplicates button (and site-vault dedupe [--prune]) scans for exact copies and near-matches server-side with masked previews only: prune deletes exact copies keeping the oldest, while near-matches are always merged manually via Edit/Delete and are never auto-deleted.
Programmatic access
API keys carry a Bearer token
Keys created at /keys authenticate scripts and services against the vault and proxy APIs. The raw key appears exactly once — copy it, store it, and it is never shown again.
Calling the API
curl -H "Authorization: Bearer <key>" \ https://your-app.vercel.app/api/vault
Only key metadata is ever listed (name, id, created / last-used dates). Deleting a key immediately stops every caller using it.
Trust model: minting or deleting a key requires a signed-in browser session — Bearer keys can't mint or revoke keys. Keys do not trigger a two-factor challenge when they are used, so treat them like passwords. New keys expire after 90 days (1–365 days on request) and are usage-capped.
The CLI can mint keys too: pipe your password to site-vault mint --email you@example.com (add --otp <code> for two-factor accounts, --save to store the fresh key as this machine's login). Like the /keys page it signs in with a session — Bearer keys can't mint by design — and the raw key prints exactly once. See cli/README.md for flags.
Proxy without touching the secret
curl -X POST \
-H "Authorization: Bearer <key>" \
-H "content-type: application/json" \
-d '{"url":"https://api.openai.com/v1/models","method":"GET"}' \
https://your-app.vercel.app/api/proxy/:idThe server checks the URL against the secret's allowed hosts, injects the value as a header or body field, and returns the upstream status, headers, and body.
Trusted-hosts warning: anyone able to call the proxy can make the server send the secret to any allowlisted host — only allowlist hosts you trust with the value. Targets must be public https hosts on the default port (no credentials in the URL); private, loopback, link-local, and other reserved ranges are rejected after DNS resolution, and the resolution is re-checked after the call.
From your computer
The site-vault CLI
A plain-node companion (zero dependencies) with the same power as the web UI. Run it in place or link it onto your PATH — values stay masked unless you explicitly ask to reveal one.
Setup & everyday use
# run in place, or link it onto PATH node cli/vault.mjs --help npm link site-vault --help # log in (one authenticated round-trip, then saved) # --api-key-stdin / --api-key-env avoid leaving the key in shell history printf '%s' "$SITE_VAULT_KEY" | site-vault login --url https://example.com --api-key-stdin site-vault login --url https://example.com --api-key-env SITE_VAULT_KEY # list and read (masked previews only) site-vault list site-vault get OPENAI_API_KEY site-vault get OPENAI_API_KEY --reveal # raw value on stdout # delete site-vault delete OLD_KEY
Pushing & proxying
# push (upsert: creates, or updates on name conflict) printf '%s' "$OPENAI_API_KEY" | \ site-vault push OPENAI_API_KEY --value-stdin --hosts api.openai.com site-vault push STRIPE_KEY --env-key STRIPE_SECRET --inject header # bulk import from an env file (names only are ever printed) site-vault push-env-file .env.local --all site-vault push-env-file .env.local --prefix OPENAI_ site-vault push-env-file .env.local --allowlist STRIPE_SECRET,OPENAI_API_KEY # proxy: secret is injected server-side, never touches this machine site-vault proxy OPENAI_API_KEY --url https://api.openai.com/v1/models
Login details are stored at ~/.config/site-vault/config.json with mode 600 — readable only by you. Prefer --api-key-stdin or --api-key-env over --api-key, which can linger in shell history. Proxy calls carry the same trusted-hosts rule as the web UI: the server will send the secret to any allowlisted host.
Operations
Seeding the first admin
scripts/seed-admin.mjs creates the admin through the better-auth API and sets role = admin. It reads strictly from the environment and refuses to run unless all three variables are set.
export VAULT_ADMIN_EMAIL='admin@example.com' export VAULT_ADMIN_NAME='Site Admin' export VAULT_ADMIN_PASSWORD='<strong-random-password>' # plus the app's own server vars: BETTER_AUTH_SECRET, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN node scripts/seed-admin.mjs
Never argv, never hardcoded, nothing in the repo. Full details live in cli/README.md next to the CLI docs.