DocsPlatformSecret Vault

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.

For production, always provide an explicit 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