ElementsVault

Secrets

Typed vault.secret contracts, boot-time resolution, Redacted reads through fx.vault.get, and per-tenant paths.

Secret contracts (vault.secret) declare the credentials your backend requires — Stripe keys, webhook HMACs, SMTP URLs. Values resolve at boot; Flows read them as Redacted through fx.vault.get.

For developers who must never leak credentials into logs or HTTP bodies — declare the name, resolve it, reveal only at the provider edge.

The one rule

Touch secrets only through fx.vault.get(contract). That records the secret effect and returns Redacted.reveal() belongs at the SDK / HMAC boundary, not in return values.

Smallest Example

Declare the contract

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",
  dev: "sk_test_local",
});

Import this module before oke() so auto-registry can adopt the contract (or pass it in oke({ secrets: […] })).

Read inside do

src/flows/payments/charge.ts
import { on, flow, http } from "okengine";
import { z } from "zod";
import { member } from "@/core/gate";
import { stripeKey } from "@/core/vault";

export const charge = on(
  http
    .post({
      in: z.object({ amount: z.number().int().positive() }),
    })
    .gate(member),
  flow({
    do: async ({ amount }, fx) => {
      const key = await fx.vault.get(stripeKey);
      const stripe = new Stripe(key.reveal());
      const intent = await stripe.paymentIntents.create({ amount, currency: "usd" });
      return { id: intent.id };
    },
  }),
);

Confirm the effect

Manifest / Console list secrets: ["STRIPE_KEY"] on payments.charge. Logs that stringify key show [redacted].

Progressive Patterns

From a bare name to schema, cadence, tenancy, and sensitivity overrides:

Name only — description helps boot gaps and Console:

export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
  description: "HMAC secret for outbound webhooks",
});

Options Reference

Third argument shape is VaultSecretOptions on vault.secret(name, options):

OptionTypeDefaultMeaning
descriptionstringBoot-gap and Console label
rotatestringomit ≈ "never"Cadence hint ("90d") for Console posture — not automatic rotation
schemaSchemaDeclared validator for docs / tooling
devstringDev-only fallback (vault.fromDocker(role) allowed)
sensitivebooleantrueFingerprinted; Console never shows cleartext
perTenantbooleantrue when tenancy onStorage path {tenantId}/{name}; skipped in boot-gap scan when true

Empty name throws TypeError: vault.secret: name is required.

Reading secrets

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.

CallReturnsNotes
await fx.vault.get(handle)Redacted<string>Preferred — capability from the handle
await fx.vault.get("NAME")Redacted<string>Same when the name is declared
key.reveal()stringOne explicit cleartext escape
key.map(fn)Redacted<U>Transform without exposing to callers
String(key) / toJSON"[redacted]"Safe for accidental serialization
const key = await fx.vault.get(stripeKey);

fx.log.info(`using ${key}`); // message contains [redacted]
// JSON.stringify({ key }) → { "key": "[redacted]" }

const client = new Stripe(key.reveal());

Secret access is never journaled — durable replay re-reads the live value so a rotated credential is not resurrected from the journal.

Effects & Manifest

Calling fx.vault.get records effects.secrets with the contract name. Declare the same list explicitly when you want Manifest truth without inference:

flow("payments.charge", {
  effects: { secrets: ["STRIPE_KEY"] },
  do: async (_, fx) => {
    await fx.vault.get(stripeKey);
  },
});

Capability enforcement: reading a name not allowed by the Flow’s effects fails the secret capability check.

Boot gaps

Missing non-tenant contracts fail boot with every hole listed once:

vault boot failed — 2 missing secret(s):
  - STRIPE_KEY: Stripe secret API key
  - APP_WEBHOOK_SECRET: HMAC secret for outbound webhooks

Fill gaps with:

LayerHow
Driveroke vault set NAME, managed provider write, memory seed
process.envExport NAME=… in the host / CI
.env.localLocal override file (gitignored)
dev:Contract fallback — only when dev fallbacks are allowed

On a TTY, oke dev prompts for each gap before the app starts and writes values into .env.local (same store as oke vault set). Non-interactive runs still fail with VaultBootError listing every hole.

Troubleshooting

Learn more

  • Vault overview — resolution chain and drivers
  • Config — cleartext contracts vs secrets
  • Key Rotation — version and master-key rotate
  • Tenancyfx.tenant.id for per-tenant paths
  • fx — full fx.vault table

Next

On this page