← All extras

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.

better-authTOTP 2FAEncrypted at restBearer API keysAllowlisted proxysite-vault CLI

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.

Open on the main site ↗

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.

Open sign-in on the main site

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 — header sends an Authorization Bearer header, body adds 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/:id so 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.

Open API keys on the main site

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/:id

The 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.

Try it on the main site.

This mirror is static — the vault, keys, and admin board run on the live deployment.