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.