garrid · developer platform

nvault v0.1

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.

See pricing Start free (local & OSS)

Why nvault

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.

Local-first

Secrets stay in the OS Keychain; params are typed files under ~/.nicos-dev/vault. Works fully offline.

Zero-drift

ndev vault, nvault, and the existing ndev secrets all write the same Keychain item — proven by a hermetic test.

Zero-knowledge

Cloud sync uploads ciphertext sealed to each member's key. The server cannot read a single secret.

Typed & versioned

Params carry a type (string/int/bool/json) and a monotonic version; the cloud keeps full history.

Pricing

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.

Team
$17.1K/yr

Land tier — a single team, narrow scope, undercuts a standalone secrets manager.

  • Zero-knowledge cloud sync
  • Up to 10 members
  • Go + TypeScript SDKs
  • Audit log
Book a demo
Enterprise
$51.3K/yr

Expand tier — captures high-WTP accounts; approaches the WTP ceiling with self-host, compliance, and support SLAs.

  • Everything in Business
  • Self-hosted / VPC deployment
  • Compliance review + DPA
  • Priority support SLA
Talk to us
Prices are the value-based good-better-best ladder from the 2026-06-07 GTM packet (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.

Three faces, one engine

Every surface dispatches through the same internal/nvaultcli engine over internal/vault, so behavior can never diverge.

SurfaceWhat it's forExample
nvaultStandalone product binary (the SaaS brand, like nship)nvault run -- pnpm dev
ndev vaultIntegrated into the daily driverndev vault ls --scope prod
ndev secrets / configExisting surfaces, unchanged, same substratendev secrets get OPENAI_API_KEY

Quickstart

Local secrets & params

# 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

Team sync (end-to-end encrypted)

# 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
Secret values are never written by ls, and the recommendation is always --stdin over --value so keys don't land in shell history.

Data model

ConceptMeaning
scopeNamespace within a store — default global, or a slug like prod, scratchpad.
secretOpaque credential in the OS Keychain. Never typed, never listed with its value.
paramTyped config value (string/int/bool/json), file-backed, version-counted.
envThe merged map a process gets: every param, then every secret (secrets win on collision).
recipientA 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.

Zero-knowledge crypto

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.

plaintext random data key (DEK) XChaCha20-Poly1305 body+ DEK sealed to each recipient (X25519) Envelope
The Go reference (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.

Cloud tier (Convex)

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.

SDKs

Go

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)

TypeScript

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 reference

CommandDoes
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
doctorCheck Keychain + param store health
key init|show|add-recipient|list-recipients|rm-recipient|recoveryManage the E2EE identity + recipients
encrypt [--aad X] / decryptSeal/open an envelope via stdin/stdout
login / logout / push / pull / syncCloud tier auth + sync

Add --json for machine-readable output on any verb. Under ndev the same surface is ndev [--json] vault <verb>.