Secret Vault
AES-256-GCM encrypted key-value store. Secrets are encrypted at rest using a 32-byte master key. Plaintext is never stored in application databases. The list endpoints return key names only (no values). The only way to read decrypted values is via the internal env endpoint or when the Core delivers them to a worker at startup.
Two scopes
| Scope | Storage key | Purpose |
|---|---|---|
| App secrets | app_id = the app's ID |
Scoped to a specific application. Delivered to the backend via the onStart hook. |
| Platform secrets | app_id = _platform |
Shared across the workspace. Consumed by Integrations via platformSecret mapping. Also delivered to all backends. |
When a backend starts, the Core decrypts and delivers both platform secrets and app-specific secrets via the IPC Discover message. The worker receives them in config.credentials (a key-value object) inside the onStart lifecycle hook.
App-level secrets override platform secrets if they share the same key name.
How secrets reach your backend
Secrets are not injected as environment variables by the Core. They are delivered via IPC:
serve({}, {
onStart: (config) => {
// config.credentials contains all decrypted secrets
const stripeKey = config.credentials.STRIPE_KEY;
const slackToken = config.credentials.SLACK_TOKEN;
},
});
If you want to use process.env.STRIPE_KEY syntax in your code, set the env vars yourself from config.credentials in the onStart hook.
Master key
The master key is a 32-byte key used for AES-256-GCM encryption/decryption.
| Source | Description |
|---|---|
ROOTCX_MASTER_KEY env var |
Hex-encoded 32-byte key. Must decode to exactly 32 bytes. |
| Auto-generated | If the env var is not set, the Core generates a random key and writes it to config/master.key in the data directory. |
Each secret is encrypted with a unique 12-byte random nonce. The nonce and ciphertext are stored together in the database.
ROOTCX_MASTER_KEY. Auto-generated keys are lost if the container is recreated without persistent storage, making all stored secrets unrecoverable.App secrets
Scoped to a single application. Only delivered to that app's backend.
Set a secret
POST /api/v1/apps/{appId}/secrets
{ "key": "STRIPE_KEY", "value": "sk_test_..." }
Idempotent: if the key already exists, the value is overwritten (re-encrypted with a new nonce).
List keys
GET /api/v1/apps/{appId}/secrets
Returns an array of key names only. Plaintext values are never returned via the API.
["STRIPE_KEY", "SENDGRID_API_KEY"]
Delete a secret
DELETE /api/v1/apps/{appId}/secrets/{key}
Returns 404 if the key does not exist.
Platform secrets
Shared across the workspace. Stored under the _platform scope. Consumed by integrations and delivered to all backends.
Set a platform secret
POST /api/v1/platform/secrets
{ "key": "SLACK_TOKEN", "value": "xoxb-..." }
Key name validation (platform secrets only): must be non-empty, alphanumeric + underscore only. App secrets have no key name restriction.
System users (agents) cannot manage platform secrets directly.
When a platform secret is set or deleted, all running workers are automatically restarted to pick up the new values.
List platform secrets
GET /api/v1/platform/secrets
Returns key names only.
Delete a platform secret
DELETE /api/v1/platform/secrets/{key}
Returns 404 if the key does not exist. Triggers worker restart.
Get platform env (decrypted)
GET /api/v1/platform/secrets/env
Returns all platform secrets as a decrypted key-value object. Used internally by the dashboard for configuration UIs. Requires authentication.
Integration credentials
Integrations that declare platformSecret in their configSchema store credentials in the platform vault:
"configSchema": {
"properties": {
"botToken": {
"type": "string",
"platformSecret": "SLACK_BOT_TOKEN"
}
}
}
When an admin saves the integration config (PUT /api/v1/integrations/{id}/config), the Core extracts fields with platformSecret annotations and stores them as platform secrets. The integration backend receives them in the config parameter on every action call.
Per-user OAuth credentials (for integrations with userAuth: "redirect") are stored separately under the integration's scope, keyed by connection ID. See Build an Integration for details.
Storage details
| Property | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key length | 32 bytes (256 bits) |
| Nonce length | 12 bytes (96 bits), random per secret |
| Database table | rootcx_system.secrets |
| Table schema | (app_id TEXT, key_name TEXT, nonce BYTEA, ciphertext BYTEA) |
| Primary key | (app_id, key_name) |
| Set behavior | INSERT ... ON CONFLICT DO UPDATE (idempotent) |
API endpoints summary
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/apps/{appId}/secrets |
Yes | Set an app secret |
| GET | /api/v1/apps/{appId}/secrets |
Yes | List app secret key names |
| DELETE | /api/v1/apps/{appId}/secrets/{key} |
Yes | Delete an app secret |
| POST | /api/v1/platform/secrets |
Yes (non-system) | Set a platform secret (restarts workers) |
| GET | /api/v1/platform/secrets |
Yes (non-system) | List platform secret key names |
| DELETE | /api/v1/platform/secrets/{key} |
Yes (non-system) | Delete a platform secret (restarts workers) |
| GET | /api/v1/platform/secrets/env |
Yes | Get decrypted platform env |