One engine for secrets and parameters — local-first on the OS Keychain, productionizable to a zero-knowledge SaaS where the server only ever sees ciphertext. The lead paid product in the garrid platform.
The repo already had two half-stories: ndev secrets (macOS Keychain credentials)
and ndev config (file-backed parameters). Neither had versioning, an SDK, or any way
to share a value across machines or teammates without copy-paste or a committed .env.
nvault unifies both behind one Go core and gives it three faces, a typed SDK in two languages, and an end-to-end-encrypted cloud tier. The local experience gets easier; the capability becomes a product.
Secrets stay in the OS Keychain; params are typed files
under ~/.nicos-dev/vault. Works fully offline.
ndev vault, nvault, and the existing
ndev secrets all write the same Keychain item — proven by a hermetic test.
Cloud sync uploads ciphertext sealed to each member's key. The server cannot read a single secret.
Params carry a type (string/int/bool/json) and a monotonic version; the cloud keeps full history.
The local CLI is free and open — secrets in your OS Keychain, typed params on disk, offline forever. The paid tier is the zero-knowledge team cloud: end-to-end-encrypted sync, RBAC, audit log, and SDKs. Priced to the value of replacing a secrets manager and a config service with one engine your server can't read.
Land tier — a single team, narrow scope, undercuts a standalone secrets manager.
The value-based price: secrets + params unified, zero-knowledge, with the surplus that makes the buyer say yes (WTP ceiling ≈ $36K).
Expand tier — captures high-WTP accounts; approaches the WTP ceiling with self-host, compliance, and support SLAs.
docs/active/06-07-1328-gtm-full-packet/nvault-pricing.md): next-best alternative $21K +
50% of $15K differentiation value = a $28.5K anchor under a $36K willingness-to-pay ceiling. The number
is a model, not yet a validated WTP — confirm with a Van Westendorp / Gabor-Granger survey before locking
list prices. Unit economics: GO gate, LTV:CAC 11.8×, 4.2-mo payback, NRR 113%, Rule of 40 = 130.Every surface dispatches through the same internal/nvaultcli engine over
internal/vault, so behavior can never diverge.
| Surface | What it's for | Example |
|---|---|---|
| nvault | Standalone product binary (the SaaS brand, like nship) | nvault run -- pnpm dev |
| ndev vault | Integrated into the daily driver | ndev vault ls --scope prod |
| ndev secrets / config | Existing surfaces, unchanged, same substrate | ndev secrets get OPENAI_API_KEY |
# store a secret (never via argv history — pipe it) printf '%s' "$OPENAI_API_KEY" | nvault set OPENAI_API_KEY --stdin # a typed parameter nvault set PORT --value 3000 --param --type int # list (secret values are NEVER printed) nvault ls --scope dev # inject the merged secret+param env into a process nvault run --scope dev -- pnpm dev # or materialize a dotenv nvault env --format dotenv > .env.local
# 1. create this machine's identity (private key → Keychain) nvault key init --label laptop # 2. register teammates so secrets get sealed to them too nvault key add-recipient bob nvpub_… # 3. authenticate to the cloud tier (token → Keychain) printf '%s' "$NVAULT_TOKEN" | nvault login --url https://…convex.site --org org_… --env env_… --stdin # 4. push (encrypt locally, upload ciphertext) / pull (download, decrypt locally) nvault push --scope prod nvault pull --scope prod
ls, and the recommendation is always
--stdin over --value so keys don't land in shell history.| Concept | Meaning |
|---|---|
scope | Namespace within a store — default global, or a slug like prod, scratchpad. |
secret | Opaque credential in the OS Keychain. Never typed, never listed with its value. |
param | Typed config value (string/int/bool/json), file-backed, version-counted. |
env | The merged map a process gets: every param, then every secret (secrets win on collision). |
recipient | A labeled X25519 public key (nvpub_…) a secret can be sealed to. |
In the cloud tier the hierarchy is org → project → environment → item, with immutable
itemVersions history, hashed apiKeys, and an append-only auditEvents log.
The trust model is hybrid: the local tier uses the OS Keychain unchanged; the remote
tier is end-to-end encrypted with an age-style scheme. The server stores only opaque envelopes
(nvault.enc.v1) and has no key material.
org/env/scope/key) is bound in as additional authenticated data, so an
envelope can't be silently relocated.internal/vault/crypto) and the TS implementation
(@nvault/client, libsodium + @noble) produce the same wire format, so a secret sealed in the
browser opens in the CLI and vice-versa.The multi-tenant backend lives in apps/nvault-cloud. Every query and mutation funnels
through one RBAC chokepoint (reader < member < admin < owner) and one audit chokepoint.
The CLI talks to two token-authenticated HTTP endpoints:
GET /sync/pull?org=<id>&env=<id> → { items: [{ key, kind, ciphertext, version }] } POST /sync/push { org, env, items } → { results: [{ key, version }] }
Both authenticate with Authorization: Bearer nvk_… service tokens (only the SHA-256 hash is
stored). Payloads are ciphertext end-to-end.
import "nicos.tools/nicos-dev/sdk/nvault" c, _ := nvault.Open(nvault.Config{}) _ = c.SetSecret("dev", "OPENAI_API_KEY", key) env, _ := c.Env("dev") // merged secrets+params _, _ = c.Push("dev") // encrypt + upload (after Login)
import { NvaultClient, generateIdentity, sealEnvelope } from "@nvault/client"; const me = await generateIdentity(); const client = new NvaultClient({ baseUrl, org, env, token }); await client.push([{ key: "DB_URL", kind: "secret", ciphertext: JSON.stringify(env) }]);
The TS types are generated from the Go source by nvault-schemagen with a
--check drift gate, so the two languages can never silently diverge.
| Command | Does |
|---|---|
set KEY [--value V|--stdin] [--param] [--type T] [--scope S] | Store/update a secret (default) or typed param |
get KEY [--param] [--scope S] | Print one value |
ls [--scope S] | List items — secret values redacted |
rm KEY [--param] [--scope S] | Delete one item |
env [--format export|dotenv|json] [--scope S] | Print merged env |
run [--scope S] -- <cmd…> | Run a command with the scope's env injected |
status [--scope S] | Counts, scope, remote |
doctor | Check Keychain + param store health |
key init|show|add-recipient|list-recipients|rm-recipient|recovery | Manage the E2EE identity + recipients |
encrypt [--aad X] / decrypt | Seal/open an envelope via stdin/stdout |
login / logout / push / pull / sync | Cloud tier auth + sync |
Add --json for machine-readable output on any verb. Under ndev the same surface is
ndev [--json] vault <verb>.