@ultimat3/cli 29.4.0 → 30.1.0

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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1314 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
package/wiki/Auth.md ADDED
@@ -0,0 +1,195 @@
1
+ # Auth
2
+
3
+ **The output of authentication is an `Actor`.** Nothing downstream authorizes on a session row, a
4
+ user row or an api key: HTTP, actions, jobs and MCP all read `ctx.actor` and hand it to the one
5
+ policy system ([Policies and authz](Policies-And-Authz)). Package `@ultimat3/auth` (tier 2) — the
6
+ full reference is
7
+ [`packages/auth/README.md`](https://github.com/developerz-ai/ultimate/blob/main/packages/auth/README.md);
8
+ the walk-through is [Tutorial 3](Tutorial-03-Auth-And-Admin).
9
+
10
+ ```ts
11
+ import { defineAuth, login, oauthLogin, postgresAuthAdapter } from '@ultimat3/auth';
12
+
13
+ export const auth = defineAuth({
14
+ adapter: postgresAuthAdapter(), // or memoryAuthAdapter()
15
+ session: { absoluteTtlMs: 30 * 864e5, idleTtlMs: 7 * 864e5 },
16
+ password: { minLength: 12 },
17
+ mfa: { issuer: 'Acme' },
18
+ providers: ['github', 'google'], // required to serve any OAuth route — the default is []
19
+ });
20
+
21
+ const { actor, token, cookie } = await login(auth, { email, password, ip });
22
+ const { start, callback } = oauthLogin(auth); // /auth/oauth/:provider and its callback
23
+ ```
24
+
25
+ The tables (`x_users`, `x_sessions`, `x_accounts`, `x_verifications`, `x_api_keys`, and the
26
+ passkeys' `x_passkeys`, `x_passkey_challenges`, `x_passkey_evidence`) are created by every boot; an
27
+ app writes no migration for them.
28
+
29
+ **Signing in by handle** (`As of 2026-10`): an app whose users are `@ada` names the lookup once —
30
+ `defineAuth({ adapter, handles: async (handle) => (await db.users.where({ handle }).one())?.id ?? null })`
31
+ — and calls `login(auth, { handle, password })`. The handle arrives trimmed and lowercased
32
+ (`normaliseHandle`), its failures count against a bucket of its own, and an unknown handle is the
33
+ same `X_UNAUTHENTICATED` after the same hash as a wrong password. The app's `users` row and the auth
34
+ user share one id: `register(auth, { id, email, password })` creates the auth user under it. A handle
35
+ login on an app that declared no `handles` is `X_CONFIG_INVALID`. The deployed demo
36
+ (`dummy/social-media-clone/apps/web/app/auth/`) signs in this way.
37
+
38
+ ## Sessions
39
+
40
+ | Property | How |
41
+ |---|---|
42
+ | opaque tokens | only `sha256(secret)` reaches the database |
43
+ | two expiries, independent | absolute and idle; activity never moves the absolute ceiling (`X_SESSION_EXPIRED` names which) |
44
+ | not a write per request | `lastSeenAt` moves at most once per `idleSlideMs` (default `idleTtlMs / 20`); a changed IP or user agent is written at once |
45
+ | revocation is immediate | the user row is re-read on every request, so a revoked role takes effect on the next one |
46
+ | the cookie | `__Host-x_session`, `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/`, `Max-Age` |
47
+ | brute force | per-IP, per-account and per-org buckets (`X_ACCOUNT_LOCKED`); every login failure is one indistinguishable `X_UNAUTHENTICATED`. `rateLimit: { scope: 'shared' }` makes the count one for the fleet. The org bucket (`orgMaxAttempts`, default 100 per window) can be exhausted by someone who knows about 20 of an org's addresses, locking its password sign-in — kept by decision ([`SECURITY.md`](https://github.com/developerz-ai/ultimate/blob/main/SECURITY.md#known-gaps)) |
48
+
49
+ **Signing out** is `logout(auth, token)` for the row plus `signOutHeaders()` on the response: it
50
+ expires the session cookie and sends `Clear-Site-Data: "cache", "storage"`
51
+ (`SIGN_OUT_CLEAR_SITE_DATA`), so the browser drops the previous principal's IndexedDB, storage,
52
+ service worker and cached pages. Revocation at scale is `revokeUserSessions`, `revokeOrgSessions`,
53
+ `revokeSessionsCreatedBefore` (after rotating a secret) and `disableUser`, which also revokes the
54
+ user's live API keys. A `passwordHash` change through `updatePrivileges` ends every other session
55
+ of that user — all of them when no session of theirs is passed, as in a reset.
56
+
57
+ **A sign-in page is one command**, `As of 2026-11`: `x g sign-in --password` (a native form over
58
+ `login()`, its second leg over `completeMfa()`) or `x g sign-in --oidc <provider>` (an OpenID Connect
59
+ issuer, discovered on the first sign-in). Both write the sign-in page, the `destroySession` action,
60
+ `auth.signInPath`, and a boot guard: outside `development` and `test` a `web` or `sync` role with
61
+ its sign-in keys unset refuses to start (`X_AUTH_SIGN_IN_UNCONFIGURED`). `--password` also writes
62
+ `createSession` / `confirmSession` and the second-factor page; `--oidc` writes the start and
63
+ callback routes and drops `x new`'s development viewer while its files are still `x new`'s own. What each writes:
64
+ [CLI reference](CLI-Reference#x-g).
65
+
66
+ ## OAuth
67
+
68
+ `github` and `google` ship; `registerOAuthProvider` adds one. PKCE is mandatory on every
69
+ provider — `usesPkce: false` does not typecheck — and the handshake is a signed `__Host-` cookie
70
+ bound to its provider. An id token is verified against the provider's JWKS. An identity links to an
71
+ **existing** account only when both the provider and that account have verified the address
72
+ (`link: 'verified-email'`, the default); `'never'` is the only alternative. Credentials are
73
+ `<PROVIDER>_CLIENT_ID` / `<PROVIDER>_CLIENT_SECRET`.
74
+
75
+ | Rule (`As of 2026-10`) | |
76
+ |---|---|
77
+ | `redirect_uri` origin | `oauthLogin(auth, { baseUrl })`, else `APP_URL`. With neither the start leg answers `X_ENV_MISSING` — never the request's `Host` |
78
+ | a refused leg's body | `code`, `title`, `docs`, one fixed `cause`, `fix: x errors explain <CODE> --json`. The authored cause and fix are the `auth.oauth.refused` log line |
79
+ | `discoverOAuthProvider` | refuses a document whose `issuer` is not the one asked for (a trailing slash aside) |
80
+ | a public client (`As of 2026-10-18`) | `tokenEndpointAuth: 'none'` on the provider, `discoverOAuthProvider` or `credentials`: no `<PROVIDER>_CLIENT_SECRET` is read and no `client_secret` is sent; PKCE still is. `'client_secret_basic'` sends the pair in `Authorization: Basic`. Absent: `'client_secret_post'`, unchanged |
81
+ | `apple` | not a built-in since 26.0.0 ([#709](https://github.com/developerz-ai/ultimate/issues/709)): Apple POSTs its callback and both callback routes are GET, so it could never complete a sign-in. An app that needs it registers its own with `registerOAuthProvider` |
82
+
83
+ ## Passkeys
84
+
85
+ **WebAuthn, verified by hand on WebCrypto — no dependency** (`As of 2026-11`). Two uses, one
86
+ declaration: `defineAuth({ passkeys: { rpId, origins } })`. ES256 and RS256; CBOR, COSE and DER are
87
+ parsed in this package, every malformed byte a refusal.
88
+
89
+ ```ts
90
+ import { canonicalJson } from '@ultimat3/core';
91
+ import {
92
+ defineAuth,
93
+ memoryAuthAdapter,
94
+ passkeyLogin,
95
+ passkeyStepUp,
96
+ sha256Hex,
97
+ verifyPasskeyEvidence,
98
+ } from '@ultimat3/auth';
99
+
100
+ const auth = defineAuth({
101
+ adapter: memoryAuthAdapter(),
102
+ passkeys: { rpId: 'example.com', origins: ['https://example.com'] },
103
+ });
104
+
105
+ // Sign-in: register onto a signed-in account, then usernameless sign-in to the same session login() mints.
106
+ const { registerOptions, register, loginOptions, login } = passkeyLogin(auth);
107
+
108
+ // Step-up: the proof is bound to bytes the SERVER derives from the input, and stored as evidence.
109
+ const stepUp = passkeyStepUp<{ readonly to: string; readonly minor: number }>(auth, {
110
+ ttlMs: 60_000,
111
+ digest: ({ input }) => sha256Hex(canonicalJson(input)),
112
+ });
113
+
114
+ declare const userId: string;
115
+ declare const response: unknown;
116
+ const evidence = await stepUp.verify({ userId, input: { to: 'acct-7', minor: 12_500 }, response });
117
+ // Later, in another process, with the row alone:
118
+ const approved = await verifyPasskeyEvidence(evidence, { rpId: 'example.com', origins: ['https://example.com'] });
119
+ ```
120
+
121
+ | Rule | |
122
+ |---|---|
123
+ | tier | functions over `Auth`, wrapped by the app's own actions (tier 2 constructs no `action`) — like `login` and `completeMfa` |
124
+ | browser | `@ultimat3/auth/client`: `newPasskey(options)` / `getPasskey(options)` answer `{ status: 'ok', response }`, `'cancelled'`, `'unsupported'` or `'failed'` — never a throw. Its graph is two files, ~2.3 kB minified (`client-bundle.test.ts`); an island never loads the server barrel |
125
+ | checked, every ceremony | `clientDataJSON` type, the origin against the declared list exactly (no cross-origin iframe), `rpIdHash`, **UP and UV** (user verification is required, never preferred), the signature with the STORED key's algorithm |
126
+ | challenge | 32 random bytes, stored as its sha256 in `x_passkey_challenges` with a TTL, consumed by one `DELETE … RETURNING` — single-use across the fleet, never a process map. Unknown, spent and late are one code: `X_PASSKEY_CHALLENGE_EXPIRED` |
127
+ | counter | a signature counter that did not move forward is a cloned authenticator (`X_PASSKEY_INVALID`); both zero is an authenticator that keeps none, allowed. Advanced compare-and-set |
128
+ | registration | `attestation: 'none'` — the statement is parsed, not trusted; a credential id already registered to anyone is `X_AUTH_WRITE_FAILED`; a challenge issued to one account never registers onto another |
129
+ | sign-in | the session is `mfaSatisfied`: a user-verified passkey is possession plus the device's PIN or biometric |
130
+ | step-up challenge | `SHA-256("ultimate.passkey.step-up.v1\0" ‖ nonce ‖ len ‖ digest ‖ userId)` — never supplied by a caller. A proof for other bytes, or none, is `X_PASSKEY_STEP_UP_REQUIRED` |
131
+ | evidence | `x_passkey_evidence`: the signed bytes, the nonce, the digest and the key itself, so `verifyPasskeyEvidence` re-checks it with no adapter and no clock; any edit to digest, nonce, user, key or signed bytes is `false`. `auth.adapter.listPasskeyEvidence(digest)` finds the approvals of one digest |
132
+ | refusals | `X_PASSKEY_INVALID` (which check failed is the `auth.passkey.refused` log line, never the cause), `X_PASSKEY_CHALLENGE_EXPIRED`, `X_PASSKEY_STEP_UP_REQUIRED`; a bad `passkeys` block is `X_CONFIG_INVALID` at `defineAuth` |
133
+
134
+ ## MFA, verification, API keys, workloads
135
+
136
+ | Capability | Calls |
137
+ |---|---|
138
+ | TOTP + recovery codes | `enrolTotp`, `saveTotpSecret`, `generateRecoveryCodes`. The secret is **sealed at rest** under the app's master key and a plaintext one is refused, never read (`X_MFA_SECRET_UNSEALED`; `x auth seal-mfa` seals rows from before); a recovery code is consumed in one statement, so it works once. A password proven with a factor outstanding is `X_MFA_REQUIRED`, whose `meta.challenge` is a sealed five-minute credential; `completeMfa(auth, challenge, code)` finishes the sign-in, meters the guess against the same lockout buckets a password guess spends, and refuses a spent code on every replica (`x_totp_steps`, installed by the boot) |
139
+ | email verification, password reset, any other mailed credential | `defineVerificationPurpose({ name, template, ttlMs, identifier? })` → `{ issue, consume }` (`As of 2026-11`); `emailVerification` and `passwordReset` are its two built-in instances, and `issueVerification` / `consumeVerification` take any declared name. A token is consumed only when its hash matches, so a wrong guess cannot burn the victim's live link, and one purpose's token never consumes another's. A name declared twice or with no template is `X_VERIFICATION_PURPOSE_INVALID` at boot; an undeclared one at the call is `X_VERIFICATION_PURPOSE_UNKNOWN` (400). `VerificationPurpose` is `string`: a `switch` over it needs a default arm. The mail is sent through an injected `MailSender` ([Mail](Mail)) |
140
+ | API keys — how an agent authenticates | `issueApiKey` → `ult_<env>_<id>_<secret>`, shown once; `verifyApiKey` + `apiKeyActor` give a `kind: 'agent'` actor whose scopes are the key's and never the owner's roles (`X_API_KEY_INVALID`). A key is its owner's credential: a missing or disabled owner refuses it on the next use, a wildcard scope (`*`, `<resource>:*`) is refused at issue, and an owned key keeps only the scopes its owner's grants cover — pass `grantsOf` to expand roles ([`packages/auth/README.md`](https://github.com/developerz-ai/ultimate/blob/main/packages/auth/README.md)); a key with no `userId` is unchanged. `apiKeyResolver(() => store)` is both as the `resolveToken` a bearer mount and `defineAppMcp()` take: `{ actor, scopes }` for a live key, one `null` for every wrong one, and a store fault left a throw. In a test the store is `memoryAuthAdapter()` |
141
+ | service-to-service | `verifyWorkloadToken` reads a Kubernetes service-account token, a SPIFFE JWT-SVID or a cloud IMDS token (they are all one JWT); `actorFromService` gives a `kind: 'service'` actor. The returned `WorkloadToken` carries `raw`, the whole decoded payload, read only after every check held. mTLS is the mesh's job |
142
+ | an external issuer's bearer (`As of 2026-11`) | `jwtBearerResolver({ keys, issuers, audience, requiredScope?, accept?, actorOf })` — `apiKeyResolver`'s twin for JWTs, the `resolveToken` a bearer mount and `defineAppMcp()` take. `keys` is a thunk read when a token arrives (no discovery at import); `accept(raw)` is the app's own claim check after the signature; `actorOf(token)` builds the actor. Every refusal — signature, issuer, audience, expiry, scope, `accept`, and a key set that cannot be read — is `null` plus one `auth.jwt_bearer.refused` log line naming the check, never a claim's value. The mount answers that `null` as `401 X_UNAUTHENTICATED` |
143
+
144
+ ## Authorization server
145
+
146
+ `As of 2026-11`. `authorizationServer({ issuer, store, consentPath, lifetimes, scopes, registration?, grants?, resources? })`
147
+ is one OAuth 2.1 server for an app's MCP endpoint, public API, CLI and partners alike. It ships the
148
+ RFC rules; the consent page, who may be a client and what a subject becomes are the app's.
149
+
150
+ ```ts
151
+ import type { Actor, PgExecutor } from '@ultimat3/core';
152
+ import { authorizationServer, oauthBearerResolver, postgresAuthServerStore } from '@ultimat3/auth';
153
+
154
+ declare const executor: PgExecutor;
155
+ declare function actorFor(subject: string): Promise<Actor | null>;
156
+
157
+ export const oauth = authorizationServer({
158
+ issuer: 'https://www.example.com',
159
+ store: postgresAuthServerStore({ executor }),
160
+ consentPath: '/oauth/consent', // the app's page; the advertised authorization_endpoint
161
+ lifetimes: { accessSeconds: 600, refreshIdleDays: 30, clientSeconds: 900 },
162
+ scopes: ['cases:read', 'cases:write'],
163
+ grants: ['authorization_code', 'refresh_token', 'client_credentials'],
164
+ });
165
+ export const runtime = { routes: [...oauth.routes] }; // apps/web/runtime.ts
166
+ const resolveToken = oauthBearerResolver(oauth, { actor: (grant) => actorFor(grant.subject) });
167
+ ```
168
+
169
+ | Piece | What it does |
170
+ |---|---|
171
+ | `routes` | `GET /.well-known/oauth-authorization-server` (RFC 8414), `POST /oauth/token`, `/oauth/revoke` (RFC 7009), `/oauth/introspect` (RFC 7662), plus `/oauth/register` (RFC 7591, `registration: 'dynamic'`) and `/oauth/device` (RFC 8628). Plain `Route`s for `runtime.routes`, public, no cookie authenticates them. Every answer is `no-store` RFC 6749 JSON |
172
+ | consent | the page calls `readRequest(url)` → `{ client, scopes, resource, redirectUri }` or a refusal; an unknown client or unregistered redirect has `redirectTo: null` — render it, never redirect. Its action calls `approve({ subject, request, scopes? })` / `deny({ request })` for the redirect URL (`code`, `state`, `iss`). A forged request is `X_AUTH_CONSENT_INVALID` (422) |
173
+ | device | the verification page calls `readDevice({ userCode, key })`, `approveDevice` / `denyDevice`; `key` is the caller's bucket — 10 misses lock it (`X_ACCOUNT_LOCKED`) |
174
+ | `registerClient(...)` | a client the app creates itself — a partner's `client_credentials` client; the secret is shown once and stored hashed |
175
+ | `resolve(token)` | `{ subject, scopes, clientId, resource } \| null`. `oauthBearerResolver(server, { actor, resource? })` is the `resolveToken` a bearer mount and `defineAppMcp` take; `actor` runs per call, so a removed member loses access at once |
176
+ | `revoke({ subject, clientId? })` · `purge({ before })` | immediate; `purge` is the app's `task` to schedule, nothing here runs on a timer |
177
+
178
+ | Rule | Held by |
179
+ |---|---|
180
+ | redirect URIs match byte for byte | `authorization-server-request.test.ts` |
181
+ | S256 only; an absent method is `plain` and refused | same |
182
+ | a code lives 2 minutes, is stored hashed, is single use; a replay revokes what it minted | `authorization-server.test.ts`, `authorization-server-store.contract.test.ts` (racing redemptions on Postgres) |
183
+ | refresh rotates; a retired one's replay revokes the grant; idle past `refreshIdleDays` is dead | `authorization-server.test.ts` |
184
+ | `resource` (RFC 8707) binds the grant; one not in `resources` is `invalid_target` | `authorization-server-request.test.ts`, `oauth-bearer-resolver.test.ts` |
185
+ | tokens are opaque and stored hashed, never a JWT, so `revoke` holds on the next lookup | `token-endpoints.test.ts` |
186
+ | an `https://` `client_id` is a metadata document, fetched through `outbound({ dial: 'public' })`, 5 KiB, cached 5 minutes, public clients only | `authorization-server-clients.test.ts` |
187
+ | `client_credentials`: confidential only, no refresh token, subject is the client | `grant-client-credentials.test.ts` |
188
+ | dynamic registration never hands out `client_credentials` and is counted per address | `authorization-server-clients.test.ts` |
189
+
190
+ Bad configuration is `X_AUTH_SERVER_INVALID` naming the field; a grant not in `grants` answers
191
+ `unsupported_grant_type` and is logged `X_AUTH_GRANT_DISABLED`. The five tables (`x_oauth_*`) are
192
+ applied by the boot. For one MCP endpoint, `mcpAuthorizationServer` ([MCP and AI](MCP-And-AI)).
193
+
194
+ Every auth code — `X_UNAUTHENTICATED`, `X_SESSION_EXPIRED`, `X_MFA_REQUIRED`, `X_OAUTH_*`,
195
+ `X_ACCOUNT_LOCKED`, `X_API_KEY_INVALID` and the rest — is in [Error codes](Error-Codes).
@@ -0,0 +1,111 @@
1
+ # Bare VM
2
+
3
+ A fresh Ubuntu box with **bun** and **git** runs a scaffolded app's whole gate. No Docker daemon, no
4
+ Postgres, no Redis, no NATS, no object store, no `.env` editing. Bun runs all four commands below;
5
+ git is what commits the scaffold, and `--no-git` is the form that skips it.
6
+
7
+ ```sh
8
+ bunx create-ultimate demo --no-git
9
+ cd demo
10
+ bun run setup
11
+ bun run verify
12
+ ```
13
+
14
+ | Command | What it needs from the box |
15
+ |---|---|
16
+ | `bunx create-ultimate demo --no-git` | bun, and a network the npm registry answers on. `--no-git` skips `git init`, which is the form an automated provisioner uses; without it the scaffold is committed, and that is git's only job here |
17
+ | `cd demo` | — |
18
+ | `bun run setup` | bun. Six steps: `bun install`, an `.env.development.local` touch, `x db gen "initial"` when `packages/db/migrations` holds no `.sql`, `x db migrate`, `x db seed`, `x manifest` |
19
+ | `bun run verify` | bun. `x verify`, the whole gate: its `budgets` step runs `x build --target static` itself, because `.x/build-stats.json` does not exist yet on a fresh box (`As of 30.0`) |
20
+
21
+ `x new` installs nothing, so `bun run setup` is not optional and `cd demo && bun run verify` stops on
22
+ `X_BUILD_FAILED` naming `bun install` ([Installation](Installation)).
23
+
24
+ ## Why no service is needed
25
+
26
+ The scaffold's `.env.development` ships committed non-secret defaults, and it leaves `DATABASE_URL`
27
+ empty. An empty `DATABASE_URL` is not a hole to fill — it is the switch that selects the embedded
28
+ database, in `resolveServices`
29
+ ([`packages/cli/src/runtime-bindings.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/runtime-bindings.ts)),
30
+ which is the one place the three service bindings are decided:
31
+
32
+ | Binding | Unset env | Resolves to | Set it to |
33
+ |---|---|---|---|
34
+ | db | `DATABASE_URL` | `pglite://<app>/.x/pgdata` — Postgres compiled to WASM, running **inside this process** | a `postgres:` URL |
35
+ | events | `NATS_URL` | `inproc://events` — in-process fanout | a NATS URL |
36
+ | storage | `S3_ENDPOINT` | `file://<app>/.x/storage` | an S3 endpoint |
37
+
38
+ The database is
39
+ [`packages/db/src/pglite.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/pglite.ts):
40
+ a real Postgres, WASM-compiled, opened on a directory under `.x/`, so `x db migrate` and `x db seed`
41
+ write to the same engine the app then reads — no container to wait for, no port to publish, and a
42
+ restart keeps the data. The module is an **optional peer resolved at first query, never at import**,
43
+ so an image that only ever talks to a managed Postgres does not carry the WASM it will never load.
44
+ `@electric-sql/pglite` is a `devDependencies` entry of the generated app, which is what puts it on
45
+ the box during step 1 of `bun run setup`.
46
+
47
+ **Its absence is a coded refusal, never a silent skip**: `X_DB_UNAVAILABLE`, whose `fix:` is
48
+ `bun add @electric-sql/pglite, or set DATABASE_URL to a Postgres server and re-run` — a command the
49
+ reader runs, on a box that has just told them what is wrong.
50
+
51
+ `x doctor` says it earlier than the first query does. Its external-database probe answers nothing
52
+ where `DATABASE_URL` is unset, and that silence is exactly this box, so it asks the embedded
53
+ question too: does the peer **resolve** from the app root — a resolve, never an import, since
54
+ loading it boots the WASM and takes the single-writer lock the next command needs. Red only where
55
+ `DATABASE_URL` is unset *and* the peer is unresolvable ([CLI reference](CLI-Reference)).
56
+
57
+ The gate's `live`, `job` and `e2e` steps need no service of their own in a generated app: the
58
+ queue is Postgres (`SELECT … FOR UPDATE SKIP LOCKED`), the fanout is in-process, and both resolve
59
+ through the table above.
60
+
61
+ ## What the embedded database does not do
62
+
63
+ **No logical replication.** PGlite has no walsender, so there is no write-ahead log to decode and no
64
+ slot to take. A `--live` query still works: `x dev` installs the in-process bridge instead — the
65
+ same row observer the framework's own live tests run on — and the boot line says which feed you
66
+ got, `live=in-process` under the embedded database and `live=replication` under a real one
67
+ (`packages/cli/src/runtime-live-feed.ts`). Its honest bound is that a write made by **another process**
68
+ is invisible to it, which holds by construction under `x dev`, where every role is this one process
69
+ ([Realtime](Realtime)).
70
+
71
+ Reach for the dev compose file only when you want parity against a real Postgres, NATS and S3
72
+ ([Deployment](Deployment)). Nothing on this page uses it.
73
+
74
+ ## Wall time
75
+
76
+ Both tables predate 30.0 and time `bun run check`, the scaffold's old wrapper: `x build --target
77
+ static`, then `x verify`. `bun run verify` does the same work in one command, the build inside
78
+ `budgets`, so the figures are its order of magnitude, not its measurement.
79
+
80
+ On a **WSL2 developer box — not a bare VM, and not a CI runner** — on a Bun the CLI's own floor
81
+ accepts (`x doctor`), against real `x new` scaffolds, first pass, nothing waived and no fix-follow,
82
+ warm Bun cache, `As of 2026-09-11`:
83
+
84
+ | Measure | Default scaffold | `--no-example` |
85
+ |---|---|---|
86
+ | files written | 161 | 133 |
87
+ | `bun run setup` | **6,802ms** | **5,135ms** |
88
+ | `bun run check` | **5,389ms** | **3,909ms** |
89
+ | the first `bun run check`'s verdict | **green, every step**, `budgets` included | **green, every step**, `budgets` included |
90
+
91
+ The cache is the variable to watch, not the box: a **cold** first install takes the `bun install`
92
+ inside `bun run setup` to **12.0s** on the same machine.
93
+
94
+ And on a **free `ubuntu-latest` runner**, which is the measurement that speaks for a provisioned
95
+ box — `ci.yml`'s `scaffold-smoke` job, running these same two scripts on a scaffold written outside
96
+ the checkout, `As of 2026-09-12`:
97
+
98
+ | Measure | Default scaffold | `--no-example` |
99
+ |---|---|---|
100
+ | files written | 161 | 133 |
101
+ | `bun run setup` | **6,665ms** | **5,994ms** |
102
+ | `bun run check` | **7,279ms** | **4,886ms** |
103
+ | the first `bun run check`'s verdict | **green, every step passes** | **green, every step passes** |
104
+
105
+ On both dates the gate was 20 steps; `exercise` joined it in 2026-11, so a run today counts 21.
106
+
107
+ `budgets` is green on that first pass on every one of those runs, because the gate builds before
108
+ it weighs — nothing is waived to get there, and no printed `fix:` is followed before the verdict
109
+ is taken. The job prints the table itself, one row per command and one per gate step, so the run is
110
+ its own measurement rather than a number this page asserts: read it from the job's log when you want
111
+ today's.
@@ -0,0 +1,136 @@
1
+ # Batching and preloading
2
+
3
+ The entity layer never sends one statement per row. A relation is a foreign key read a second way, not a second declaration, and every batching path — automatic or named — reads the same `references()` columns [Entities and migrations](Entities-And-Migrations) already derives relations from.
4
+
5
+ `As of 2026-08`. Source: [`packages/entity/src/relations.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/entity/src/relations.ts), [`query.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/entity/src/query.ts), [`pg-driver.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/entity/src/pg-driver.ts).
6
+
7
+ ## Relations are derived, never declared twice
8
+
9
+ `belongsTo`/`hasMany` come out of the `.references(() => orgs.id)` calls an entity's columns already make — there is no `hasMany: […]` init key, and adding one would put two declarations of one fact in the schema. A `belongsTo` reads the FK's own column (`authorId` → `author`); a `hasMany` reads it from the other side (`posts` inbound to `orgs`). When two foreign keys would collide on one name, **every** member of that colliding group takes its long form together — never just the newcomer — so a second FK declared later cannot silently rename the first relation. What the two tiers still cannot separate is refused as `X_INVARIANT_VIOLATED`, naming both columns, never collapsed into one relation.
10
+
11
+ There is exactly one flat namespace of relation names per entity, `belongsTo` and `hasMany` mixed, resolved through `relationNamed(entity, name)` — a name no foreign key produces is `X_PRELOAD_UNKNOWN_RELATION`, listing the relations that do exist.
12
+
13
+ ## Three ways the same batching shows up
14
+
15
+ | Form | Trigger | Statements |
16
+ |---|---|---|
17
+ | Point-lookup coalescing | several `findById` calls in one microtask | one `where id in (…)` |
18
+ | JIT (sibling-aware) preload | a `findById` whose id is a FK value on a page already read | one `where <key> in (…)` for the whole loop |
19
+ | `.preload('name')` | named on the chain | one extra statement per relation, resolved concurrently |
20
+
21
+ The first two are automatic — nothing to opt into, no `dataloader()`, no `batch()` call. `.preload()` is the explicit, declarative third: name the relation once and every row of the page carries it, resolved *before* the caller sees a row.
22
+
23
+ ### Same-microtask coalescing
24
+
25
+ `findById` called several times inside one microtask of one request is one `select … where "id" in (…)`. The store is a per-request `WeakMap<Ctx, …>`, so a batch dies with the request and never crosses one; a sequential `for … of` loop shares no microtask — its `await` ends the window — which is what the sibling preload below exists for.
26
+
27
+ ### JIT preload — the sibling-aware case
28
+
29
+ `findMany` tags the rows it returns with their own foreign key **values** (never the rows) under a per-request store. The first `findById` whose id matches one of those values then resolves that key for **every** row of the page in one statement, and the rest of a `for … of` loop is served from memory:
30
+
31
+ ```ts
32
+ const page = await db.posts.findMany({ orgId });
33
+ for (const post of page.rows) {
34
+ // Two statements for the whole loop: the page, then one `where "id" in (…)` over every
35
+ // author on it. Every lookup after the first is memory.
36
+ const author = await db.users.findById(post.authorId, { orgId });
37
+ }
38
+ ```
39
+
40
+ Controlled by the `jitPreload` option, default `true`, on `postgresDriver({ jitPreload })` / `postgresRepo(entity, { jitPreload })`. **Not** an `app.config.ts` key — nothing reads config at the seam that builds a repository, so a `database.jitPreload` field would be a switch the framework cannot read. Set it where the driver is built.
41
+
42
+ ### `.preload('relation')` — named on the chain
43
+
44
+ ```ts
45
+ export const db = database({ orgs, posts, members });
46
+
47
+ // Two statements: the page, then one `select … where "id" in (…)` over its authors.
48
+ const page = await db.posts.where({ orgId }).preload('author').page();
49
+ page.rows[0].author; // the member row, or null — always present
50
+ ```
51
+
52
+ A `belongsTo` attaches the row or `null`; a `hasMany` attaches an array, always present so "no author" and "nobody preloaded the author" cannot read the same. `preload()` is resolved on the chain (`relationNamed`), so an unknown relation fails there and not a page later. Attachment happens after the projection — `select()` is widened internally with every preloaded relation's own key, so a projection can never drop what a preload needs; `plan().select` reports the widened list that actually ran. Only the terminals that read a row resolve it: `page()`, `all()`, `one()` — never `count()`, `countBy()` or `plan()`, since none of those has a row to attach one to.
53
+
54
+ A preload is **bounded**: one relation attaches at most `max` related rows to one page — `preload('comments', { max: 2000 })`, default 10,000 (`MAX_PRELOADED_ROWS`). Past it the read is refused (`X_INVARIANT_VIOLATED`, naming the raised ceiling and the `inBatches` form), never truncated — an array one row short reads as "these are the children".
55
+
56
+ Reach for `.preload()` when the relation is part of what the page *is* — a list rendered with its authors, rows handed somewhere that will not call back into the repo, or a read a reviewer should see stated rather than inferred from a loop. The other two forms ask for nothing.
57
+
58
+ ## The tenancy guarantee — a security boundary, not a perf detail
59
+
60
+ **A coalesced or JIT-preloaded statement carries exactly the scope every single lookup it replaces would have carried — same tenant predicate, same soft-delete visibility, same projection, same entity, same client.** This is enforced, not incidental:
61
+
62
+ - The coalescer's scope key (`scopeKey` in `batch-read.ts`) is built from the entity, the table, `includeDeleted`, the projection and every `where` predicate — two lookups share a statement only when all of that matches. A predicate whose value cannot be rendered to a string declines the batch rather than guess.
63
+ - The JIT preload's bucket is keyed the same way, and a bucket is only reused when its client and its write-generation still match; a write to that entity drops the bucket before the statement goes out (`forgetPreloaded`), so a row a request just changed is re-read, never served stale.
64
+ - `.preload()`'s tenancy is *carried*, never inferred: the page's own tenant predicate reaches the related read only when **both** entities are scoped by a column of the same name. A source scoped by `workspaceId` may still carry an ordinary `orgId` predicate of its own — a filter, not its tenancy — and matching on the target's column name alone would lift that filter into the target's tenant scope. Carrying nothing is not a failure of this rule: the related read builds its own plan, and `assertScoped` refuses it as `X_TENANCY_UNSCOPED` rather than answer with a guess.
65
+
66
+ `pg-driver.test.ts`, `jit-preload.test.ts` and `preload.test.ts` each pin this against a real cross-tenant fixture — another tenant's row is unreachable from every one of the three paths, never served by whichever tenant asks.
67
+
68
+ ## Bulk writes — the same idea on the write side
69
+
70
+ A loop of `insert()` calls is the same defect a per-row read loop is, on the other side of the statement:
71
+
72
+ ```ts
73
+ await db.tags.insertAll(names.map((name) => ({ orgId, name }))); // one statement, n rows
74
+
75
+ await db.likes.upsertAll(rows, { // insert, or leave what's there
76
+ onConflict: ['postId', 'memberId'], // the declared unique index, verbatim — `likes` keys on the pair
77
+ onMatch: 'nothing', // …so the tenant column is not in the target: see below
78
+ });
79
+
80
+ const n = await db.posts.updateWhere({ orgId, status: 'draft' }, { status: 'archived' });
81
+ const removed = await db.likes.deleteWhere({ orgId, postId, memberId });
82
+ ```
83
+
84
+ | Call | Shape |
85
+ |---|---|
86
+ | `insertAll(rows)` | one multi-row `insert`, every row parsed through `$parse` exactly as `insert(row)` parses one |
87
+ | `upsertAll(rows, { onConflict, onMatch })` | `on conflict (…) do update` / `do nothing`; a collision overwrites every column the batch names except the conflict target, the primary key, and the soft-delete stamp |
88
+ | `updateWhere(filter, patch)` | the bulk form of `update(id, patch)` — and the only write path for a composite-key entity (`likes`, `blocks`, any join table) with no single-column id to address |
89
+ | `deleteWhere(filter)` | the bulk form of `delete(id)`, same reason |
90
+ | `countBy(column)` | one grouped count, the aggregate a `count()` per row is the N+1 of — see [Entities and migrations → A count per row is one grouped count](Entities-And-Migrations#a-count-per-row-is-one-grouped-count) |
91
+
92
+ `onConflict` names the columns of a **declared unique index**, never a convenient superset: `likes` is `primaryKey: ['postId', 'memberId']` with `orgId` as its tenant column, so `['orgId', 'postId', 'memberId']` matches no index and is refused. The tenant column belongs in the conflict target only under `onMatch: 'update'`, where a collision writes — a target without it would match and overwrite another tenant's row, which is `X_TENANCY_UNSCOPED` and not an N+1 code. Under `onMatch: 'nothing'` no write happens to a row you do not own, so the pair alone is right. Every other call still passes its tenant filter: `deleteWhere({ orgId, postId, memberId })`, not `deleteWhere({ postId, memberId })`.
93
+
94
+ `deleteWhere({})` and `updateWhere({}, patch)` are `X_WRITE_UNFILTERED`, never every row; an empty patch is `X_PATCH_EMPTY`. Both are covered in full, including the tenancy rule on `onMatch: 'update'`, in [Entities and migrations → Writing many rows is one statement](Entities-And-Migrations#writing-many-rows-is-one-statement).
95
+
96
+ ## `inBatches(size)` — sweeping a whole table
97
+
98
+ The chain's own terminal for "every row, not one page":
99
+
100
+ ```ts
101
+ await using batches = db.posts.where({ orgId }).preload('author').inBatches(500);
102
+ for await (const batch of batches) {
103
+ await search.index(batch);
104
+ }
105
+ ```
106
+
107
+ `inBatches` returns a disposable async iterator (`AsyncIterable & AsyncDisposable`) — `break`, `return`, a throw, and `await using` all stop the *next* statement rather than the one in flight, and `close()` is idempotent by construction. Its `.cursor` is where the next batch starts, advanced before the yield, so a consumer that stops early can persist it and resume later with `.after(cursor).inBatches(size)`. Every batch is exactly the page `page()` would have sent at that position — same filters, tenancy, soft delete, projection and every `.preload()` — so this is not a second read path.
108
+
109
+ Keyset only, the same reason there is no `offset()` anywhere in this package: an insert before an OFFSET shifts every later page, silently skipping or repeating rows under concurrent writes. Two refusals happen at `inBatches()` itself rather than one batch later: a size that is not a whole number of rows ≥ 1, and a chain that also called `limit()` — one number would then carry two meanings, and neither reading is safe to guess. An ordering that cannot carry a cursor (a nullable sort column) is refused there too.
110
+
111
+ This is also the mechanism `backfill()` runs on — see [Migrations and backfills](Migrations-And-Backfills).
112
+
113
+ ## The jobs `PgExecutor` carve-out
114
+
115
+ `@ultimat3/jobs`' Postgres driver (`driver-pg.ts`) does **not** go through any of the above. It speaks the one-method `PgExecutor` interface (`query(sql, params)`, declared once in `@ultimat3/core`), and has no `@ultimat3/db` or `@ultimat3/entity` dependency at all — a claim, an ack, a nack, an enqueue or a heartbeat is compiled straight to SQL against that executor, never through a repository. This is a documented exception, not a gap: **queue traffic is invisible to this batching machinery by design**, because there is no repository call for the coalescer, the JIT preload or `.preload()` to attach to. It follows that a job driver statement carries no statement attribution either (see `packages/jobs/CLAUDE.md`), which is a separate, narrower observability note and not something this page's tenancy guarantee needs to answer for.
116
+
117
+ ## Errors
118
+
119
+ Each code carries its own `fix:` line, computed from the call that raised it — the entity's real name, its real columns, the relations it really has. The table below shows the *shape* that line takes; the authority is the error itself, and `x errors explain <CODE> --json` prints it.
120
+
121
+ | Code | Owner | Cause | `fix:` shape |
122
+ |---|---|---|---|
123
+ | `X_PRELOAD_UNKNOWN_RELATION` | `entity` | `<entity>` has no relation named `"<name>"` | `relationNamed('<entity>', '<known>') # or: <the other names>` — and `x entities list --json` when the entity declares none |
124
+ | `X_TENANCY_UNSCOPED` | `entity` | `<entity>.<op>()` was built without an org predicate but the entity has an `orgId` column | `pass { orgId } to <entity>.<op>(), or wrap the plan with orgScoped(entity, orgId, plan)` |
125
+ | `X_TENANCY_UNSCOPED` (upsert) | `entity` | the collision is judged on columns that exclude the tenant column, so another tenant's row would match | `<entity>.upsertAll(rows, { onConflict: ['orgId', …] }) # or onMatch: 'nothing'` |
126
+ | `X_N_PLUS_ONE_QUERY` | `entity` | `<subject>` ran `<n>` times in one request — one read per row | `db.<entity>.preload('<relation>')`, or `db.<entity>.andWhere('id', 'in', ids).all()` |
127
+ | `X_N_PLUS_ONE_WRITE` | `entity` | `<subject>` ran `<n>` times in one request — one write per row | `db.<entity>.insertAll(rows)` (or `upsertAll`/`updateWhere`/`deleteWhere`), or `expectedQueryLoop('<why one per row is optimal>', fn)` |
128
+ | `X_QUERY_SCALES_WITH_ROWS` | `testing` | `x exercise`'s scale sweep: `<primitive>` sent `<shape>` `<n>`× at 2 rows a table and `<m>`× at 8 — a loop no test named ([Testing](Testing#the-plan-and-scale-sweeps)) | `x exercise <primitive> --json # after preload() …, or expectedQueryLoop('<why>', fn)` |
129
+ | `X_WRITE_UNFILTERED` | `entity` | `<entity>.<op>()` named no filter columns — an empty filter would reach every row | `<entity>.<op>({ <primary key columns> }, …)`; a deliberate whole-table write is `x db gen "<name>"` |
130
+ | `X_PATCH_EMPTY` | `entity` | `<entity>.<op>()` named no columns to write | `<entity>.<op>(filter, { <column>: <value> }) # pick a column from: <the entity's columns>` |
131
+
132
+ ```bash
133
+ x errors explain X_N_PLUS_ONE_QUERY --json # the canonical cause + fix for any code above
134
+ ```
135
+
136
+ Full list with `--json` shapes: [Error codes](Error-Codes). Resource lifetimes referenced above — the `using pinned` client and the advisory-lock RAII shape — are covered once in [Resource management](Resource-Management).