Elements

Vault

Secrets, API keys, and config — declared once as typed contracts, resolved from a real vault in production, never leaked into logs.

Vault is where your app's protected knowledge lives: API keys, database URLs, tokens — plus non-secret config like a public app URL. This page shows how to declare a secret, give it a value, and read it in a Flow, then how OKE keeps it safe in production.

The one rule

You declare contracts, never values, in code. Values come from the environment at boot — a secret that has no value anywhere fails the boot loudly, with every gap listed at once.

Quick start

Declare the contract

In src/vault.ts, declare what your app needs. This says "a secret named STRIPE_KEY exists" — it does not contain the key itself:

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

export const stripeKey = vault.secret("STRIPE_KEY", {
  description: "Payments gateway key",
  rotate: "90d",
  schema: z.string().startsWith("sk_"),
  dev: "sk_test_local", // used only by `oke dev` (local) when nothing else provides a value
});

Give it a value

For local development, put the real value in .env.local (gitignored):

.env.local
STRIPE_KEY=sk_test_your_key_here

Or set it from the CLI — it writes to the right place for the active mode:

oke vault set STRIPE_KEY sk_test_your_key_here

Read it in a Flow

Flows read secrets through fx.vault — never through process.env:

src/flows/billing/charge.ts
export const charge = on(
  http.post("/charge"),
  flow({
    in: ChargeInput,
    out: z.object({ id: z.string() }),
    do: async (input, fx) => {
      const key = fx.vault(stripeKey); // cleartext, inside this flow only
      const intent = await stripe(key).create(input);
      return { id: intent.id };
    },
  }),
);

That's the whole loop: declare → set → read. Everything below is what OKE guarantees on top of it.

Two kinds of contracts

DeclarationSensitive?Console showsUse for
vault.secret("STRIPE_KEY", …)yesfingerprint onlyAPI keys, tokens, passwords
vault.config("PUBLIC_APP_URL", …)nocleartext valuePublic URLs, feature flags

vault(name, …) is shorthand for vault.secret(name, …).

Contract options

OptionTypeMeaning
descriptionstringShown in boot-gap listings and the Console
rotatestringRotation hint ("90d") — drives the rotation-due signal in the Console
schemazod / Standard SchemaValidated at boot; a bad value fails boot like a missing one
devstringLocal-only fallback when no source provides a value (never used in prod)
sensitivebooleanOverride the default (true for secrets, false for config)

Zero-setup local fallback

dev: vault.fromDocker("store.sql") resolves the connection URL that oke dev --docker generated for that role — no manual copying of Postgres URLs:

export const dbUrl = vault.secret("DATABASE_URL", {
  dev: vault.fromDocker("store.sql"),
});

Where values come from

At boot, each contract is resolved through this chain — first hit wins:

#SourceTypical content
1process.envReal environment (CI, hosting platform)
2.env.localYour machine's local overrides (gitignored)
3docker/.env.dockerGenerated compose stack credentials
4vault driverOpenBao in docker/prod mode
5dev fallbackThe dev: option on the contract (never in prod)

If every layer misses a contract, boot fails before any request is served:

VaultBootError: 1 secret(s) missing:
  STRIPE_KEY — Payments gateway key

Good to know

This is the fail-loud contract: an app with a missing secret never boots halfway and fails later at request time. All gaps are listed in one error, not one at a time.

Setting and rotating values

CommandWhat it does
oke vault set NAME valueWrite a value (OpenBao API in docker mode, .env.local locally)
oke vault listList names present in the active store
oke vault import <file>Bulk-import names from a dotenv file

In docker mode, oke vault set writes straight to the running OpenBao — no git commit, no rebuild, no restart of the vault.

The Console (:6533) can also set and rotate values, but it is write-only: it shows a salted fingerprint (sha256:…) per secret, never the cleartext. When you rotate a key there, the panel shows the blast radius — which in-flight durable runs will wake up with the new value.

Secrets never reach logs

Every loaded secret is registered with the redactor at boot. Even if you accidentally pass one to fx.log, the log line shows *** — the value, never. Traces and the Console get fingerprints.

Per-environment drivers

Which backend holds your values depends on the mode — configured once in oke.config.ts:

oke.config.ts
vault: {
  local: "dotenv",   // .env.local — zero ceremony
  docker: "openbao", // real vault in the compose stack
  test: "memory",    // seeded map — deterministic tests
  prod: "openbao",   // real vault in production
},
DriverBackendBest for
dotenv.env.localLocal loop — no infra at all
openbaoOpenBao (KV v2)Docker + prod — durable, access-controlled
memoryin-process mapTests

Other drivers (infisical, managed) implement the same VaultDriver interface — adding one is a driver exercise, not an architecture change.

OpenBao in docker and prod

When you run oke dev --docker (or bring up a --prod stack), OKE runs a real OpenBao container with durable Raft storage — not an in-memory dev server.

First boot is automatic

OKE initializes OpenBao (Shamir 1-of-1), unseals it, writes a least-privilege policy that covers only your declared secrets, and mints an app token bound to that policy. You never run raw bao commands.

Keys stay on the host

The unseal key and root token are written to .oke/openbao/ (mode 0600, gitignored) — never into docker/.env.docker, never into YAML. Only the least-privilege app token lands in .env.docker as OKE_VAULT_TOKEN.

Restarts just work

On every later start OKE detects the sealed vault and unseals it with the host key. Secrets set before a container restart are still there after it — proven by an integration test, not assumed.

FileHoldsUsed by
.oke/openbao/unseal.keyThe single unseal shareoke (every start)
.oke/openbao/root.tokenRoot tokenoke (policy sync / token mint)
.oke/openbao/app.tokenLeast-privilege app tokenthe app + Console

Single point of failure — read this once

Back up .oke/openbao/unseal.key to a separate safe location. Losing it means losing every secret permanently, with no recovery.

This is by design: self-hosted single-node, Shamir 1-of-1, no cloud KMS. If OpenBao is initialized but the key is gone, boot fails with an explicit permanent-loss error — it never silently starts empty.

Troubleshooting

Learn more

  • Console · Vault — fingerprints, resolution chain, rotation blast radius
  • Flow — how fx.vault reads secrets inside do
  • CLI Referenceoke vault set · list · import

Next

On this page