Skip to main content
Version: 0.3.36

Secrets

API keys, OAuth tokens and passwords should not sit in content. RaisinDB keeps them in an encrypted secret store and leaves a reference in the node, so a credential can be part of a node's schema without appearing in a query result, an API response, an event or a log.

Declare an encrypted field​

Mark the property in the node type definition:

properties:
- name: api_key
type: String
encrypted: true

That is the whole opt-in. The same flag exists on element-type fields. (The older spelling meta: { secret: true } is still read.) There is no SQL DDL modifier for it, so encrypted fields are declared in YAML or through the node-type management API.

Write plaintext, store a reference​

Your application writes the value the normal way, through REST, SQL or the WebSocket API:

INSERT INTO 'crm' (path, node_type, name, properties)
VALUES ('/acme', 'docs:Connection', 'acme',
'{"label":"Acme","api_key":"sk-live-abc123"}'::jsonb);

Before the node is indexed or stored, the server moves the value into the secret store and replaces the property with a reference:

SELECT properties FROM 'crm';
-- {"label":"Acme","api_key":"secret://node/67b793df-f435-46f0-b4f4-44cccba20952/api_key@1"}

The reference is secret://node/{node id}/{field}@{version}. Every read returns this reference, whether it comes from SELECT *, a REST read, a webhook payload, the audit log or a replication message. Because the value is vaulted before indexing, a query like properties->>'api_key' = 'guess' cannot be used to probe it.

Writing a reference back is a no-op, so a client that reads a node and saves it again does not create a new version. Writing a new plaintext value creates a new version and updates the pin. An encrypted field that holds something other than a string (or null) is rejected rather than stored.

Use it where it matters​

Server-side function code resolves a reference at the moment it needs the value. A function must declare which secrets it may read; without a policy, every read is denied.

# .node.yaml of the function
secret_policy:
enabled: true
allowed_names: ["node/*"]
const rows = await raisin.sql.query("SELECT properties FROM 'crm' WHERE path = '/acme'");
const key = raisin.secrets.get(rows.rows[0].properties.api_key);
// "sk-live-abc123"

allowed_names entries are globs where * also matches /, so node/* covers every node-owned secret and stripe/* covers stripe/api_key. raisin.secrets.get accepts a bare name or a full secret:// reference; raisin.secrets.resolve(value) returns the plaintext when value is a reference and the value unchanged otherwise. A denied read fails with [secrets:policy_denied].

Named secrets​

A credential that is not tied to one node, such as an email provider key or a webhook signing secret, is stored under a name you choose. Names may contain /.

raisindb secret set stripe/api_key -r myrepo          # value read from stdin
raisindb secret list -r myrepo
raisindb secret show stripe/api_key -r myrepo # metadata and version history
raisindb secret rotate stripe/api_key -r myrepo # appends a new version
raisindb secret rm stripe/api_key -r myrepo --yes # writes a tombstone

The same operations exist over HTTP (admin principal required):

PUT    /api/secrets/{repo}/{branch}/{name}          {"value": "..."}
POST /api/secrets/{repo}/{branch}/rotate/{name} {"value": "..."}
GET /api/secrets/{repo}/{branch}
GET /api/secrets/{repo}/{branch}/{name}
DELETE /api/secrets/{repo}/{branch}/{name}

A write returns the name, the new version and an unpinned reference:

{"name":"stripe/api_key","version":2,"reference":"secret://stripe/api_key"}

No route, SQL function or CLI command returns a plaintext value. Listing and show return metadata only: version, key id, timestamps, author and, for node-owned secrets, owner_node and owner_field.

Versions and rotation​

Every write appends a version. A reference without @version resolves to the newest version at read time; secret://stripe/api_key@1 stays pinned to version 1. Node fields are always written pinned, so reading an older node revision gives you the credential that revision held.

Rotation is therefore an append: set the new value, let consumers pick it up, and anything pinned to the old version keeps working until it is rewritten.

Branches​

The secret store is branch-scoped. Forking a branch copies its secrets, so a feature branch can hold test credentials while main keeps the real ones. Copying nodes between branches carries the secret versions those nodes reference along with them.

Encryption and keys​

Values are sealed with AES-256-GCM under a master keyring that lives outside the database, in the server's environment:

RAISIN_MASTER_KEYS="1:<64 hex chars>,2:<64 hex chars>"
RAISIN_MASTER_KEY_ACTIVE=2 # the key id that seals new writes
RAISIN_CRYPTO_EMIT_V2=1 # required for the node secret store

RAISIN_MASTER_KEY (a single 64-hex key) is still accepted and is loaded as key id 0. With more than one key listed, RAISIN_MASTER_KEY_ACTIVE is required. A server with no key configured fails at startup rather than falling back to a built-in key.

The v2 envelope records which key sealed each value and binds the ciphertext to its tenant, repository and field, so bytes copied out of one place do not decrypt somewhere else. Older v1 blobs are always readable, so the setting can be turned on at any time; on a cluster, enable it once every node runs a version that reads v2. Node-owned secrets are only ever written in v2 format, which is why RAISIN_CRYPTO_EMIT_V2 must be set before encrypted fields are used. When a value was sealed with a key the keyring no longer holds, the error names the missing id and the ids available.

The [secrets] section of the server config has one switch, vaulting_enabled (default true). Turning it off stores declared-encrypted fields as plaintext.

In short​

  • One schema flag turns a property into a managed secret
  • Reads return secret://node/{id}/{field}@{version}, never the value
  • Functions read secrets only through an explicit secret_policy
  • Every write is a new version, so rotation and history come for free
  • Keys live in the environment, not in the data directory