okengine 0.17.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -8,6 +8,8 @@ source: "docs/spec/unified-theory.md"
8
8
  Every provider below speaks a protocol oke already drives — Postgres wire or Redis wire.
9
9
  Hand the connection string to that driver; no new driver id, no Flow code changes.
10
10
 
11
+ These managed providers are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
12
+
11
13
  <Callout title="The one rule">
12
14
  Vendor choice lives in a connection URL (`DATABASE_URL` / `REDIS_URL`) — never in
13
15
  `drivers.store.sql` / `drivers.store.kv`, which only ever say `postgres` or `redis`.
@@ -8,6 +8,8 @@ source: "docs/spec/unified-theory.md"
8
8
  Every recipe below is already wired into `oke docker` — pin an image in `oke.config.ts`
9
9
  and get env, healthcheck, and a connection URL for free.
10
10
 
11
+ These recipes are real, working infrastructure choices behind the same two templates (`standard`, `advanced`) and the same eight elements — not alternative products or separate frameworks.
12
+
11
13
  <Callout title="The one rule">
12
14
  Vendor choice lives in `images[…]` — never in `drivers.*`, which only ever say protocol ids
13
15
  (`postgres`, `redis`, `s3`, `smtp`, `meilisearch`, `openai-compatible`, `ollama`, …).
@@ -27,7 +27,8 @@ oke test # bun test with PGLite posture (NODE_ENV=test,
27
27
  oke mode # removed — prints error; use oke dev (Compose) / oke test (PGLite)
28
28
 
29
29
  oke start # runs exactly what production runs (this is the Docker CMD)
30
- oke doctor # verify secrets, ports, drivers, tenancy, schema drift
30
+ oke doctor # verify secrets, ports, drivers, tenancy, schema drift, FD headroom,
31
+ # CDC outbox backlog/retention, live fan-out pressure
31
32
  oke console claim-code # print setup claim code from .oke/claim-code (after Console boot)
32
33
  # → `bun run dev:console` boots kernel + Vite with a fixed
33
34
  # operator (`dev@oke.dev` / `Okengine123!`) — Sign in (prefilled)
@@ -141,7 +142,7 @@ the hero (not once per process).
141
142
  that `@electric-sql/pglite` resolves, then forwards argv to `bun test`.
142
143
 
143
144
  `oke mode` and `oke dev --local` / `-l` are removed — they exit with an error.
144
- See [Migrating environments](/docs/reference/migrating-environments).
145
+ Rename the env keys in `oke.config.ts` to `dev` / `test` / `prod`.
145
146
 
146
147
  ### create-oke
147
148
 
@@ -172,7 +172,8 @@ const { data } = await api.auth.signInEmail({ email, password });
172
172
  if (data) session.set(data);
173
173
  ```
174
174
 
175
- React: `useSession(api, session)` and `useLive(api, signal, input?)` from `okengine/client-react`.
175
+ React: `useSession(api, session)`, `useLive(api, signal, input?)`, and
176
+ `useLiveQuery({ api, listFlow, live, query? })` from `okengine/client-react`.
176
177
 
177
178
  | Step | What happens |
178
179
  | ---------------------- | ---------------------------------------------------------------- |
@@ -205,6 +206,7 @@ A 401 with no `{ data, error }` body becomes `TransportError` with `data.status:
205
206
  | `AUTH_ERROR_CODES` | `okengine/client/auth` | Common auth Flow / gate codes |
206
207
  | `useSession(api, session?)` | `okengine/client-react` | React status + `auth.me` |
207
208
  | `useLive(api, signal, …)` | `okengine/client-react` | React `events` / `latest` / `isConnected` |
209
+ | `useLiveQuery({ … })` | `okengine/client-react` | Live list + optimistic `mutate` over a resource |
208
210
 
209
211
  Core `okengine/client` stays under the size budget — helpers are separate exports. Not in
210
212
  core today: cookie jars or plugin `.client()` decorations. Browser apps: also see
@@ -305,6 +307,61 @@ const { events, latest, error, isConnected } = useLive(
305
307
  );
306
308
  ```
307
309
 
310
+ ### Live queries (`store.resource({ live: true })`)
311
+
312
+ When a resource opts into `live: true`, the compiler mounts `GET <path>/live` next to the CRUD
313
+ verbs. That route streams **classified** row events — not a shared tape — so each subscriber only
314
+ sees rows that still pass their RLS stamp + list filters:
315
+
316
+ | `kind` | Meaning |
317
+ | --------- | -------------------------------------------------------------- |
318
+ | `upsert` | Row visible under stamp + query — merge/replace by primary key |
319
+ | `revoked` | Row left visibility (`reason: "rls"` \| `"query"`) — remove |
320
+ | `delete` | Row deleted in CDC — remove |
321
+
322
+ ```typescript
323
+ import { useLiveQuery } from "okengine/client-react";
324
+
325
+ const { data, error, isLoading, isConnected, isReconnecting, refetch, mutate } = useLiveQuery({
326
+ api,
327
+ listFlow: api.tasks.list,
328
+ query: { status: "open" },
329
+ live: { method: "GET", path: "/tasks/live" }, // from app.$routes
330
+ options: {
331
+ enabled: session.status === "authenticated", // default true — idle when false
332
+ refreshKey: tenantId, // identity change → full re-subscribe
333
+ onAuthRefresh: onAuthRefreshed, // auth.refresh() → new snapshot + replay
334
+ },
335
+ });
336
+
337
+ await mutate(
338
+ api.tasks.update,
339
+ { id, status: "done" },
340
+ {
341
+ optimistic: (rows) => rows.map((r) => (r.id === id ? { ...r, status: "done" } : r)),
342
+ pkOf: (input) => input.id,
343
+ },
344
+ );
345
+ ```
346
+
347
+ Every `mutate` call generates a client UUID sent as the `X-Oke-Mutation-Id`
348
+ header — the server echoes it onto that write's CDC events, so:
349
+
350
+ - Your own late SSE echoes never double-apply (pending-set dedupe).
351
+ - Reconnects replay-guard by event `seq` (`isReplayedEvent`).
352
+ - Manual `refetch()` re-runs only the HTTP list read; reconnects always do a
353
+ full subscribe-protocol cycle (new snapshot + replay).
354
+
355
+ | State | Meaning |
356
+ | ---------------- | ------------------------------------------------------------------- |
357
+ | `isLoading` | Waiting for the first snapshot — no data yet |
358
+ | `isConnected` | SSE stream is open |
359
+ | `isReconnecting` | Stream dropped after a successful load; reconnect backoff in flight |
360
+
361
+ **Consequence:** optimistic patches roll back automatically when the Flow returns
362
+ `error !== null`. Server CDC / the successful response clear the override so the next upsert is
363
+ authoritative.
364
+
308
365
  ## Exports
309
366
 
310
367
  | Export | Kind | Role |
@@ -28,8 +28,7 @@ export default defineConfig({
28
28
  | `prod` | Production deploy | Shared durable backends |
29
29
 
30
30
  Missing `dev` pins fill from `prod` (`fillDevFromProd`). Only `dev` / `test` / `prod` keys are
31
- valid — see [Migrating environments](/docs/reference/migrating-environments) if you still have
32
- `local` / `docker` maps.
31
+ valid — rename `local` `dev` and `docker` → `prod` if you still have the old maps.
33
32
 
34
33
  ## Quick start
35
34
 
@@ -246,7 +245,7 @@ Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated t
246
245
 
247
246
  <Accordion title='oke.config: … uses removed driver "sqlite"'>
248
247
  The `sqlite` driver is gone. Pin `postgres` for `dev`/`prod` and `pglite` for `test`. Edit the
249
- file by hand — see [Migrating environments](/docs/reference/migrating-environments).
248
+ file by hand — rename the env keys to `dev` / `test` / `prod` first.
250
249
  </Accordion>
251
250
 
252
251
  <Accordion title='drivers.store.sql.test must be "pglite"'>
@@ -268,7 +267,6 @@ Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated t
268
267
 
269
268
  ## Learn more
270
269
 
271
- - [Migrating environments](/docs/reference/migrating-environments) — four-key maps → `dev`/`test`/`prod`
272
270
  - [CLI Reference](/docs/reference/cli) — `oke dev` · `oke test` · `oke db`
273
271
  - [Environment Variables](/docs/reference/environment-variables) — URL and secret resolution
274
272
  - [Store](/docs/elements/store) — what the store drivers back
@@ -230,7 +230,9 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
230
230
  | `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***` |
231
231
  | `fx.t(key, values?)` | ICU MessageFormat — active locale → `i18n.default` → key |
232
232
  | `fx.locale` | Active locale (`Accept-Language` matched to `i18n.locales`) |
233
- | `fx.id()` | UUIDthe only legal id generator |
233
+ | `fx.id()` | OKID21-char native id from `okengine/okid` |
234
+
235
+ See [OKID](/docs/reference/okid) for generation options and the entropy model.
234
236
 
235
237
  `fx.t` uses [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
236
238
  (FormatJS): `{name}`, plurals, `select` / `selectordinal`, rich-text tags.
@@ -15,11 +15,6 @@ Dense tables and command lists. Reach for these when you already know what you a
15
15
  description="Every option in oke.config.ts."
16
16
  href="/docs/reference/configuration"
17
17
  />
18
- <Card
19
- title="Migrating environments"
20
- description="local/docker maps → dev/test/prod."
21
- href="/docs/reference/migrating-environments"
22
- />
23
18
  <Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
24
19
  <Card
25
20
  title="i18n"
@@ -4,7 +4,6 @@
4
4
  "pages": [
5
5
  "index",
6
6
  "configuration",
7
- "migrating-environments",
8
7
  "fx",
9
8
  "i18n",
10
9
  "client",
@@ -12,6 +11,7 @@
12
11
  "errors",
13
12
  "plugins",
14
13
  "cli",
15
- "security"
14
+ "security",
15
+ "okid"
16
16
  ]
17
17
  }
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: "OKID"
3
+ description: "OKE's native id generator — compact, URL-safe, cryptographically random ids from okengine/okid, with an opt-in time-sortable and alphabet-controlled variant."
4
+ icon: "Hash"
5
+ source: "docs/spec/unified-theory.md"
6
+ ---
7
+
8
+ Using `okengine/okid` gives you an id for any primary key, request trace, or event that is short (`okid()` is 21 characters), URL-safe, and random from a cryptographic source. Turn to it when a plain UUID string is more than you need; your app already generates them wherever `defaultFn(id)` is used.
9
+
10
+ <Callout title="The one rule">
11
+ Use OKID for identity, never for secrets. An id is enumerable by design, so anything you hand to
12
+ an untrusted client must be a token from the Vault, not an OKID.
13
+ </Callout>
14
+
15
+ ## Quick start
16
+
17
+ <Steps>
18
+
19
+ <Step>
20
+ ### Install nothing — it is exported by the package
21
+
22
+ ```typescript
23
+ import { okid } from "okengine/okid";
24
+ ```
25
+
26
+ </Step>
27
+
28
+ <Step>
29
+ ### Generate an id
30
+
31
+ ```typescript
32
+ const userId = okid();
33
+ const requestId = okid(16);
34
+ const eventKey = okid({ sortable: true });
35
+ const inviteCode = okid({ lookAlikes: false, uppercase: false });
36
+ ```
37
+
38
+ </Step>
39
+
40
+ <Step>
41
+ ### Store it anywhere a string fits
42
+
43
+ ```typescript
44
+ // field.id() is shorthand for "default generation id" — currently OK ID.
45
+ field.id().primaryKey();
46
+ // Or pin OK ID explicitly:
47
+ field.okid().primaryKey();
48
+ ```
49
+
50
+ The same 21-character id lands in your SQL primary keys, KV keys, and trace ids.
51
+
52
+ </Step>
53
+
54
+ </Steps>
55
+
56
+ ## Reference
57
+
58
+ | Call | Result | Notes |
59
+ | -------------------------------------------------- | ----------------------------------------- | --------------------------------------- |
60
+ | `okid()` | 21-char URL-safe id, 126 bits of entropy | 64-char alphabet, `a-zA-Z0-9-_` |
61
+ | `okid(length)` | id of exactly `length` characters | integer between 8 and 128 |
62
+ | `okid({ length })` | options form, id of `length` | default 21 |
63
+ | `okid({ sortable })` | time-prefixed id, 8 prefix + `length − 8` | lexicographic order ≈ creation order |
64
+ | `okid({ numbers, lowercase, uppercase, symbols })` | charset control | each group defaults to on |
65
+ | `okid({ lookAlikes })` | confusable-char control | `lookAlikes: false` drops `1lI0Oouv5Ss` |
66
+
67
+ ### Options
68
+
69
+ | Option | Type | Default | Meaning |
70
+ | ------------ | --------- | ------- | ----------------------------------------------------------- |
71
+ | `length` | `number` | `21` | total id length in characters, between 8 and 128 |
72
+ | `sortable` | `boolean` | `false` | prefix an 8-char epoch-ms timestamp so sorting follows time |
73
+ | `numbers` | `boolean` | `true` | include `0-9` |
74
+ | `lowercase` | `boolean` | `true` | include `a-z` |
75
+ | `uppercase` | `boolean` | `true` | include `A-Z` |
76
+ | `symbols` | `boolean` | `true` | include `-` and `_` |
77
+ | `lookAlikes` | `boolean` | `true` | include confusable chars `1lI0Oouv5Ss`; set `false` to drop |
78
+
79
+ ### Exported constants
80
+
81
+ | Constant | Value | Meaning |
82
+ | -------------------------- | ----------------------------------------- | -------------------------------- |
83
+ | `OKID_ALPHABET` | `a-zA-Z0-9-_` | default, Base64URL order |
84
+ | `OKID_SORTABLE_ALPHABET` | alphabet sorted by code unit (same chars) | used by the sortable encoder |
85
+ | `OKID_LOOKALIKE_CHARS` | `1lI0Oouv5Ss` | dropped when `lookAlikes: false` |
86
+ | `OKID_DEFAULT_LENGTH` | `21` | default length |
87
+ | `OKID_MIN_LENGTH` | `8` | shortest non-sortable length |
88
+ | `OKID_MAX_LENGTH` | `128` | longest length |
89
+ | `OKID_SORTABLE_MIN_LENGTH` | `16` | shortest sortable length (8+8) |
90
+
91
+ ## Collision resistance
92
+
93
+ Every character is drawn uniformly from the alphabet with `crypto.getRandomValues()`. Because it uses an unbiased character selection (never modulo), each character carries exactly `log2(alphabet)` bits of entropy. At the default 21 characters over 64 symbols, that is 126 bits — the birthday-bound collision probability across one billion ids is on the order of `10⁻²¹`. You do not need a UUID for collision resistance; this is where a UUID is stronger only because it is a different format, not a different amount of randomness.
94
+
95
+ **Consequence:** two ids minted at the same millisecond are still distinct — the timestamp prefix never replaces entropy, it prefixes it.
96
+
97
+ ## Sortable ids
98
+
99
+ `sortable: true` prepends 48 bits of `Date.now()` encoded in exactly 8 characters, in an alphabet whose sort order matches time order. Sorting a batch of these ids reproduces the creation order across milliseconds.
100
+
101
+ **Consequence:** a sortable id embeds its creation time (millisecond precision), so keep them out of public, enumerable surfaces. Clock skew distorts order but can never produce a duplicate — the tail stays random.
102
+
103
+ ## Alphabet control
104
+
105
+ Turning groups off shrinks the alphabet. With a non-power-of-two alphabet, OKID uses rejection sampling instead of modulo, so every remaining character stays equally likely — the output never becomes measurably biased.
106
+
107
+ **Consequence:** smaller alphabets mean fewer bits per character. `lookAlikes: false` alone drops the default entropy only slightly (126 → ~120 bits); dropping whole groups costs more. Choose the smallest alphabet that fits the human-transcription use case.
108
+
109
+ ## Under the hood
110
+
111
+ The generator is a pure function: no counters, no process or machine fingerprint, no shared mutable state. It is safe to call concurrently from any number of workers, and every id uses only the bytes it needs — no hidden timestamp, no hidden prefix.
112
+
113
+ ## Troubleshooting
114
+
115
+ <Accordions>
116
+ <Accordion title="I get an error for an empty alphabet">
117
+
118
+ Passing `numbers: false, lowercase: false, uppercase: false, symbols: false` at the same time throws a `RangeError` with the message `okid: alphabet is empty — enable at least one character group`. Re-enable at least one group, or don't use the option object and rely on the default alphabet.
119
+
120
+ </Accordion>
121
+ <Accordion title="I get a RangeError for a length">
122
+
123
+ `okid(0)`, `okid(-1)`, `okid(7)`, `okid(129)`, and non-integer lengths throw a `RangeError`. Sortable ids have a higher floor: passing a sortable length below 16 throws. Keep lengths between 8 and 128 (16–128 for sortable).
124
+
125
+ </Accordion>
126
+ <Accordion title="My ids are not sortable by the alphabet order I expected">
127
+
128
+ The default alphabet order is not lexicographic; `_` sorts between uppercase and lowercase. When `sortable` is on, ids use the code-point-ordered alphabet, so plain string comparison matches time order. Do not customize the alphabet in sortable mode — the option exists exactly because the default order is not trustworthy for ordering.
129
+
130
+ </Accordion>
131
+ </Accordions>
132
+
133
+ ## References
134
+
135
+ - [Store](/docs/elements/store) — `defaultFn(id)` in table declarations delegates to `okid()`
136
+ - [fx](/docs/reference/fx) — `fx.id()` in the runtime uses `okid()`
137
+ - [Clock](/docs/elements/clock) — process `instanceId` (`inst-<okid>`) is an OKID
@@ -175,7 +175,7 @@ export function createAuthHttpBindings(
175
175
  } = {},
176
176
  ): AuthHttpMaterialization {
177
177
  const sessions = options.sessions ?? config.sessions ?? createSessionStore();
178
- const identities = options.identities ?? createIdentityStore();
178
+ const identities = options.identities ?? config.identities ?? createIdentityStore();
179
179
  const loginAttempts = createLoginAttemptBag();
180
180
  const rateGates = createAuthRateGates(options.rateLimitEnabled !== false);
181
181
  const policyGates = createAuthPolicyGates(config);
@@ -8,6 +8,7 @@ import type { PasswordHashOptions } from "../runtime/types.ts";
8
8
  import type { SessionStore } from "./sessions.ts";
9
9
  import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
10
10
  import type { BreachCheckFn } from "./breach-check.ts";
11
+ import { createIdentityStore, type IdentityStore } from "./identity.ts";
11
12
  import type { PasswordPolicyOptions } from "./password-policy.ts";
12
13
  import {
13
14
  resolveAuthSchema,
@@ -116,6 +117,11 @@ export interface GateAuthOptions extends AuthSchemaOptions {
116
117
  * Not part of the public app DX — escape hatch for embedding.
117
118
  */
118
119
  readonly sessions?: SessionStore;
120
+ /**
121
+ * Shared identity / credential store. Created automatically when omitted.
122
+ * All Gate auth method plugins resolve against this one store by default.
123
+ */
124
+ readonly identities?: IdentityStore;
119
125
  /**
120
126
  * Shared API key store. Created automatically when omitted
121
127
  * (`gate.auth.secret` is the HMAC pepper).
@@ -186,6 +192,8 @@ export interface ResolvedGateAuth {
186
192
  readonly hooks: AuthDatabaseHooks | undefined;
187
193
  readonly sessions: SessionStore | undefined;
188
194
  readonly apiKeyStore: ApiKeyStore;
195
+ /** Shared identity store — the single user/credential source across Gate auth + method plugins. */
196
+ readonly identities: IdentityStore;
189
197
  readonly tenant: ResolvedTenantAuth | undefined;
190
198
  readonly tenantStore: TenantStore | undefined;
191
199
  readonly now: (() => number) | undefined;
@@ -296,6 +304,7 @@ export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAu
296
304
  hooks: auth.hooks,
297
305
  sessions: auth.sessions,
298
306
  apiKeyStore: auth.apiKeyStore ?? createApiKeyStore({ pepper: secret }),
307
+ identities: auth.identities ?? createIdentityStore(),
299
308
  tenant: auth.tenant ? resolveTenantAuth(auth.tenant) : undefined,
300
309
  tenantStore: auth.tenant ? (auth.tenantStore ?? createTenantStore()) : auth.tenantStore,
301
310
  now: auth.now,