Every env var is a secret
There's no separate secrets vault sitting alongside an app's environment — the environment is the secret store. Set DATABASE_URL or an API key the same way, and both get the same envelope-encrypted, write-only treatment.
Envelope encryption
Each value is encrypted under its own random data key (AES-256-GCM), and that data key is in turn wrapped under a versioned master key. Rotating the master key means re-wrapping those small per-value keys — the encrypted values themselves never move. That's a cheap, fleet-wide key rotation instead of a bulk re-encryption pass over every secret you've ever set.
Opaque, never hand-formatted
Values are stored and passed around as opaque encrypted blobs and decoded exactly once, at the point they're actually used — never spliced together as text into a .env-style line. That rule exists because hand-formatting secret values into dotenv syntax is exactly how a PEM key with embedded \n characters gets silently corrupted; Sproobo never does that construction at all.
Write-only, with one deliberate exception
The CLI and an MCP-native client (an AI agent, an editor connector) only ever see names: sproobo secrets ls and the equivalent MCP tool list keys, never values, and both ride the same audited API a human uses.
The one exception is the dashboard's app editor itself: because editing an existing value means showing you what's already there, a teammate with edit access (MEMBER role or above) sees each value masked behind a reveal toggle in their own browser. A teammate with only VIEWER access still sees names alone — never a value, revealed or otherwise.
Setting secrets
Three equivalent paths, all backed by the same write:
- Dashboard — an app's Settings tab has a key/value editor: add a row, edit one in place, or remove one, then save.
- CLI — one at a time, or a bulk import as a single atomic write:
sproobo secrets set <appId> DATABASE_URL --value postgres://... -y sproobo secrets import <appId> .env -y sproobo secrets ls <appId>
secrets import reads a .env file (or stdin) and replaces the app's environment in one atomic write — the shape you want when you're bringing over an existing .env you already track outside Sproobo, rather than re-typing every key with secrets set.
MCP — an MCP-native client calls the equivalent set/import/delete tools over the same audited API, gated by the same org role.
Every one of these is plan-first: without -y (CLI) or an explicit approval flag (MCP), it prints a plan and changes nothing. sproobo secrets rm <appId> KEY is destructive — it deletes a value outright — so confirm with the human before approving it.
Export & import in the dashboard
The Settings-tab editor also has Export .env and Import .env buttons. Export downloads the app's current rows (including any edits you haven't saved yet) as a real .env file, and is recorded as an audit event so an export is never a silent read. Import lets you paste or pick a .env file and previews every parsed key as added, updated, or unchanged against your current rows before you touch anything; Apply merges those changes into the editor — keys the file doesn't mention are left alone — and nothing is actually written until you save the form, so an import rides the exact same audited path as a manual edit.
Setting a secret doesn't restart anything by itself. The new value takes effect the next time the app deploys — the same env map is also what reaches the build (a Dockerfile build gets it as a --build-arg; a Railpack build gets it as a BuildKit secret mount instead) and the running container. See Apps & deploys for exactly how that build-time vs. runtime split works, and why it matters for which values are actually secret-safe.
Next
- See how build-time vs. runtime env is delivered by build strategy on Apps & deploys.
- A bound database or cache injects its own connection string the same way — see Databases & services.
- Read the full trust-boundary and encryption model on Security model.