Secret Vault
ddcore provides a dedicated, encrypted vault for credentials and secrets.
There are two kinds of secrets in an application:
- Site-level credentials (e.g. Stripe webhook secret, SMTP password): fixed per deployment, configured via environment variables and accessed with
ddcore.secret("stripe_key")(readsDDCORE_SECRET_STRIPE_KEY). - Per-record dynamic credentials (e.g. multi-tenant API tokens, customer credentials, OAuth refresh tokens): stored encrypted in the vault and linked to documents via the
Vaultfieldtype or managed directly viaddcore.vault.*.
1. Master Encryption Key
The vault uses AES-256-GCM authenticated encryption. The encryption key is derived from the DDCORE_SECRET_KEY environment variable by trimming it and hashing it with SHA-256 — any non-empty string works, hex is not decoded, and there is no length requirement:
export DDCORE_SECRET_KEY="a long, random value provisioned once per deployment"If DDCORE_SECRET_KEY is not set, vault operations fail immediately with a configuration error, preventing plaintext storage.
ddcore.secret("key") cannot read this variable: Engine.Secret refuses any name that maps to DDCORE_SECRET_KEY, so the master key is unreachable from app code even though it shares the same environment prefix as an app secret.
2. Server-side API (ddcore.vault)
Server-side controllers, services, and background jobs have direct synchronous access to the vault:
// Store or update a secret
ddcore.vault.set("asaas:token:ACC-00042", "sk_live_abcdef123456");
// Retrieve and decrypt a secret (returns string | null)
const token = ddcore.vault.get("asaas:token:ACC-00042");
if (!token) {
ddcore.throw(_("Integration token not configured"));
}
// Delete a secret
ddcore.vault.del("asaas:token:ACC-00042");
// List secret names matching a prefix (never returns values)
const keys = ddcore.vault.list("asaas:token:"); // ["asaas:token:ACC-00042", ...]3. Fieldtype Vault
DocTypes can declare fields with fieldtype: "Vault":
import { defineDoctype } from "@ddcore/sdk";
export default defineDoctype({
name: "Integration Account",
fields: [
{ fieldname: "account_name", fieldtype: "Data", label: "Account Name", reqd: true },
{ fieldname: "api_key", fieldtype: "Vault", label: "API Key", reqd: true },
// Custom key naming using doc fields interpolation:
{ fieldname: "custom_token", fieldtype: "Vault", label: "Custom Token", options: "custom:token:{name}" },
],
});Key properties of Vault fields:
- Virtual field (no column):
Vaultfields never generate a column intab_<doctype>. They are persisted exclusively in the encryptedddcore_vaulttable. - Key derivation:
- Default:
${DocType}:${fieldname}:${doc.name}(e.g.Integration Account:api_key:ACC-00001). Renaming the document re-keys this shape automatically. - Custom template in
options: interpolates document fields using{field}placeholders (e.g.options: "custom:token:{name}"). A rename does not re-key a custom template — see Limitations.
- Default:
- Lifecycle on save: a
Vaultfield accepts one of a few shapes:- A non-empty string encrypts and saves it under the derived key.
- An empty string,
null,{ configured: true }(what a read gives back), or the field simply not appearing leaves the current vault secret untouched. { value: "…" }or{ secret: "…" }sets it, same as a plain string.{ clear: true }removes it from the vault.- On document
delete, associated vault entries are cleaned up automatically.
- Border security:
- Reading documents via REST API, Desk, or MCP never returns the plaintext secret. The field is redacted to
{ "configured": true }if present, ornullif not configured. - Excluded from
Versiondiffs (audit history diffs will never leak secrets). - Excluded from data exports.
- Reading documents via REST API, Desk, or MCP never returns the plaintext secret. The field is redacted to
4. Desk UI Control
The Desk provides a dedicated control for Vault fields:
- When configured: shows a green
Configuredbadge with Change (to enter a new secret) and Clear buttons. - When entering a new secret: displays a masked password input.
- When marked for clearing: displays an indicator with an Undo button.
5. Audit Logging
ddcore.vault.set, ddcore.vault.get, and ddcore.vault.del are audited in the Audit Event DocType (PRD-06):
action:vault.write,vault.read, orvault.delete.target_doctype:Vault Secret.target_name: Name of the vault key.actor: User email who triggered the action (orSystem).outcome:Allowed(orDenied).ip: Client IP address.request_id: Request correlation ID.
Two paths are not audited:
ddcore.vault.listnever touches the audit log — it never returns a value, only key names.- Framework code reading a key it owns on its own schedule, such as signing each webhook delivery attempt, reads the vault without recording an entry. An audit row per retry would bury the reads a person made under the ones a worker made. Anything an app or a person asks for through
ddcore.vault.getstill goes through the audited path.
Writing the audit entry itself is best-effort: if it fails, the vault operation still succeeds and the failure is silently discarded.
System Managers can review audit events in the Desk at /app/Audit Event or via ddcore audit list.
6. Health & Diagnostics (ddcore doctor)
Run ddcore doctor to inspect the vault status:
ddcore doctorThe report indicates whether DDCORE_SECRET_KEY is configured, the total count of stored secrets, and their key names — values are never displayed, but the names themselves are printed, and a key can leak a detail (a document name, a tenant) worth keeping out of a report pasted into an issue.
7. Limitations
- No key rotation. A different
DDCORE_SECRET_KEYmakes every existing secret fail to decrypt; there is no re-encryption path. Doctor only warns when the key is missing, not when it has changed. - Backups are useless alone. A database backup carries the ciphertext but not the key; restoring it without the same
DDCORE_SECRET_KEY, provisioned separately, leaves every secret undecryptable. - No permission check inside
ddcore.vault.*. Server code — a controller, a service, a job — is trusted with any key it names; the boundary is that this API only exists on the server, never in desk-sdk. ddcore doctorprints vault key names, not values (see above).- Some renames still orphan a secret. A default-shaped key (
<DocType>:<fieldname>:<name>) is re-keyed on rename. A custom key template inoptions, and removing a child row during an update, are not: the secret stays inddcore_vaultunder a key nothing reads anymore.