@ultimat3/cli 29.4.0 → 30.0.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 +1313 -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
@@ -0,0 +1,594 @@
1
+ # Migrating an existing app
2
+
3
+ **A runbook for the agent doing the migration.** Ordered phases, each with an entry condition, executable steps, a machine-checkable exit condition, and a table of error codes to branch on. Strangler fig on one shared database — a big-bang rewrite is the topology that fails, and it is not documented here.
4
+
5
+ `As of 2026-08`. Source app: a production app on another stack — Rails, Node/Drizzle, Django, Laravel. This page is not about moving between Ultimate versions.
6
+
7
+ | You want | Page |
8
+ |---|---|
9
+ | move an app **from another framework** to Ultimate | this page |
10
+ | what each Rails, Django, Laravel or Node/Drizzle term becomes | [From Rails](Migrating-From-Rails) · [From Django](Migrating-From-Django) · [From Laravel](Migrating-From-Laravel) · [From Node and Drizzle](Migrating-From-Node-Drizzle) |
11
+ | how it compares, and when to pick another stack instead | [Compared](Compared) |
12
+ | move an app **from `@ultimat3/*` 1.x or 2.x to the current major** | [Upgrading](Upgrading) |
13
+ | what is unfinished in the release being adopted | [Known gaps](Known-Gaps) |
14
+ | every `X_*` code named below | [Error codes](Error-Codes) |
15
+
16
+ ## How to execute this page
17
+
18
+ | Rule | Detail |
19
+ |---|---|
20
+ | Every command takes `--json` | the envelope is `{ ok, command, summary, steps?, findings?, data? }`. **`.ok === true` is the postcondition** for every step below unless it says otherwise |
21
+ | A failure names a code | `.findings[].code` is a stable `X_*`. Branch on it by name; never on the summary string |
22
+ | An unrecognised code | `x errors explain <CODE> --json` → `.data.fix` is a runnable line. Run it, then re-run the failed step |
23
+ | A `x verify` failure | `.steps[]` carries `{ name, ok, findings[] }`. The failing step's `name` is the branch |
24
+ | A fork | is pre-decided below, with the condition under which the other arm applies. Where a genuine judgement is required, it is a **⛔ STOP** and you halt |
25
+ | Never invent a command | if a step names no command for a case, that case is a STOP |
26
+
27
+ ## ⛔ Stop conditions — halt and report to the human
28
+
29
+ Do not perform these unattended. Each is irreversible, outward-facing, or a judgement no repository state can settle.
30
+
31
+ | # | Stop |
32
+ |---|---|
33
+ | 1 | **The production cutover** — routing live traffic to Ultimate for the first time |
34
+ | 2 | **Dropping, truncating or retyping any legacy table or column** — `x db gen` refuses without `--allow-destructive`; that refusal is the guard, do not pass the flag |
35
+ | 3 | **Deleting legacy source data** after a backfill, at any scale |
36
+ | 4 | **DNS, load-balancer or reverse-proxy configuration changes** |
37
+ | 5 | **Running any migration against production** — `ROLE=migrate` in a deploy hook is a human's release, not a step here |
38
+ | 6 | **Deciding which app owns DDL** when both still run migrations ([Phase 0, check 6](#6-who-owns-ddl)) |
39
+ | 7 | **Any preflight branch this page marks STOP** — a money column past the ceiling, a password scheme with no verifier, a required `vector` column |
40
+ | 8 | **Retiring the legacy slice** (Phase 7) |
41
+
42
+ Everything else here is safe to run against a **development or branch** database. `x db branch create <name> --json` gives you a disposable copy-on-write clone to work in.
43
+
44
+ ## Where am I? — resuming with a fresh context
45
+
46
+ Determine the current phase from repository and database state alone. Run in order; the first row that does **not** hold is the phase to enter.
47
+
48
+ | Probe | Holds when | Else |
49
+ |---|---|---|
50
+ | `x doctor --json` → `.ok` | the app boots and its environment validates | fix what `.findings[]` names |
51
+ | `x entities --json` lists every table this slice owns | Phase 1 is done | **Phase 1** |
52
+ | `x db migrate --json` → `.ok` | no `X_DB_DRIFT` | **Phase 2** |
53
+ | `x verify --json` → `.ok` | the gate is green | **Phase 2** |
54
+ | a `configureAuthenticator()` call exists under `apps/` | a legacy session resolves to an `Actor` | **Phase 3** |
55
+ | `x queries --json` lists the slice's reads | Phase 4 is declared | **Phase 4** |
56
+ | `x actions --json` lists the slice's writes | Phase 5 is declared | **Phase 5** |
57
+ | `x db backfill --pending --json` → `.ok` | nothing declared is unswept | **Phase 6** |
58
+ | all of the above | | **Phase 7 — ⛔ STOP** |
59
+
60
+ If no `app.config.ts` exists at or above the working directory, every command answers `X_NOT_IN_APP`. Scaffold first: `x new <name>`.
61
+
62
+ ---
63
+
64
+ ## Phase 0 — Preflight
65
+
66
+ **Entry:** a connection to the legacy database; read-only is enough.
67
+ **Exit:** all seven checks answered, and none landed on a ⛔ STOP.
68
+
69
+ ### 1. Money — magnitude
70
+
71
+ `MoneyValue.minor` is a JavaScript `number`. The ceiling is **`Number.MAX_SAFE_INTEGER` = 9,007,199,254,740,991 minor units** — roughly $90 trillion in USD cents. Past it the value is **refused, never truncated**.
72
+
73
+ Find the candidates:
74
+
75
+ ```sql
76
+ select table_name, column_name, data_type
77
+ from information_schema.columns
78
+ where table_schema = 'public'
79
+ and (column_name like '%amount%' or column_name like '%price%'
80
+ or column_name like '%cents%' or column_name like '%total%'
81
+ or column_name like '%minor%' or column_name like '%balance%')
82
+ order by table_name, column_name;
83
+ ```
84
+
85
+ Then, per hit:
86
+
87
+ ```sql
88
+ select max(abs(<column>)) as biggest from <table>;
89
+ ```
90
+
91
+ | `biggest` | Branch |
92
+ |---|---|
93
+ | `null`, or under `9007199254740991` | **proceed** — declare it as `money()` |
94
+ | at or over `9007199254740991` | **⛔ STOP.** Report the table, the column and the value. The remedy is a judgement: the column may store units rather than minor units (fix the writer, then backfill), or the amount may be genuinely that large — then it is `bigint()` or `decimal({ precision, scale })` with the currency in a column of your own. There is no `bigint` money type and there will not be one |
95
+
96
+ A single over-ceiling row makes every read of that entity throw `X_INVARIANT_VIOLATED` forever. Fix the row before the entity exists.
97
+
98
+ ### 2. Money — currency
99
+
100
+ **`money()` is an amount *and* a currency by construction.** Per candidate column from check 1, confirm a currency column sits beside it in the same table.
101
+
102
+ | Table has | Declare as |
103
+ |---|---|
104
+ | `amount_cents` **+** a `currency` column, no scale column | `money({ columns: { minor: 'amount_cents', currency: 'currency', scale: null } })` |
105
+ | `amount_cents` + `currency` + a scale column | `money({ columns: { minor, currency, scale } })` — per part, **merged** over the defaults, so a table that renamed one of the three does not restate the other two |
106
+ | an amount with **no** currency column | `decimal({ precision, scale })` plus the app's own currency column. **Not `money()`** — a single implied currency is the bug `Money` exists to prevent, and a framework that guessed one would guess on every row |
107
+
108
+ Detail: [Money](Money).
109
+
110
+ ### 3. Column types
111
+
112
+ ```sql
113
+ select data_type, count(*) as columns
114
+ from information_schema.columns
115
+ where table_schema = 'public'
116
+ group by data_type order by columns desc;
117
+ ```
118
+
119
+ Match every `data_type` against [the type map](#the-type-map).
120
+
121
+ | Result | Branch |
122
+ |---|---|
123
+ | every type has a builder | **proceed** |
124
+ | `timestamp without time zone` | plan the conversion to `timestamptz` as a migration step, naming the zone the old values were written in. There is no naive-timestamp builder and there will not be one |
125
+ | `USER-DEFINED` that is a native Postgres `enum` type | declare the column as `text()`. It reads and writes correctly; what is missing is a declaration that knows the variants |
126
+ | `USER-DEFINED` that is `vector` (pgvector) | **⛔ STOP.** A `vector` column on an entity is not declarable `As of 2026-08`. Report which table |
127
+ | `inet`, `cidr`, `tsvector`, `hstore`, `interval`, `citext`, PostGIS | **⛔ STOP.** No builder, none planned. Report the columns; the options are leaving them out of the entity or leaving the table to the legacy app |
128
+
129
+ ### 4. Password hashes
130
+
131
+ ```sql
132
+ select left(password_digest, 4) as scheme, count(*)
133
+ from users group by scheme order by count desc;
134
+ ```
135
+
136
+ | Prefix | Branch |
137
+ |---|---|
138
+ | `$2a$`, `$2b$`, `$2y$` (bcrypt) | **proceed.** Verified: the hash verifies through `verifyPassword`, `needsRehash` answers `true`, and the first successful login rewrites it as argon2id — no code, no user-visible change |
139
+ | `$arg` (argon2) | **proceed.** Rehashed on login when the stored parameters are weaker than the policy |
140
+ | anything else — `pbkdf`, `sha`, `md5`, scrypt, bespoke | **⛔ STOP.** No verifier ships. Such a row is safe — it is the one generic credential failure, not a 500 — but it never migrates itself and it burns lockout budget on every attempt ([Passwords](#passwords-rehash-on-login)). Report the schemes and their counts |
141
+
142
+ ### 5. Row counts — the backfill budget
143
+
144
+ ```sql
145
+ select relname, n_live_tup from pg_stat_user_tables order by n_live_tup desc limit 20;
146
+ ```
147
+
148
+ `backfill()` paces at `rate` batches per second of `batch` rows. At the defaults — 1,000 rows, 5 batches/second — 200 million rows is about eleven hours of wall clock. Record the number; it sizes Phase 6.
149
+
150
+ ### 6. Who owns DDL
151
+
152
+ ```sql
153
+ select table_name from information_schema.tables
154
+ where table_schema = 'public'
155
+ and table_name in ('schema_migrations', 'django_migrations', 'knex_migrations',
156
+ '_prisma_migrations', 'x_migrations')
157
+ order by table_name;
158
+ ```
159
+
160
+ | Result | Branch |
161
+ |---|---|
162
+ | only `x_migrations`, or nothing | **proceed** — Ultimate owns DDL |
163
+ | a legacy ledger table is present | **⛔ STOP.** Two migration engines against one schema is the failure mode of every shared-database strangler. A human decides which one is frozen, and writes it down |
164
+
165
+ ### 7. Tenancy
166
+
167
+ ```sql
168
+ select table_name, column_name from information_schema.columns
169
+ where table_schema = 'public'
170
+ and column_name in ('org_id', 'tenant_id', 'account_id', 'workspace_id')
171
+ order by table_name;
172
+ ```
173
+
174
+ Presence of a tenant column is what turns tenancy on. Declare it with `.tenant()` on the column, or `tenant:` on the entity; omitted, it is inferred from `.tenant()` or a property named `orgId`. A tenant-scoped entity refuses any query with no org predicate — so a table listed here and not declared is `X_TENANCY_UNSCOPED` at the first read.
175
+
176
+ ---
177
+
178
+ ## Phase 1 — Model the slice's schema
179
+
180
+ **Entry:** Phase 0 exits clean.
181
+ **Exit:** `x entities --json` → `.ok`, and every table this slice owns appears with every one of its columns.
182
+
183
+ ### 1.1 Declare the entities
184
+
185
+ One `entity()` per table, in `packages/db/src/`. **Every column of every table the slice owns** — a column left out is drift (Phase 2).
186
+
187
+ ```ts
188
+ import {
189
+ arrayOf, bigint, bytes, database, date, decimal,
190
+ entity, json, money, t, text, timestamp, uuid,
191
+ } from '@ultimat3/entity';
192
+
193
+ export const orders = entity('order', {
194
+ table: 'tbl_orders', // the physical table
195
+ columns: {
196
+ id: uuid().primaryKey().column('order_uuid'),
197
+ orgId: uuid().tenant().column('tenant_id'),
198
+ legacyId: bigint().unique().column('id'), // int8 past 2^53 — row type is a string
199
+ reference: text({ max: 64 }).column('order_ref'),
200
+ total: money({ columns: { minor: 'amount_cents', currency: 'currency', scale: null } }),
201
+ taxRate: decimal({ precision: 18, scale: 8 }).column('tax_rate'),
202
+ effectiveOn: date().column('effective_on'), // a calendar date — PlainDate, no zone
203
+ payload: json(t.object({ source: t.string, retries: t.number.int() })).column('meta'),
204
+ signature: bytes().column('sig'),
205
+ tags: arrayOf(text({ max: 40 })),
206
+ createdAt: timestamp().defaultNow().column('created'),
207
+ },
208
+ });
209
+
210
+ export const db = database({ orders });
211
+
212
+ orders.$name; // 'order' — the framework's key
213
+ orders.$table; // 'tbl_orders' — the physical object
214
+ orders.$cacheTag; // 'entity:order' — keyed by the NAME, so renaming a table moves no tag
215
+ ```
216
+
217
+ ### 1.2 The three physical-name overrides
218
+
219
+ | Override | Declares |
220
+ |---|---|
221
+ | `entity(name, { table })` | the physical table, when it is not the entity's own name. **The entity name stays the framework's key** — the registry, the cache tag, `x entities show`, every relation and every policy are keyed by it, so renaming a table never moves a cache tag or a policy. Index names follow the **table**, because an index is a physical object |
222
+ | `.column('<physical>')` | one column's physical name. Name it **last** in the chain — that link returns the general column, so a builder's own methods (`defaultNow()`, a uuid key's narrowed `primaryKey()`) come first. `uuid()` and `timestamp()` override it to keep theirs |
223
+ | `money({ columns: { minor, currency, scale } })` | where the money columns already live, merged **per part** over the `<name>_minor` / `<name>_currency` / `<name>_scale` defaults. `scale: null` says the table has no scale column, which means every amount is at the currency's own minor unit |
224
+
225
+ Without an override a column is `snake(property)` and the table is the entity name.
226
+
227
+ ### The type map
228
+
229
+ Two vocabularies. `columns.ts` holds the **opinionated** builders — one way to store an id, an instant, money — each a decision the framework made for a table it was going to create. `columns-data.ts` holds the shapes a table **already has**. Both export from `@ultimat3/entity`.
230
+
231
+ | Legacy Postgres type | Declare as | Row type | `As of 2026-08` |
232
+ |---|---|---|---|
233
+ | `uuid` | `uuid()` | `string` | shipped |
234
+ | `text`, `varchar(n)` | `text()` / `text({ max: n })` | `string` | shipped. The `CHECK` is `char_length(col) <= n`, not a `varchar` cast |
235
+ | `integer`, `serial` | `integer()` | `number` | shipped |
236
+ | `boolean` | `boolean()` | `boolean` | shipped |
237
+ | `timestamptz` | `timestamp()` | `Date` | shipped |
238
+ | `text` with a fixed value set | `enumerated([...])` | the union | shipped — emitted as a `CHECK`, never `CREATE TYPE`, so adding a variant is a one-line migration instead of an `ALTER TYPE` that cannot run inside a transaction on older servers |
239
+ | a URL column | `url()` | `string` | shipped, validated on write |
240
+ | an IANA zone column | `tz([...])` | the union | shipped |
241
+ | a BCP-47 locale column | `locale([...])` | the union | shipped |
242
+ | `bigint` + `char(3)` (+ `integer` scale) amount | `money({ columns })` | `MoneyValue` | shipped |
243
+ | `jsonb` / `json` | `json(schema)` | the schema's inferred type | shipped. The schema is **required** — there is no untyped `json()`, because a column returning `unknown` is the `any` hole this framework forbids, and the value arrives from the database as often as from a caller |
244
+ | `bigint` / `int8` | `bigint()` | **`string`** | shipped. Not a JS `bigint` (`JSON.stringify` throws on one) and not a `number` (it loses digits past 2^53, exactly where a legacy `int8` key or a snowflake id lives). Both driver spellings arrive and leave as one: Bun's `sql` returns `int8` as a string, PGlite as a `bigint` |
245
+ | `numeric` / `decimal` | `decimal({ precision, scale })` | **`string`** | shipped, and deliberately not arithmetic-friendly — the honest thing a driver already does is give you the digits. A value with more decimal places than the column stores is **refused, not rounded**. `precision` and `scale` are declared together, or neither |
246
+ | `date` | `date()` | `PlainDate` | shipped. A calendar date: no time, therefore no zone. A branded ISO string, not a `Date` — binding a `Date` to a `date` parameter fails outright on a server whose client zone has no name Postgres knows |
247
+ | `bytea` | `bytes()` | `Uint8Array` | shipped, normalised — Bun returns a `Buffer`, PGlite a `Uint8Array`, and the two serialise differently |
248
+ | an array column | `arrayOf(column)` | `readonly T[]` | shipped. The element is a column, so its own `$parse` decides every member. `arrayOf(money())` and nested arrays are refused **at declaration** |
249
+ | `timestamp without time zone` | — | — | **no builder, and there will not be one.** A naive timestamp is a bug that only surfaces twice a year |
250
+ | a native Postgres `enum` **type** | `text()` today | `string` | **deferred.** `text()` reads and writes such a column correctly; what is missing is a declaration that knows the type's variants |
251
+ | `vector` (pgvector) | — | — | **deferred.** `postgresVectorStore()` in `@ultimat3/ai` is the retrieval path; a `vector` **column** on an entity is not declarable |
252
+ | `inet`, `cidr`, `tsvector`, `hstore`, `interval`, `citext`, PostGIS | — | — | **no builder**, and none planned |
253
+
254
+ ### 1.3 There is no schema-to-entity generator
255
+
256
+ No command turns a live schema into `entity()` declarations `As of 2026-08`. `introspectSchema()` from `@ultimat3/db` is public and gives the catalog as deterministically ordered, JSON-safe output — the same reader **drift detection** uses. It has exactly that one framework consumer: the `/_x` schema panel and the MCP `schema.describe` tool both answer from the **entity registry**, never from the live catalog, so what they show is what the app declared and not what the database holds.
257
+
258
+ ```ts
259
+ import { introspectSchema } from '@ultimat3/db';
260
+
261
+ const live = await introspectSchema();
262
+ // { tables: [{ schema, name, columns, primaryKey, indexes, foreignKeys }] }
263
+ ```
264
+
265
+ Use it as the input; write the entities.
266
+
267
+ **App tables only, `As of 2026-08-24`.** `introspectSchema()` excludes every relation Postgres records as extension-owned (`pg_depend`, `deptype = 'e'`) and everything that is not an ordinary or partitioned table — views, materialised views and foreign tables. `create extension pg_stat_statements` in `public` is the CNPG, RDS, Supabase and Neon default, and its view used to read as `unexpected-table` with `x db gen "add pg_stat_statements"` as the fix, so every deploy failed terminally and the fix would have written an extension's internal view into the app's migration set. Ownership rather than a name: a `pg_*` prefix rule covers that view and misses PostGIS's `spatial_ref_sys`. A table someone created **by hand** carries no such dependency and is still `unexpected-table`, which is the finding this phase is about.
268
+
269
+ ---
270
+
271
+ ## Phase 2 — Reach green
272
+
273
+ **Entry:** Phase 1 exits clean.
274
+ **Exit:** `x db migrate --json` → `.ok` **and** `x verify --json` → `.ok`.
275
+
276
+ ```bash
277
+ x db gen "adopt <slice>" --json # entities diffed against what the migrations declare
278
+ x db migrate --json # apply, then the live post-migrate drift check
279
+ x verify --json # the gate
280
+ ```
281
+
282
+ ### What drift compares
283
+
284
+ `x db migrate`, `x db reset` and `ROLE=migrate` run a post-migrate check of the **live catalog** against what the applied migrations' snapshots declare. It filters out framework bookkeeping — every table whose name starts with `x_` — and nothing else.
285
+
286
+ | Live | Reported | Meaning |
287
+ |---|---|---|
288
+ | a table no migration declares | `unexpected-table` | a legacy table not modelled |
289
+ | a column on a modelled table that no snapshot names | `unexpected-column` | a column left out of the entity |
290
+ | a nullability disagreement | `changed-column` | the entity says `NOT NULL` and the table does not, or the reverse |
291
+ | a **type** disagreement | *nothing* | types are deliberately not compared — the catalog and a snapshot spell them differently often enough that comparing them would report drift on a correct database |
292
+ | an index no snapshot names | *nothing* | only the declared side is judged. A DBA's index is a planner decision, not a divergence |
293
+
294
+ `x verify`'s `drift` step is a **different** check: it compares the entity **source** against the hash sidecar committed beside each migration and opens no database. Two conditions, two detectors, one `X_DB_DRIFT`.
295
+
296
+ ### Nothing baselines a pre-existing schema
297
+
298
+ No command snapshots what is already there as the starting point migrations are measured against. Two paths, pre-decided:
299
+
300
+ | Path | Take it |
301
+ |---|---|
302
+ | **Model every table and every column** | by default. It is the only path that ends with `x db migrate` green against the real database |
303
+ | **Give Ultimate its own database** | only when modelling the legacy schema is a bigger project than the migration. Drift is clean immediately; cross-database work becomes entirely yours, and `db()` holds one connection ([One database, globally](#one-database-globally)) |
304
+
305
+ A separate Postgres **schema** is still not a third path `As of 2026-08`, for one reason rather than the two this page carried until now.
306
+
307
+ **The `search_path` itself now works.** `connectionUrl` **merges** the operator's libpq `options` instead of assigning over them, on every role, so a `DATABASE_URL` carrying `?options=-c search_path=app` survives everywhere. The framework wins only on the setting it names — the role's `statement_timeout` bound — and every other `-c` an operator wrote is theirs.
308
+
309
+ **The drift check does not follow it.** `introspectSchema()` defaults `schema` to `'public'`, `checkDrift()` forwards a `schema` option only when it is given one, and `runMigrations` calls `checkDrift({ migrations })` with none. So the post-migrate check reads `public` whatever the session's `search_path` is: it finds none of the app's tables, and reports **every declared table as `missing-table`**, each with a `create table` repair — unqualified, so under that `search_path` it lands in the app's own schema, where the table already exists, and fails with `42P07` rather than creating copies in `public`. There is no `--schema` flag on `x db` to close it with.
310
+
311
+ ### Branch table
312
+
313
+ | `.findings[].code` | `meta.kind` | Action |
314
+ |---|---|---|
315
+ | `X_DB_DRIFT` | `unexpected-table` | add an `entity()` for the table `cause` names, then re-run `x db gen` |
316
+ | `X_DB_DRIFT` | `unexpected-column` | add the column to that entity — `.column('<physical>')` when the property name differs — then re-run `x db gen` |
317
+ | `X_DB_DRIFT` | `changed-column` | the `fix:` is one command — `psql "$DATABASE_URL" -c 'alter table … alter column … set not null;'` (or `drop not null`) — run against that database, then `x db migrate` re-checks |
318
+ | `X_DB_DRIFT` | `unknown-schema` | the newest applied migration wrote no snapshot. The `fix:` names both remedies, in order |
319
+ | `X_MIGRATION_SNAPSHOT_MISSING` | — | the same condition from the other side. Take the `fix:` verbatim |
320
+ | `X_MIGRATION_DESTRUCTIVE` | — | **⛔ STOP.** A committed `up` drops, truncates or retypes. Do not add `-- destructive: true` and do not pass `--allow-destructive` |
321
+ | `X_TENANCY_UNSCOPED` | — | the entity has a tenant column and the query carries no org predicate. Scope the read |
322
+ | `X_NOT_IN_APP` | — | no `app.config.ts` at or above the cwd |
323
+ | anything else | — | `x errors explain <CODE> --json` → run `.data.fix` → re-run the step |
324
+
325
+ ---
326
+
327
+ ## Phase 3 — Bridge identity
328
+
329
+ **Entry:** Phase 2 exits clean.
330
+ **Exit:** a request carrying a legacy session cookie resolves to an `Actor` with the right `id`, `orgId` and `roles`.
331
+
332
+ Ultimate re-reads the user row on **every request** and trusts no claim from a token. A revoked user is revoked on the next request, with no stale-claims window — and that is why the bridge is app code rather than a config line.
333
+
334
+ ### Accept the legacy session
335
+
336
+ `configureAuthenticator()` is the seam. Ultimate ships **no** general-purpose JWT verifier: `verifyIdToken` is OIDC-shaped and wants an issuer, a client id, a nonce and a JWKS key source, which a bespoke app JWT has none of.
337
+
338
+ ```ts
339
+ import { userActor } from '@ultimat3/core';
340
+ import { configureAuthenticator } from '@ultimat3/http';
341
+
342
+ /** Your own verifier, over the legacy app's signing key. */
343
+ declare function verifyLegacyJwt(token: string): Promise<{
344
+ sub: string;
345
+ org: string;
346
+ roles: readonly string[];
347
+ } | null>;
348
+
349
+ configureAuthenticator(async (request) => {
350
+ const cookie = request.header('cookie') ?? '';
351
+ const token = /(?:^|;\s*)legacy_session=([^;]+)/.exec(cookie)?.[1];
352
+ if (token === undefined) return null;
353
+ const claims = await verifyLegacyJwt(token);
354
+ if (claims === null) return null;
355
+ return userActor({ id: claims.sub, orgId: claims.org, roles: [...claims.roles] });
356
+ });
357
+ ```
358
+
359
+ | Rule | Detail |
360
+ |---|---|
361
+ | `null` means anonymous | not an error |
362
+ | **One** authenticator per process | two functions answering "who is this?" is two identities per request |
363
+ | Call it at module scope under `apps/*/` | a process with routes declaring `auth: 'required'` and no authenticator refuses every session, and `x dev` says so with an `X_CONFIG_INVALID` line naming the fix |
364
+
365
+ ### Passwords: rehash on login
366
+
367
+ `verifyPassword` calls `Bun.password.verify`, which reads the algorithm out of the PHC string.
368
+
369
+ | Stored hash | Behaviour |
370
+ |---|---|
371
+ | `$argon2id$…` at weaker parameters than the policy | verifies; `needsRehash` is `true`; `login()` rewrites it at the current parameters while it still holds the plaintext |
372
+ | `$2a$` / `$2b$` / `$2y$` **bcrypt** | verifies; `needsRehash` is `true` (the PHC prefix is unrecognised, which the rule treats as "upgrade it"); rewritten as argon2id on that first successful login |
373
+ | PBKDF2, scrypt, SHA-family, MD5, bespoke, or a truncated hash | **does not verify** — it is the one generic credential failure, indistinguishable from a wrong password. Not a migration path: nothing rewrites the row |
374
+
375
+ **An unreadable hash is safe, and it is silent — both matter here.** `Bun.password.verify` *throws* on a hash it cannot parse (measured on bun 1.3.14: a Django `pbkdf2_sha256$…` row is `UnsupportedAlgorithm`, a truncated bcrypt string is `InvalidEncoding`). `verifyPassword` catches that and routes it through the **same** branch as an unknown user — burning the same full KDF, so neither the answer nor the response time separates it from a wrong password ([`packages/auth/src/password.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/auth/src/password.ts)). `X_OVERLOADED` from the KDF gate is re-thrown rather than swallowed: load shedding is not a verdict on the credential.
376
+
377
+ Three consequences you plan around:
378
+
379
+ | Consequence | What it means during a migration |
380
+ |---|---|
381
+ | **It consumes lockout budget** | a failed verification is a failed verification: the attempt `login()` reserved before the KDF stays counted on the account, the IP and the org bucket alike ([`packages/auth/src/auth.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/auth/src/auth.ts)). A table full of foreign hashes locks accounts out through ordinary login attempts, at the normal rate |
382
+ | **Nothing is logged**, deliberately | the algorithm of an unreadable hash is the same oracle one layer down, and a line per attempt is a spray amplifier. So you see uniform credential failures and no signal that the cause is the hash scheme rather than the password |
383
+ | **It is not a migration path** | nothing rewrites the row. bcrypt is the only "proceed" case — verified natively, `needsRehash` flags it, and the first successful login rewrites it as argon2id |
384
+
385
+ So a non-bcrypt estate still needs a plan before Phase 3: bridge the legacy login endpoint and keep it until the last user has signed in, or force a reset for the remainder.
386
+
387
+ ### Enterprise SSO
388
+
389
+ OAuth2/OIDC with PKCE, nonce and JWKS signature verification ships, and the provider set is **open** — `registerOAuthProvider()`, or `discoverOAuthProvider({ id, issuer })` to read one out of the issuer's discovery document. Okta, Entra, Ping, Keycloak and an internal gateway are all expressible.
390
+
391
+ **SAML does not ship and will not.** XML-DSig canonicalisation has no Bun native, and implementing it would put a real dependency in the primitive vocabulary. Put an OIDC-speaking SAML bridge in front and register **that**.
392
+
393
+ ---
394
+
395
+ ## Phase 4 — Reads through Ultimate
396
+
397
+ **Entry:** Phase 3 exits clean.
398
+ **Exit:** `x queries --json` lists the slice's reads, `x verify --json` → `.ok`, and the slice's read paths answer from Ultimate for a full traffic cycle with error rate and p95 unchanged.
399
+
400
+ | Step | Postcondition |
401
+ |---|---|
402
+ | declare the slice's `query()`s and `policy`s | `x queries --json` → `.ok`, listing them |
403
+ | write the contract tests | `x verify --json` → the `.steps[]` entry named `contract` has `ok: true` |
404
+ | route the slice's read paths at the proxy | **⛔ STOP** when this is production traffic |
405
+
406
+ Reads move **before** writes, and that ordering is what makes Phase 4 rollback-safe: routing the paths back at the proxy is a complete undo, because nothing was written.
407
+
408
+ ### What Ultimate does not see
409
+
410
+ | Concern | Consequence while both apps run |
411
+ |---|---|
412
+ | Caching | Ultimate's tags are its own. A legacy write it does not know about goes stale until its TTL → [Caching and invalidation](Caching-And-Invalidation) |
413
+ | Realtime | live queries observe Ultimate's own layer. A row the legacy app writes pushes no patch |
414
+ | Background jobs | Ultimate's queue is `x_jobs` plus a transactional outbox. It does not read the legacy queue and cannot |
415
+
416
+ Which is why the unit of migration is a **slice**: move a table's reads and writes together, or accept staleness on the half you did not move.
417
+
418
+ ### A renamed column and a live query — on 21.x only
419
+
420
+ On **21.x**, a **live query** fed by the WAL decoder (a real `DATABASE_URL` and the `replicator`
421
+ role) delivers a renamed column under its **physical** name: the decoder rebuilt an entity row from
422
+ the replication stream's column names alone and never consulted `.column()` overrides.
423
+
424
+ | Read through, on 21.x | `githubLogin: text().column('gh_login')` arrives as |
425
+ |---|---|
426
+ | a repository / `query()` | `githubLogin` |
427
+ | a **live** query patch | `ghLogin` |
428
+
429
+ **Closed in 22.0.0**: `entityRow` in
430
+ [`packages/realtime/src/pg-entity-row.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/realtime/src/pg-entity-row.ts)
431
+ decodes through the registered entity (`decodeRow`, the repository's own reader), so a live patch
432
+ carries `githubLogin` too. **Pinned below 22.0.0, on 21.x:** do not make a slice with renamed
433
+ columns your first live slice, or have the subscriber map the physical name itself.
434
+
435
+ ---
436
+
437
+ ## Phase 5 — Writes through Ultimate
438
+
439
+ **Entry:** Phase 4 exits clean and has held for a full traffic cycle.
440
+ **Exit:** the legacy write path for this slice is dead code, not a fallback.
441
+
442
+ | Step | Postcondition |
443
+ |---|---|
444
+ | declare the slice's `action()`s and `mutator()`s | `x actions --json` → `.ok`, listing them |
445
+ | `x verify --json` | `.ok` |
446
+ | route the slice's write paths | **⛔ STOP** when this is production traffic |
447
+
448
+ **Expand before you contract.** New columns nullable, the old column still written for one release, no drops. That is what keeps the Phase 4 rollback available after writes move: routing back only works if Ultimate wrote nothing the legacy app cannot read.
449
+
450
+ ---
451
+
452
+ ## Phase 6 — Reshape the data
453
+
454
+ **Entry:** Phase 5 exits clean.
455
+ **Exit:** `x db backfill --pending --json` → `.ok` (it exits non-zero while anything declared is unswept).
456
+
457
+ `backfill()` is a factory over `job()` — a one-pass sweep declared as durable background work. It resumes, it paces, and it refuses to run in an environment it was not declared for.
458
+
459
+ ```ts
460
+ import { backfill } from '@ultimat3/jobs';
461
+ import { db, referenceFor } from './schema';
462
+
463
+ export const normaliseOrderRefs = backfill({
464
+ name: 'normalise-order-refs',
465
+ tenant: 'none',
466
+ requires: '20260814120000_add_order_ref', // the migration this sweep needs applied first
467
+ environments: ['staging', 'production'],
468
+ batch: 1_000,
469
+ rate: 2, // batches per second
470
+ source: () => db.orders.where({ reference: null }),
471
+ count: () => db.orders.where({ reference: null }).count(),
472
+ handle: async ({ rows }) => {
473
+ // Idempotent: the second run writes the same value, so a replayed page changes nothing.
474
+ await db.orders.upsertAll(
475
+ rows.map((row) => ({ ...row, reference: referenceFor(row.id) })),
476
+ { onConflict: ['id'], onMatch: 'update' },
477
+ );
478
+ },
479
+ });
480
+ ```
481
+
482
+ ```bash
483
+ x db backfill <name> --json # dry run — reports the plan, writes nothing
484
+ x db backfill <name> --write --json # enqueue the pass
485
+ x db backfill --list --json # the x_backfills ledger
486
+ x db backfill --pending --json # declared minus completed; non-zero exit when unswept
487
+ ```
488
+
489
+ | Property | Why it matters here |
490
+ |---|---|
491
+ | Every page is its own `step.run` | a killed attempt resumes on the page it stopped at. What a step persists is a **cursor**, never the page |
492
+ | `handle` is **at least once** | it runs before its checkpoint lands, so an attempt cancelled between the two replays that page. Write with `upsertAll`, `updateWhere`, or a statement whose second run changes nothing — **never `count + 1`** |
493
+ | Pacing is mandatory | `rate` defaults to 5 batches/second and there is **no unthrottled mode**: this pass shares its pool with the requests the app is still serving |
494
+ | `environments` is a gate | a mismatch is `X_BACKFILL_ENVIRONMENT`, refused inside the pass as well as by the CLI, because a backfill enqueued by app code never passes through a command |
495
+ | `requires` names a migration | checked against `x_migrations`, so a sweep cannot run against a schema that has not caught up |
496
+ | `count` makes convergence arithmetic | a pass whose source is exhausted while `count` still answers above zero has two predicates that disagree — `X_BACKFILL_STALLED`, an authoring bug |
497
+ | The ledger is append-only | a rerun is a **new** row; history is never overwritten. The idempotency key is the name, so a second enqueue while it runs is the same pass |
498
+
499
+ | `.findings[].code` | Action |
500
+ |---|---|
501
+ | `X_BACKFILL_ENVIRONMENT` | the sweep is not declared for this environment. Add it to `environments:`, or run it where it is declared |
502
+ | `X_BACKFILL_STALLED` | `source` and `count` disagree. Fix the predicate; do not re-run |
503
+ | `X_ABORTED` | the attempt was cancelled. Re-enqueue the same name — it resumes at the last checkpoint |
504
+
505
+ For **reference data** the new app is wrong without — currencies, plans, tiers, locations — use `x db seed` instead. A seed is replayable by construction and declares a tier: `reference` ships to production, `dev` does not reach it without `--tier dev`.
506
+
507
+ ```bash
508
+ x db seed --dry-run --json # lists every declared seed, writes nothing
509
+ x db seed <name> --json
510
+ ```
511
+
512
+ Full detail: [Migrations and backfills](Migrations-And-Backfills).
513
+
514
+ ---
515
+
516
+ ## Phase 7 — Retire the legacy slice
517
+
518
+ **⛔ STOP. Every step of this phase is a human's.**
519
+
520
+ | Step | Why it is a stop |
521
+ |---|---|
522
+ | Delete the legacy slice's source | the rollback path dies with it |
523
+ | Drop the legacy columns or tables | irreversible. `x db gen` refuses without `--allow-destructive`, and that refusal is the guard |
524
+ | Delete migrated source data | irreversible at any scale |
525
+ | Retire the legacy login endpoint | strands every user still holding a legacy cookie |
526
+
527
+ Report to the human: which slices are complete, `x verify --json`'s result, `x db backfill --pending --json`'s result, and the exact tables and columns that would be dropped.
528
+
529
+ ---
530
+
531
+ ## Reference
532
+
533
+ ### Cutover topologies
534
+
535
+ | Topology | Supported `As of 2026-08` |
536
+ |---|---|
537
+ | **Strangler fig, one shared database** | **the supported path.** Everything above assumes it |
538
+ | **Strangler fig, two databases** | **partially.** `db()` is one global handle, so Ultimate holds exactly one of the two. The reconciliation is entirely yours |
539
+ | **Read replica first** | **no, and that is unchanged by the read/write split that now ships.** `DATABASE_REPLICA_URL` + `withReplicaReads` route *plain reads* off a primary the app still holds; pointing `DATABASE_URL` itself at a replica fails every write, the migration ledger's included. A replica is a capacity tier, never a cutover step |
540
+ | **Big bang** | technically possible, and the one that fails. Not documented here |
541
+
542
+ ### One database, globally
543
+
544
+ `db()` is a module-level singleton in [`packages/db/src/client.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/client.ts): one ambient client, built lazily from `DATABASE_URL`, sized by the runtime `ROLE`. `setDbClient()` replaces it and does not add a second.
545
+
546
+ | Consequence | Detail |
547
+ |---|---|
548
+ | Ultimate holds exactly **one** database | a second is reachable only through your own client — outside `db()`, outside transactions, outside the observer seam, outside N+1 detection |
549
+ | Inside `withTransaction`, `db()` **is** the transaction | which is what lets a repository written against `db()` join the caller's transaction without knowing it exists |
550
+ | The pool is sized per role | `web: 20`, `sync: 10`, `worker: 8`, `scheduler: 2`, `migrate: 1`, `replicator: 4`. `DATABASE_POOL_MAX` is the one knob an operator turns without a rebuild — 400 `web` pods at 20 is 8,000 backends against a `max_connections` of 450 |
551
+
552
+ ### What does not work yet
553
+
554
+ Verified against the code, `As of 2026-08`.
555
+
556
+ | Gap | Detail | Do this instead |
557
+ |---|---|---|
558
+ | No schema baseline | every unmodelled table is `unexpected-table`, every unmodelled column is `unexpected-column`; no command snapshots the pre-existing schema as a starting point | model everything, or give Ultimate its own database |
559
+ | No schema-to-entity generator | no command turns a live schema into `entity()` declarations. `introspectSchema()` is public and gives the catalog as JSON | write the entities from `introspectSchema()`'s output |
560
+ | Three column shapes have no builder | a naive `timestamp without time zone` (permanently), a native Postgres `enum` **type**, and `vector` | convert a naive timestamp to `timestamptz`; declare an enum column as `text()`; keep vectors in `postgresVectorStore()` |
561
+ | A renamed column arrives under its **physical** name in a live query | `@ultimat3/realtime` rebuilds rows from physical names and cannot read `.column()` overrides across the tier boundary | do not make a renamed slice live first, or map the name in the subscriber |
562
+ | One database globally | `db()` is a singleton | one Postgres, or a second client outside the framework |
563
+ | ~~No read/write split~~ | **shipped `As of 2026-08-24`** — `DATABASE_REPLICA_URL` builds a primary + replica client and `withReplicaReads(fn)` is the scope inside which a plain read may leave the primary, with read-your-writes and a 3-failure/10s breaker back to it | opt in twice ([Configuration → Read replicas](Configuration#read-replicas)); the scope is the app's to open |
564
+ | No legacy-hash verifier beyond bcrypt/argon2 | an unreadable hash is the generic credential failure — safe, silent, and it consumes lockout budget — but nothing rewrites the row | bridge the legacy login, or force a reset |
565
+ | No SAML | permanently out of scope | an OIDC bridge in front |
566
+ | No app-contributed raw `Route` | the HTTP surface is composed from actions, queries, assets and page routes; `configureAuthenticator()` is the only app-installed hook of that shape | express the endpoint as an `action` or a `route` |
567
+ | Cache and realtime do not see legacy writes | Ultimate's tags and live queries observe its own layer | move a slice's reads and writes together |
568
+
569
+ ### Rollback
570
+
571
+ Available at every phase up to 7, by routing the slice's paths back to the legacy app at the proxy.
572
+
573
+ | Risk | Mitigation |
574
+ |---|---|
575
+ | A migration dropped or retyped a column | `x db gen` needs `--allow-destructive` to emit one, and a committed `up` that destroys must carry `-- destructive: true` or the gate refuses it. Take the refusal as the answer |
576
+ | Ultimate wrote a column the legacy app cannot parse | add columns nullable, expand before you contract, keep the old column written for one release |
577
+ | A backfill half-ran | it resumes; it does not restart. Re-enqueue the same name |
578
+ | Sessions were reissued as Ultimate sessions | keep the legacy cookie valid until the slice is permanent |
579
+
580
+ ## Rules
581
+
582
+ - Strangler fig on one shared database. Move a slice's reads and writes together.
583
+ - Every step has a machine-checkable postcondition: `--json`, then `.ok`; on failure, `.findings[].code`.
584
+ - Branch on error codes by name. An unrecognised one is `x errors explain <CODE> --json`, then `.data.fix`.
585
+ - One owner for DDL, decided by a human before Phase 1.
586
+ - Audit money before writing an entity: `select max(abs(<col>))` against 9,007,199,254,740,991, and confirm a currency column beside every amount.
587
+ - An amount with no currency column is `decimal()` plus your own column, never `money()`.
588
+ - Declare every table and every column, or drift is red on every `x db migrate`.
589
+ - Physical names are overrides: `entity(name, { table })`, `.column('<physical>')`, `money({ columns })`. The entity **name** stays the framework's key.
590
+ - A live query delivers a renamed column under its physical name.
591
+ - Bcrypt migrates itself on first login. Nothing else does — a foreign hash fails safely, silently, and forever.
592
+ - Reshape with `backfill()`: paced, resumable, at least once, so every `handle` must be idempotent.
593
+ - Reads move before writes, so rollback stays a proxy change.
594
+ - Anything irreversible or outward-facing is a ⛔ STOP. Halt and report.
@@ -0,0 +1,54 @@
1
+ **Status: `not executed end to end`.** No Django app has been cut over by following this page yet; it flips to `executed` on evidence from an adopter's measured run.
2
+
3
+ # Migrating from Django
4
+
5
+ The concept map for a Django app. The runbook — phases, exit conditions, stop conditions — is
6
+ [Migrating an existing app](Migrating-An-Existing-App); this page says what each Django term
7
+ becomes. `As of 2026-11`, against Django 6 ([docs.djangoproject.com](https://docs.djangoproject.com/)).
8
+
9
+ ## Concept map
10
+
11
+ | Django | Ultimate |
12
+ |---|---|
13
+ | `urls.py` + a view that renders a template | a route file: [Routes and render modes](Routes-And-Render-Modes) |
14
+ | a view that writes, a DRF viewset | an `action`: [Actions](Actions) |
15
+ | a model | an `entity()`: [Entities and migrations](Entities-And-Migrations) |
16
+ | a queryset, a manager method | a `query`: [Queries and live queries](Queries-And-Live-Queries) |
17
+ | `makemigrations`, `migrate` | `x db gen "<name>"`, then `x db migrate` |
18
+ | `startapp` | `x g resource <name>` ([CLI reference](CLI-Reference)) |
19
+ | Celery tasks | a `job`: [Jobs and workflows](Jobs-And-Workflows) |
20
+ | Celery beat | a `task`: [Scheduled tasks](Scheduled-Tasks) |
21
+ | `django.contrib.auth` | [Auth](Auth) |
22
+ | permissions, `django-guardian` | a `policy`: [Policies and authz](Policies-And-Authz) |
23
+ | Channels | a live query: [Realtime](Realtime) |
24
+ | `send_mail` | [Mail](Mail) |
25
+ | `FileField`, `django-storages` | [Storage and uploads](Storage-And-Uploads) |
26
+ | `django.contrib.admin` | [Admin dashboard](Admin-Dashboard) |
27
+ | `gettext` `.po` files | one flat catalog per locale: [I18n](I18n) |
28
+ | DRF's schema, drf-spectacular | `openapi.json`, projected from every action: [Actions](Actions) |
29
+ | `pytest-django`, `TestCase` | [Testing](Testing) |
30
+ | `runserver` | `x dev` |
31
+
32
+ ## What does not map
33
+
34
+ | Django | Why |
35
+ |---|---|
36
+ | Python and its packages | Bun only. A Python dependency is rewritten or bought as a service |
37
+ | MySQL, SQLite, Oracle | Postgres only, by design: [Home](Home) |
38
+ | templates | pages render with SolidJS; interactive parts are islands. No converter |
39
+ | signals (`post_save`) | no implicit hooks: the write is an `action` or a `mutator` that says what it does ([Actions](Actions)) |
40
+ | generic relations (`ContentType`) | model each target as its own foreign key ([Entities and migrations](Entities-And-Migrations)) |
41
+
42
+ ## Schema adoption
43
+
44
+ Django names tables `<app>_<model>` and ids are `bigint` (`DEFAULT_AUTO_FIELD`). Phase 1 of the
45
+ runbook declares the existing tables with the physical-name overrides it gives, then `x db gen`
46
+ until `x verify --only drift` agrees: [Migrating an existing app](Migrating-An-Existing-App).
47
+
48
+ ## Identity cutover
49
+
50
+ | Django stores | Branch |
51
+ |---|---|
52
+ | `auth_user.password` as `pbkdf2_sha256$…` — Django's default hasher | **⛔ stop**: no PBKDF2 verifier ships, and a row in that scheme never verifies. Keep Django's login as a bridge until every user has signed in once, or force a reset ([Migrating an existing app](Migrating-An-Existing-App)) |
53
+ | `bcrypt_sha256$…` | **⛔ stop** for the same reason: the prefix is Django's, not a PHC string |
54
+ | the session: `django_session` rows keyed by a cookie | bridge it with your own lookup behind `configureAuthenticator()` ([Auth](Auth)) |