Provisions
Advanced — durability, Vault, Channel, plugins.
Advanced — durability, Vault, Channel, plugins.
Scaffold:
bunx create-oke@latest my-provisions --from-example provisionsthenoke dev(app:6530, Console:6533).
3 · ADVANCED — Provisions
A subscription store: orders, payments, notifications.
New ideas: durable flows and the journal · vault · channel with fallback chains and i18n · live queries · plugins · a CDC trigger · the three cache tiers.
provisions/
├── oke.config.ts
├── src/
│ ├── app.ts
│ ├── core.ts # the primary database
│ ├── gates.ts # shared gates
│ ├── vault.ts # every secret contract, one auditable file
│ ├── channels.ts # how we reach humans
│ ├── locales/{en,ar}.ts
│ ├── plugins/audit.ts
│ ├── schema.ts
│ └── flows/
│ ├── orders/{index.ts,shapes.ts,signals.ts}
│ ├── payments/{index.ts,shapes.ts}
│ └── notifications/index.ts
└── tests/orders.test.tsThe layout rule from here on: whoever produces an element declares it; consumers import it. Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
examples/provisions/src/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const products = sqliteTable("products", {
sku: text("sku").primaryKey(),
name: text("name").notNull(),
stock: integer("stock").notNull().default(0),
});
export const orders = sqliteTable("orders", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
sku: text("sku").notNull(),
qty: integer("qty").notNull(),
status: text("status").notNull().default("pending"),
createdAt: integer("created_at").notNull(),
});examples/provisions/src/vault.ts
import { vault } from "okengine";
import { z } from "zod";
// A declaration is a CONTRACT, not a value.
// Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
export const stripeKey = vault.secret("STRIPE_KEY", {
schema: z.string().startsWith("sk_"),
description: "Payments gateway key",
rotate: "90d",
dev: "sk_test_local",
});
export const dbUrl = vault.secret("DATABASE_URL", {
schema: z.string().url(),
dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
});Missing or invalid at boot, oke doctor lists all of them at once with their descriptions — before a single request is served. Values are never readable from the Console; only fingerprints are shown.
examples/provisions/src/channels.ts
import { channel } from "okengine";
import { z } from "zod";
export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
export const sms = channel.sms({ sender: "PROVISIONS" });
export const wa = channel.whatsapp();
export const orderConfirmed = mail.template("order-confirmed", {
schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
});
export const otpCode = channel.template("otp-code", { // medium-agnostic
schema: z.object({ code: z.string() }),
});Recipient address, language and opt-out consent are resolved from the user automatically. In development the console driver puts every medium into a built-in inbox instead of sending.
examples/provisions/src/flows/orders/index.ts
import { on, flow, gate, http } from "okengine";
import { eq } from "drizzle-orm";
import { db } from "../../core";
import { member } from "../../gates";
import { orderPlaced, orderNews } from "./signals";
import { chargeOrder } from "../payments";
import { NewOrder, OrderId, OrderRow, OutOfStock } from "./shapes";
import { orders, products } from "../../schema";
const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
export const create = on(http.post("/orders").gate(member, canOrder), flow({
in: NewOrder, out: OrderId, errors: { OutOfStock },
do: async (input, fx) => {
const [product] = await fx.store(db).select({ stock: products.stock })
.from(products).where(eq(products.sku, input.sku)).limit(1);
if (!product || product.stock < input.qty) return fx.fail("OutOfStock",
{ left: product?.stock ?? 0 },
{ message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) });
const id = fx.id();
await fx.store(db).insert(orders).values(
{ id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
await fx.emit(orderPlaced, { orderId: id });
return { id };
},
}));
// LIVE QUERY — realtime and auto-caching from one flag
export const mine = on(http.get("/orders").gate(member).live(), flow({
out: OrderRow.array(),
do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
}));
export const getOrder = flow({
in: OrderId, out: OrderRow,
do: async ({ id }, fx) => {
const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
return order;
},
});
// SIGNAL consumer
on(orderPlaced, flow({
do: async ({ orderId }, fx) => {
const paid = await fx.call(chargeOrder, { orderId });
await fx.store(db).update(orders).set({ status: paid ? "confirmed" : "failed" })
.where(eq(orders.id, orderId));
await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
},
}));
// CHANGE trigger — CDC, built in
on(db.table(orders).changed("status"), flow({
do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
}));.live() is the whole of realtime. The result is cached, invalidated by exactly the writes that touch those rows, and pushed to subscribed clients on exactly those writes. No cache code, no socket code.
src/flows/payments/index.ts — durability is a flag
import { flow } from "okengine";
import { z } from "zod";
import { stripeKey } from "../../vault";
import { OrderRef } from "./shapes";
export const chargeOrder = flow({
durable: true, // every fx call below is journaled
in: OrderRef, out: z.boolean(),
do: async ({ orderId }, fx) => {
const intent = await fx.step("create-intent", () => // never re-runs on replay
stripe(fx.vault(stripeKey)).create(orderId));
await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
},
});Workflows are not a separate API. They are ordinary flows with one option. A process killed between the two steps resumes at confirm — the card is not charged twice.
src/flows/notifications/index.ts — reaching humans
import { on, flow } from "okengine";
import { z } from "zod";
import { orderNews } from "../orders/signals";
import { getOrder } from "../orders";
import { orderConfirmed, otpCode, wa, sms } from "../../channels";
on(orderNews, flow({
do: async ({ orderId, status }, fx) => {
if (status !== "confirmed") return;
const o = await fx.call(getOrder, { id: orderId });
await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
},
}));
export const sendOtp = flow({
in: z.object({ userId: z.string(), code: z.string() }),
do: ({ userId, code }, fx) => fx.send(otpCode, { to: userId, via: [wa, sms], data: { code } }),
// ↑ fallback chain: WhatsApp, else SMS
});Fallback is recorded as a chain, not an outcome — so the Console can tell you that 23% of OTPs fell back to SMS this week and what that cost.
src/plugins/audit.ts — every extension point in one file
import { plugin, store } from "okengine";
import { z } from "zod";
export const audit = plugin("audit", { version: "1.0.0" })
.config(z.object({ retain: z.string().default("2y") }))
.element(store.sql("audit", { schema: () => import("./audit-schema") }))
.needs("store.kv")
.decorate("audit", { enabled: true })
.hook("afterHandle", async (ctx, fx) => {
if (ctx.trigger.meta?.audit) await fx.store("audit").log(ctx);
})
.errors({ AuditWriteFailed: z.object({ reason: z.string() }) })
.consolePanel({ id: "audit", title: "Audit Trail", entry: "./panel.tsx" })
.cli("audit:export", ({ fx }) => fx.store("audit").exportCsv());src/app.ts — scope is the attachment point
import { oke } from "okengine";
import { auth } from "okengine/auth";
import { audit } from "./plugins/audit";
import * as orders from "./flows/orders";
import * as payments from "./flows/payments";
import * as notifications from "./flows/notifications";
export const app = oke({ name: "provisions" })
.adopt({ orders, payments, notifications })
.plug(auth()) // zero ceremony: uses your configured store
.plug(audit) // app-wide
.hook("onError", (ctx, err, fx) => fx.log.error(err));
app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
export type App = typeof app;app.plug() is app-wide, app.unit(name).plug() covers one unit, flow.plug() covers one flow. The position is the scope — no global: true, no inheritance rule to remember. .adopt() is what makes typeof app carry every flow's contract for the client; on() inside each flow file still does the actual trigger registration.
Auth needs no adapter. The framework already knows your store; its tables come from oke schema generate. Options exist when you want them, and the identity provider is a seam — auth({ provider: betterAuth(...) }), clerk(), supabase(), auth0(), kinde() all normalise to the same fx.auth, so gates, ABAC, rate limits and channel recipients keep working unchanged when you switch.
Cache — three visible tiers
// Tier 1 — automatic for live and read flows; invalidation computed from effects
// Tier 2 — a flag on any flow
export const popular = on(http.get("/popular"), flow({ cache: "5m", do: /* … */ }));
// Tier 3 — manual
const rate = await fx.cache.getOrSet("fx-rate:USD-SAR", "1h", fetchRate);i18n
// src/locales/ar.ts
export default { "order.outOfStock": "لم يتبقَّ سوى {left} قطع" };Typed keys — a missing key is a compile error. Locale resolves per request: user profile → Accept-Language → configured default. Errors and channel templates are localised, and the dir flag reaches the client so the frontend gets RTL for free.
examples/provisions/tests/orders.test.ts
const t = await createTestApp(app); // memory drivers, frozen clock
const u = await t.auth.loginAs({ scopes: ["order:create"] });
const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
await t.signals.drain();
await t.clock.advance("2m"); // the durable sleep elapses instantly
await t.signals.drain();
expect(t.channels.sent()).toContainEqual(
expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));What you have
All ten exports and seven of the eight elements. Durable execution, human-facing delivery, realtime, plugins, i18n, and secrets with boot-time validation.
What is missing
The system serves one customer, treats every user the same, and has no way to state what "working" means. It also cannot reason about anything.