v0.20.0
✨ Added
Dev, Keel & create-oke
- create-oke ships a **blank** starter (
-t blank) — Docker-first empty app withmain.healthand Store/Vault wiring (no Notes domain). Scaffold rewritesoke({ name }), the welcomeapp:field, and seednameto the project name (same for Shorter). - create-oke ships a **shorter** starter (
-t shorter) — URL shortener with email/password auth, owner checks, publicGET /:code302, and a Reach click digest (clock.daily+ email).
Docs
- HTTP Response Envelopes documents returning a raw
Responseas the exception for302+Location.
💥 Breaking Changes
Dev, Keel & create-oke
- The
.adopt()barrel issrc/flows/index.ts(import "@/flows").oke dev/oke buildstill regenerate it; leftoversrc/flows/generated.tsis removed on write. Changeimport "@/flows/generated"toimport "@/flows". - create-oke no longer ships the Notes
standardandadvancedstarters. Use-t blank(default, empty app) or-t shorter(URL shortener).
♻️ Changed
Runtime
- Channel bodies live on
.template({ catalog })for every medium (email, SMS, WhatsApp, push).oke({ channel.catalog })and plugin.channelCatalog()remain overlays. OTP, magic-link, and two-factor copy ride the template declaration. - When an exposure declares
out, success values are projected onto that schema.fx.json.create(row),return row, andfx.json.withQuery(rows, input)map Date timestamps and temporal epoch-ms (*At/*_at/at, e.g.fx.clock.now()) to ISO-8601 and strip extra columns — no DTO mapper. - SQL insert/update/upsert **and WHERE binds** coerce parseable ISO datetime strings and epoch-ms (
fx.clock.now()) toDatefortimestamp/datecolumns, so HTTPinfields likeexpiresAt: z.iso.datetime()andlte(col, fx.clock.now())pass through withoutnew Date(...).
Dev, Keel & create-oke
- Domain schema declare auto-resolves:
schema.ts(single file) if present, elsesrc/db/schema/index.ts(folder).schema.decl.tsstill resolves.db.declareoverrides. Shorter and Keel use the folder only. Generated emit issrc/db/drizzle/(one file per table +index.ts).oke devwatchessrc/db/schema/*.ts. - create-oke starters no longer ship a Vite
web/SPA — backend + Console only. Add your own frontend when you need one. - shorter starter README documents the URL-shortener architecture (HTTP map, insert-only RLS, KV redirect, Reach digest).
- shorter starter link
in/outcontracts are plain Zod (z.object+z.iso.datetime()on the wire) — notdrizzle-orm/zod. Store insert coerces ISO / epoch-ms onto timestamp columns, solinks.createpassesinput.expiresAtthrough. - shorter starter
link-created/link-clickedsubscribers live inlinks/signals.tsnext to the declarations (noton-created.ts/on-clicked.ts).oke dev/oke buildstill lifton()exports fromsignals.tsonto the unit. - shorter starter
links.createstampsid/createdAtfromfx.id/fx.clock.now()(epoch-ms; store coerces for SQL), passesexpiresAtfrominas ISO, omits nullablearchivedAt, and returnsfx.json.create(row). List / get / archive return store rows;out: LinkOutprojects timestamps (Date or epoch-ms). - shorter starter
links/_shared.tsholds owner lookup, short-code mint, 302, KV warming, andpublicShortUrl(oke buildskips_files). Expire / reach stay inline with a few comments. - shorter starter exports carry TSDocs (error shapes, schema, seed, locales,
App). - shorter starter public
GET /:codeusesgate.all(gate.public, ip rate)(linksRedirect, 300/min) instead of a bare.public(). - shorter starter
mainunit putsHealthOut/RootOutinshapes.ts(same contract file aslinks), and drops the emptysignals.ts. - blank starter
mainunit matches that pattern (HealthOut/RootOutinshapes.ts, typed healthout, no emptysignals.ts). - shorter starter element wiring lives under
src/core/(store.ts,gate.ts,vault.ts,email.ts,ai.ts) withsrc/core/index.tsas the barrel (import "@/core").oke ai setupfillssrc/core/ai.ts. - shorter starter
oke({ secrets })takesAPP_VAULT(same name as blank). - shorter starter
shapes.tsis HTTP/Zod contracts only; lookups, mint, KV, 302, andpublicShortUrllive in_shared.ts. List/report use SQLorderBy; Reach loads owners withinArray; expire archives pastexpiresAtin one SQLUPDATE(lte+fx.clock.now()) and only loops to drop KV keys; public SQL fallback is the inverse (isNull(archivedAt)andexpiresAtnull orgtnow). SeedcreatedAtis ISO (store coerces).mail.template({ catalog })supplieslink-created/reach-digestsubject/body. - Keel Channel templates carry per-locale
catalogbodies (no JSON-fallback subjects).
Docs
- SQL docs declare both layouts: single-file
schema.ts(blank) and folderschema/(shorter / Keel), both imported as@/db/schema.schema.tswins when both exist. Generated emit issrc/db/drizzle/(one file per table). - Vault overview names shorter contracts in
src/core/vault.ts. - Channel docs put
{{field}}bodies on.template({ catalog })for email, SMS, WhatsApp, and push.oke({ channel.catalog })is documented as an overlay. Plugin API.channelCatalog()matches. - Routing, Channel, Clock, CLI, Try It, and the homepage match create-oke
blank/shorter(linkstree,signals.ts, no Notes /standard). - Routing skip-list:
on()consumers insignals.tsstill join the unit (api.links.onCreatedover RPC). - HTTP
outprojects store rows:fx.json.create(row)/return row/fx.json.withQuery(rows, input)(Date → ISO-8601). Documented on Flow, HTTP, Store, SQL, and fx. - Clock and SQL docs: pass
fx.clock.now()intotimestampcolumns as-is for insert, update, **and WHERE** — store coerces epoch-ms / ISO toDate.
🐛 Fixed
Runtime
- File / memory / postgres CronStore reconcile is an atomic read-modify-write, so a second instance cannot clobber a live leader lease and double-fire a due tick.
- Edge LinearRouter prefers static paths over an earlier
/:param, soGET /healthis not swallowed byGET /:code(okengine/httpdefault). okengine/client-reactno longer pulls the server realtime binder into Vite SPAs. ImportingCancrashed the page onnode:async_hooks. The mutation-id header now lives in a Node-free module.