ElementsVault

Overview

Protected knowledge — declare secret and config contracts, resolve values at boot, read Redacted credentials through fx.vault.

Vault is how your backend holds secrets and configuration safely. Declare a contract (vault.secret, vault.config), resolve the value through the boot chain, and read it inside Flows as Redacted — never as a plain string in logs or responses.

For developers wiring Stripe keys, SMTP URLs, and feature flags on okengine — contracts first, values only at the provider edge.

The one rule

Contracts, never values. Application code declares vault.secret("STRIPE_KEY") (or vault.config). Cleartext crosses only at .reveal() on the credential boundary — never in console.log, JSON, or the HTTP envelope.

Resolution chain — first hit wins

miss every layer → VaultBootError
contractSTRIPE_KEY
  1. 1driverBuilt-in vault / managed bag
  2. 2process.envReal env (CI, hosting)
  3. 3.env.localLocal overrides (gitignored)
  4. 4dev-fallbackdev: on the contract — never in prod
  5. Resolving chain…

Smallest Example

Declare a secret contract

src/core/vault.ts
import { vault } from "okengine";

export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
  description: "HMAC secret for outbound note webhooks",
  // Dev-only fallback — never used in prod boot
  dev: "dev-webhook-secret-change-me",
});

Read it in a Flow

src/flows/notes/create.ts
import { on, flow, http } from "okengine";
import { webhookSecret } from "@/core/vault";
import { notesMutate } from "@/core/gate";

export const create = on(
  http.post().gate(notesMutate),
  flow({
    do: async (input, fx) => {
      // Touches the contract → effects.secrets; reveal only at an SDK edge
      await fx.vault.get(webhookSecret);
      return fx.json.create({ id: fx.id(), title: input.title });
    },
  }),
);

The compiler stamps secrets: ["APP_WEBHOOK_SECRET"] on the Flow’s effects.

Boot and call

# Missing value → VaultBootError lists every gap at once
oke vault set APP_WEBHOOK_SECRET

curl -X POST http://localhost:6530/notes \
  -H "accept: application/json" \
  -H "content-type: application/json" \
  -d '{"title":"hello"}'

fx.log / String(key) / JSON.stringify(key) all show [redacted] — never the HMAC material.

Progressive Patterns

From a fingerprinted secret to cleartext config, plain env helpers, and Docker-local fallbacks:

Fingerprinted contract — Console shows a fingerprint, never the value:

src/core/vault.ts
import { vault } from "okengine";
import { z } from "zod";

export const stripeKey = vault.secret("STRIPE_KEY", {
  description: "Stripe secret API key",
  schema: z.string().startsWith("sk_"),
  rotate: "90d",
  // Optional local fallback when drivers.vault.dev allows it
  dev: "sk_test_local",
});

vault("STRIPE_KEY", opts) is the same as vault.secret.

Declaration Reference

DeclarationSignaturePurpose
vault.secretvault.secret(name, options?)Fingerprinted secret contract
vaultvault(name, options?)Alias for vault.secret
vault.configvault.config(name, options?)Cleartext config contract
vault.fromDockervault.fromDocker(role)Dev fallback marker for an image role
vault.envvault.env / .required / .int / .bool / .jsonSync process env — no contract
OptionTypeDefaultMeaning
descriptionstringShown in boot-gap listings and Console
rotatestringomit ≈ "never"Cadence hint ("90d") or "never" — Console posture, not auto-rotate
schemaSchemaDeclared shape (Zod / Standard Schema) for docs and tooling
devstringDev-only fallback; never used in prod boot
sensitivebooleantrue secret · false configWhether cleartext must never leave the runtime
perTenantbooleantrue when tenancy onResolve {tenantId}/{name} at request time; not boot-gap fatal

Resolution Chain

Detailed section

If you only need “declare and fx.vault.get”, the Smallest Example is enough. This section is how boot finds a value — first hit wins.

Order (Console labels match these source ids):

  1. driver — built-in encrypted store, managed bag, or env / memory driver
  2. process.env — real environment (CI, hosting)
  3. .env.local — local overrides (gitignored)
  4. dev-fallbackdev: on the contract (dev boot only)

Miss every layer → VaultBootError listing all gaps in one pass (including vault.env.required names):

vault boot failed — 2 missing secret(s):
  - STRIPE_KEY: Payments gateway key
  - DATABASE_URL: Primary SQL URL

Consequence: fix every listed name before traffic — boot does not take a half-configured app.

See Secrets for contracts and Config for cleartext + vault.env.

Redacted until reveal

Redacted until you reveal

fx.vault.get(secret) → Redacted
get
log
json
reveal
shared beat — hold vs reveal
  • hold Redactedawait fx.vault.get(stripeKey)
    • fx.log.info(key)·
    • JSON.stringify({ key })·

    shows [redacted]

    fx.log, String(), and JSON.stringify never see the value — nested Redacted included.

  • reveal at boundarykey.reveal()
    • new Stripe(key.reveal())·
    • fx.log after reveal·

    shows sk_test_…

    One explicit call at the Stripe (or SMTP, or SDK) edge — the credential crosses only there.

fx.vault.get(contract) returns Promise<Redacted<string>>. Printing, logging, and JSON all yield [redacted]. Call .reveal() only at the Stripe / SMTP / SDK edge.

Loaded secret substrings are also scrubbed from fx.log even when you pass cleartext by mistake (mask token [redacted:secret]).

fx.vault surface

MethodNeeds backendMeaning
get(contract)NoRedacted<string> — every app has this
set(path, value, opts?)Yes (vault)New version; optional ttlMs / metadata
rotate(path, value)Yes (vault)New version under a fresh data key
delete(path)Yes (vault)Crypto-shred; returns whether anything went
list(prefix?)Yes (vault)Paths only — never values
status()Yes (vault){ sealed, initialized, backend }

Without a bound encrypted backend, mutations throw:

fx.vault.set needs a bound Vault backend — configure the vault element (drivers.vault = "vault") …

Dry-run refuses writes rather than mutate a live secret (DryRunWriteIsolationError).

Per-environment drivers

Defaults from DRIVER_DEFAULTS.vault — pin overrides in oke.config.ts:

oke.config.ts
import { defineConfig } from "okengine/config";

export default defineConfig({
  drivers: {
    // Default vault.dev is "env"; Docker-first starters pin built-in:
    vault: {
      dev: "vault",
      // test: "memory" (default)
      // prod: "vault" (default)
    },
  },
});
DriverRuns asBest for
envProcess / dotenv bagSimple local / CI without SQL vault tables
vaultBuilt-in AES-256-GCM in PostgresDocker-first apps; oke vault init / unseal
memoryIn-process mapTests
managedRemote provider bagAWS / Azure / GCP / Doppler / 1Password

create-oke Notes starters pin vault.dev: "vault" and declare stack + app contracts in src/vault.ts (vault.secret / vault.config). Console Vault lists those contracts — values still resolve from .env.local, process env, oke vault set, or dev: / vault.fromDocker fallbacks. Pass oke({ secrets: NOTES_VAULT }) so configs resolve (only vault.secret auto-registers).

Managed provider ids: aws-secrets-manager · azure-key-vault · gcp-secret-manager · doppler · 1password. Env knobs: Environment variables.

The Capabilities of Vault

Troubleshooting

Learn more

  • Secrets — options, Redacted, effects, tenancy paths
  • Config — cleartext contracts and vault.env
  • Key Rotationoke vault rotate / rotate-master
  • fxfx.vault.get · set · rotate · delete · list · status
  • ErrorsVaultBootError · VaultError · VaultSealed
  • Environment variablesOKE_VAULT_*

Next

On this page