The canonical secret store for the entire RevealFleet. Age-encrypted vault with a CLI, a built-in TUI editor, and a Tauri desktop app. 100% passage-compatible.
Per the fleet-wide secrets rule, every secret RevealUI depends on lives here — API keys, database URLs, webhook secrets, JWT/session keys, keypairs, license keys, OAuth client secrets, age identities, SSH keys, anything else. One encryption boundary (the age identity) gates the whole fleet; rotation updates one store and downstream targets (Vercel env vars, Fly app secrets) re-read from the same source.
- Encrypted at rest —
.agefiles using x25519 key exchange (theagecrate, 0.11) - CLI.
init,get,set,generate,list,search,export-env,edit,delete,migrate,sync,doctor,completions,rotate,rotation-verify,rotation-promote,rotation-status. Global--jsonflag for structured output on every command. - Editing —
editdecrypts to$EDITOR(or the editor configured inconfig.toml) and re-encrypts on save. With no editor set it falls back to a built-in ratatui TUI editor. Decrypted plaintext only ever lands in a tmpfs/memfd-backed temp path that is zeroized and unlinked on exit. - Password generation —
generateproduces a strong random password (configurable length, optional--no-symbols/--no-ambiguous), printed, copied to clipboard, or stored under a path. - Desktop app — Tauri 2 backend (
crates/tauri-app) + React 19 frontend (frontend/) - Namespaces — secrets are organized by their first path segment. Built-in namespaces are
revealui/,credentials/,ssh/, andmisc/; any other first segment is treated as a dynamic project namespace (e.g.revforge/,revdev/). - Fuzzy search —
searchfinds secrets by partial path match - Migration —
migrateimports plaintext secret files from external sources with automatic categorization - Rotation.
revvault rotate <provider>runs a full rotation againstlocal,http,neon, ored25519-keypairproviders (optionaldual_slotwrites{path}-next/-previous; promote withrevvault rotation-promote) (auto-selected viatypeinrotation.toml): reads the current key, dispatches the provider, validates the returned value's shape, writes the new key back, applies the optional post-rotation sync hook, runspost_rotatehooks (warn-on-failure) and an optional strictverifygate, then appends a log entry.--dry-runpreviews the plan without touching the vault or any API.rotation-verifyis a read-only dual-slot next-leaf check (presence +computeKeyIdmatch; kids/paths only).rotation-statusreports the last 10 log entries. - Downstream sync —
sync vercelandsync flypush vault secrets to Vercel env vars / Fly app secrets, driven by a TOML manifest. Dry-run by default (--applyto write), with declared-shape validation, orphan detection, a strict no-auto-delete policy, and an append-only audit log. - Path validation — directory traversal and injection attacks blocked
- Health check —
doctorreads every manifest entry and validates value shapes against their declared types
- Nix with flakes enabled
- An age identity at
~/.config/age/keys.txt(XDG location, checked first) or the legacy~/.age-identity/keys.txt
cd ~/revealfleet/revvault
direnv allow # or: nix develop
cargo build --workspace# Initialize a new vault (creates the store directory and an age identity)
revvault init
# Store a secret
echo "sk_live_abc123" | revvault set revealui/prod/stripe/secret-key
# Retrieve it (TTY print; use vault-private / REVVAULT_ALLOW_PRINT=1 under STREAM_SAFE)
revvault get revealui/prod/stripe/secret-key
# Stream-safe: inject into child env only (paths on argv, never values)
revvault run --env STRIPE_SECRET_KEY=revealui/dev/stripe/secret-key -- pnpm stripe:seed -- --dry-run
revvault run --namespace stripe --namespace neon -- pnpm billing:catalog:sync -- --mode test
# Structured output (use --json in scripts — bare `revvault get` is silent in $(...))
revvault --json get revealui/prod/stripe/secret-key | jq -r .value
# Copy to clipboard instead of printing (not stream-safe; vault-private only)
revvault get revealui/prod/stripe/secret-key --clip
# Generate a strong password and store it (default length 32)
revvault generate revealui/prod/some/api-key
revvault generate --length 48 --no-ambiguous --clip
# List
revvault list
revvault list --tree
# Fuzzy search
revvault search stripe
# Edit in $EDITOR (or the built-in TUI editor when EDITOR is unset)
revvault edit revealui/prod/stripe/secret-key
# Export as KEY=VALUE for shell eval
eval "$(revvault export-env revealui/prod/stripe/secret-key)"
# Rotate a provider's key (dry-run, then apply).
# The provider must match a [providers.<name>] block in .revvault/rotation.toml
revvault rotate vercel --dry-run
revvault rotate vercel
# Dual-slot: verify next leaves (no promote; kids only)
revvault rotation-verify license-signing
# Show the last 10 rotation log entries
revvault rotation-status
# Push vault secrets to Vercel env vars (dry-run, then apply)
revvault sync vercel --manifest revvault-vercel.toml
revvault sync vercel --manifest revvault-vercel.toml --apply
# Push vault secrets to a Fly app's secrets
revvault sync fly --manifest fly-secrets.toml --apply
# Validate the store (read every entry, check shapes)
revvault doctor
# Migrate plaintext secret files into the vault
revvault migrate --plaintext-dir <source>
# Delete
revvault delete revealui/prod/stripe/secret-key
# Shell completions
revvault completions bash >> ~/.bashrcFor a retryable single-secret conditional write, pipe the desired UTF-8 bytes to
revvault set <path> --operation-id <UUID> --expected-current-sha256 <SHA256>.
Use --expected-absent instead of a hash for creation. The hash describes the
exact decrypted current bytes; conditional stdin preserves whitespace. Reuse
an operation UUID only for the identical path, expected value and desired bytes.
The command prints a secret-free JSON receipt. A committed retry reports
current_matches: false if a later write replaced that operation's value; it
never restores the older value.
Conditional writes require Unix directory syncing. Interrupted prepared writes
fence ordinary reads and writes until the identical operation retries; the
journal and durable receipt are age-encrypted under the store's recipients.
Completed receipts are retained indefinitely; there is no automatic cleanup,
because removing a receipt would discard its operation's retry guarantee.
Writers require writable coordination metadata under .revvault. On Unix,
readers open an existing permanent lock read-only; historical read-only stores
without coordination metadata fail closed until supported writable bootstrap.
All maintained writers use the same OS lock, released automatically on process
exit. External Passage writers do not participate in this protocol. This
contract covers one leaf, not atomic promotion of several secrets.
nix develop
cargo tauri devPaths are lower-kebab, grouped by repo or product, then by subsystem:
| Pattern | Example |
|---|---|
revealui/dev/<subsystem>/<name> |
revealui/dev/electric/service-url, revealui/dev/admin-session-cookie |
revealui/prod/<subsystem>/<name> |
revealui/prod/neon/postgres-url, revealui/prod/stripe/secret-key, revealui/prod/stripe/webhook-secret |
revealui/prod/storage/r2/<name> |
revealui/prod/storage/r2/access-key-id |
revforge/customers/<slug>/<name> |
Operator/private RevForge tenant path (no public RevForge repo), e.g. revforge/customers/acme/admin-password |
revdev/<name> |
revdev/license-signing-private-key |
credentials/<system>/<name> |
credentials/github/personal-token, credentials/anthropic/api-key |
New paths get a docs/SECRETS.md entry in the relevant repo. Mirroring to CI is a publish step, never hand-typed.
Store, identity, editor, and tmpdir resolve in this order — config file wins, then environment, then platform default:
| Setting | Config file (~/.config/revvault/config.toml) |
Env var | Default |
|---|---|---|---|
| Store directory | store_path |
REVVAULT_STORE (or legacy PASSAGE_DIR) |
~/.revealui/passage-store |
| Identity file | identity |
REVVAULT_IDENTITY |
~/.config/age/keys.txt, then ~/.age-identity/keys.txt |
| Editor | editor ("builtin" forces the TUI editor) |
EDITOR |
built-in TUI editor |
| Temp dir (for edit) | tmpdir |
TMPDIR |
/dev/shm / memfd / OS temp dir |
crates/core — shared library (store, crypto, identity, config, namespaces, import, rotation, sync, init)
crates/cli — revvault CLI binary (clap)
crates/tauri-app — Tauri 2 desktop backend
frontend/ — React 19 + TypeScript + Tailwind CSS v4 (Vite)
Workspace at version 0.4.0 (pre-1.0 per fleet versioning; see Cargo.toml workspace package + CHANGELOG.md).
Secrets live in a directory hierarchy as .age files:
~/.revealui/passage-store/
├── .age-recipients
├── revealui/
│ ├── dev/
│ │ └── electric/service-url.age
│ └── prod/
│ ├── neon/postgres-url.age
│ └── stripe/secret-key.age
├── revforge/ # operator/private project namespace (RevForge is not a public repo)
│ └── customers/
│ └── acme/admin-password.age
├── credentials/
│ └── github/personal-token.age
└── ssh/
└── github.age
# Enter dev shell
direnv allow # or: nix develop
# Run all CI checks (fmt, clippy, tests, frontend build)
bash scripts/ci.sh
# Run tests
cargo test --workspace
# Run specific crate tests
cargo test -p revvault-core
cargo test -p revvault-cliLibrary errors use thiserror; binary errors use anyhow. Decrypted values are wrapped in secrecy::SecretString and never logged. All encryption goes through the age crate — no custom crypto.
MIT.