Vaultbase
Vaultbase is a self-hosted, open-source secret management platform — think of it as your own private Infisical or HashiCorp Vault that you run on your own infrastructure. Instead of scattering API keys, database credentials, and certificates across .env files, CI settings pages, and Slack messages, teams store them in one encrypted vault and pull them into apps, servers, and pipelines on demand.
Under the hood, Vaultbase uses PostgreSQL as its storage engine, with the supabase_vault extension doing the heavy cryptographic lifting. The extension provides an encrypted vault.secrets table backed by libsodium's authenticated encryption, with the root key held outside the database — so a leaked database dump contains only ciphertext. Vaultbase never implements encryption itself: secret values live in the vault, while everything that makes it a product — projects, environments, folders, versions, tokens, audit — lives in ordinary Postgres tables that reference vault secrets by ID.
The goal of the hackathon project is to build a working slice of this platform: a secret store (API + web UI + CLI) that keeps values encrypted in Postgres via supabase_vault, organizes them per project and environment, and injects them into a running application without ever writing them to disk.
What you are building
At a high level, a user of Vaultbase should be able to:
- Create a project — a container for secrets, with environments like
development,staging, andproduction. - Store secrets — key/value pairs (API keys, DB URLs, certificates), organized by environment and folder path. Values land in the extension's encrypted
vault.secretstable; your own tables hold the folder tree, environment mapping, and version history. - Control access — invite teammates with roles, and mint service tokens so machines (CI jobs, servers) can read only the secrets they need.
- Inject secrets — run
vaultbase run -- npm startand have the CLI fetch secrets and inject them as environment variables, no.envfile involved. - Audit & version — see who read or changed what and when, diff versions, and roll a secret back.
Architecture overview
Vaultbase is split into the server (API + Postgres with the supabasevault extension) and the clients that consume secrets (web UI, CLI, SDKs, CI integrations). Encryption happens _inside Postgres: the API never handles keys or ciphertext, only plaintext over TLS and vault secret IDs.
The pieces
1. Web UI A single-page app where users manage projects, browse secrets per environment, edit values, compare environments side by side, invite teammates, and read the audit log. Secret values are masked by default and revealed (and audited) on click.
2. API Server The core of Vaultbase. Responsibilities:
- Authentication & authorization — user accounts, sessions, roles per project, and API/service tokens for machines.
- Domain model — projects, environments, folders, secrets, secret versions, tokens, audit events.
- Vault orchestration — every write of a secret value calls
vault.create_secret(), every read selects fromvault.decrypted_secrets; the API never sees keys or ciphertext, and never logs plaintext. - Audit trail — every access is recorded before the response is returned.
3. supabase_vault (the crypto engine) The supabase_vault extension is where encryption actually happens, so the team writes zero crypto code:
vault.create_secret(value, name)encrypts a value with libsodium authenticated encryption and stores the ciphertext invault.secrets, returning a UUID.- The
vault.decrypted_secretsview decrypts on read — which meansSELECTon that view is the crown jewels. Lock it down: the API connects as a dedicated role that is the only role granted access, and no other user or service gets near it. - The root key lives outside the database (supplied to Postgres via configuration at startup), so a stolen dump of
vault.secretsis useless ciphertext. - Practical constraint: the extension must be installed in your Postgres image (Supabase publishes one; you can also build it into a stock
postgresimage). Managed providers like RDS or Neon don't ship it — fine for a self-hosted product, but worth knowing.
4. State database (PostgreSQL — the metadata layer)
Everything around the secret values is plain relational modeling you own: users, projects, environments, folders, secret metadata (key name, path), immutable version rows, tokens, and the append-only audit log. Each secret_version row stores a vault_secret_id pointing at the encrypted value in vault.secrets. Because the vault is just another table in the same database, a secret write, its version row, and its audit entry all commit in one transaction — or not at all.
5. CLI
How secrets actually reach applications. vaultbase login authenticates, vaultbase run --env production -- node server.js fetches secrets over HTTPS, decrypted server-side, and injects them into the child process's environment. vaultbase pull can export a .env file for tools that need one. Machines authenticate with a scoped service token instead of a user login.
6. Service tokens & machine identities A CI job or production server shouldn't log in with a human's password. Service tokens are long-lived, revocable credentials scoped to one project + one environment + read-only (for example). The token string is shown once at creation and only its hash is stored — like GitHub personal access tokens.
7. Audit logger
An append-only table recording every event: secret.read, secret.updated, token.created, member.invited — with actor, IP, timestamp, and the affected keys (never the values). This is what makes the vault trustworthy in a team setting.
How a secret flows
Understanding this end-to-end path is the heart of the project:
- Write — a user saves
STRIPE_KEY=sk_live_...forproductionin the UI. The API authorizes the request and opens a transaction. - Encrypt & version — inside that transaction, the API calls
vault.create_secret()(the extension encrypts and returns a UUID), inserts an immutablesecret_versionrow holding thatvault_secret_id, points the secret'scurrent_versionat it, and writes an audit event. One commit covers all of it. - Read — a CI job calls
GET /api/projects/:id/secrets?env=productionwith its service token. The API checks the token's scope, joins the current version rows tovault.decrypted_secrets(decryption happens inside Postgres), logssecret.readto the audit trail, and returns plaintext over TLS. - Inject — the CLI receives the key/value pairs and
execs the target command with them merged into its environment. Nothing is written to disk. - Rotate — the user updates
STRIPE_KEYto a new value. A new vault secret is created and wrapped in a version 2 row; version 1's vault entry stays untouched, so history and one-click rollback come for free. - Revoke — the CI provider is compromised, so the user deletes its service token. The very next request with that token gets a
401, and the audit log shows exactly what the token read while it was alive.
Technologies to use
The whole stack is TypeScript / JavaScript end-to-end — pick what your team is fastest in within that ecosystem. The concepts matter more than the exact library.
| Layer | Options |
|---|---|
| Runtime | Node.js or Bun (TypeScript throughout) |
| API server | Hono (with @hono/node-server), or Hono on Bun |
| Secret encryption | The supabase_vault Postgres extension (libsodium authenticated encryption) — no application-level crypto code |
| Database | PostgreSQL (image with supabase_vault installed, e.g. supabase/postgres) with Drizzle ORM (or Prisma) for the metadata tables |
| Auth | Session cookies or JWT; argon2 / bcrypt for password hashing; SHA-256 hashes for token storage (node:crypto covers this) |
| Web UI | React (Next.js or Vite) |
| CLI | A small TypeScript CLI with commander or citty; spawn child processes with execa |
| Validation | Zod (shared types between API, CLI, and UI) |
| Audit log | A plain append-only Postgres table — no extra infra needed |
Concepts to understand
Before writing code, make sure the team is comfortable with these ideas.
Applied cryptography (what the vault does for you)
- Authenticated encryption (AEAD) — supabase_vault uses libsodium's authenticated encryption: it doesn't just hide the value, it detects tampering. Know what the key, nonce, and auth tag are conceptually, even though the extension manages them.
- Root key custody — the key that unseals the vault lives in Postgres configuration, never in the database it protects. Understand why that separation is the whole point.
- Hashing vs. encryption — passwords and token strings are hashed (one-way, argon2/bcrypt); secret values are encrypted (two-way, because you need them back). The vault handles the second; you still implement the first.
- The trust boundary —
vault.decrypted_secretsdecrypts for anyone who canSELECTit, so database grants are your encryption boundary. A dedicated API role, and nothing else, touches the vault schema. - What not to do — never log plaintext, never roll your own crypto when an audited extension exists, never let migrations or debug tooling connect as the vault-privileged role.
Data modeling for secrets
- Projects → environments → folders → secrets — the hierarchy that maps to how teams actually organize configuration. All of it lives in your own metadata tables; only the values live in
vault.secrets. - Metadata / value split — a
secret_versionrow carries the key name, path, actor, and avault_secret_idforeign reference; the plaintext-equivalent lives only behind the vault. Deleting metadata without orphaning vault rows (and vice versa) is part of the design. - Immutable versioning — updates create a new vault secret + a new version row instead of overwriting, enabling history, diffs, and rollback.
- Soft deletes — a "deleted" secret stays recoverable and keeps the audit trail intact.
Authentication & authorization
- Humans vs. machines — interactive sessions for people, scoped long-lived tokens for CI and servers.
- Role-based access control — admin / developer / viewer per project; who can read
productionvs.development? - Token scoping and revocation — least privilege: a token for one project + one environment + read-only.
API & transport security
- TLS everywhere — plaintext secrets only ever exist in memory and on an encrypted wire.
- Keeping secrets out of logs — request logging middleware must redact bodies on secret endpoints.
- Constant-time comparison — comparing token hashes with
timingSafeEqualto avoid timing attacks.
Operational concerns
- Root key at boot — Postgres needs the vault root key in its configuration at startup; the database is sealed (by design) without it. What does your restart / backup-restore story look like?
- Running Postgres with the extension — the
supabase/postgresDocker image ships it; managed providers (RDS, Neon) don't. Your deployment docs must account for this. - Audit-first design — the access log is written in the same transaction as the access itself.
- Secret rotation workflows — how teams rotate a leaked key without downtime.
Suggested milestones
A realistic path for a hackathon, from smallest useful slice to full loop:
- Vault round-trip — stand up Postgres with the
supabase_vaultextension (Docker), then prove the loop:vault.create_secret()→ read back viavault.decrypted_secrets→ confirm a raw dump ofvault.secretsshows only ciphertext. - Persist state — model users, projects, environments, folders, secrets, and versions in your own tables, with version rows referencing vault secret IDs; wire up the API so every write/read goes through the vault.
- Minimal UI — log in, create a project, add/reveal/edit secrets per environment with masked values.
- Close the loop with the CLI —
vaultbase run --env dev -- <cmd>fetches secrets with a service token and injects them into the child process. - Versioning & audit — every change creates a version, every access writes an audit event; show both in the UI with rollback.
- Polish — RBAC roles, environment diffing, token scoping and revocation,
.envimport/export.
Stretch goals
- Point-in-time secret references — pin a deployment to secret versions as of a given moment.
- Secret scanning — detect secrets accidentally committed to a connected Git repo.
- Dynamic secrets — generate short-lived Postgres credentials on demand and revoke them after TTL.
- Kubernetes / Docker integration — an agent or operator that syncs Vaultbase secrets into cluster secrets.
- Webhooks & integrations — push updated secrets to GitHub Actions, Vercel, or trigger redeploys on change.
- End-to-end encryption mode — an extra client-side encryption layer in the browser and CLI, so even the server and vault only ever see values the client already encrypted.
- Root key via cloud KMS — fetch the vault root key from AWS KMS / GCP KMS at Postgres startup instead of static configuration.