@voltro/cli 0.33.0 → 0.35.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 (343) hide show
  1. package/CHANGELOG.md +1968 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -17,7 +17,7 @@ Voltro's migrator does not generate or apply migration files. Instead, on every
17
17
  2. **Diffs** that against your declared schema (every `*.entity.ts` / `*.schema.ts` file in the project plus the framework's bookkeeping tables).
18
18
  3. **Classifies** each pending DDL op into one of seven `OperationClass`es — `safe`, `needs-default`, `needs-backfill`, `needs-rename-annotation`, `lossy`, `online-required`, `multi-step`.
19
19
  4. **Refuses** to apply anything that can't be made safe automatically. The diff output tells you exactly which DSL annotation to add (`.backfill()`, `.renamedFrom()`, `dropped()`, …).
20
- 5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema.
20
+ 5. **Applies** the rest under an advisory lock + records the result in `_voltro_migration_plans` with a fingerprint of the post-apply schema. On a dialect that cannot apply the whole plan in one transaction (MySQL / MariaDB / SQLite / Turso), each operation is also recorded in a **resume ledger** (`_voltro_migration_ops`) as it lands, so a crashed apply is continued rather than re-planned blind — see [multi-dialect](./multi-dialect.md).
21
21
 
22
22
  There are no generated SQL files to commit, no `migrations/` directory to rebase, no checksum manifest to repair. The source of truth is your schema TypeScript; the DB is the slave.
23
23
 
@@ -862,28 +862,36 @@ completed rows.
862
862
  <!-- source: en/database/migrations/multi-dialect.md -->
863
863
  ## Multi-dialect strategy
864
864
 
865
- _How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite — the atomicity matrix, MySQL's implicit-commit landmine + re-diff-to-recover, SQLite's table-rewrite mechanic, plus the per-dialect DDL idioms the framework hides._
865
+ _How the planner + applier behave across Postgres, MySQL, MariaDB, MSSQL, SQLite, Turso — the atomicity matrix, the resume ledger that carries a crashed apply on the non-transactional dialects, SQLite's table-rewrite mechanic, plus the per-dialect DDL idioms the framework hides._
866
866
 
867
867
  The planner produces ONE `MigrationPlan` regardless of dialect. The applier executes it per-dialect, dispatching through `sql.onDialectOrElse` for every emit + falling back to runtime probes when behaviour diverges. The same `voltro db apply` invocation against the same schema produces structurally identical results on every backend.
868
868
 
869
869
  What ISN'T uniform: **transactional DDL semantics**.
870
870
 
871
- | Dialect | Transactional DDL | Advisory-lock mechanism | Backfill in tx with DDL |
871
+ | Dialect | Plan applied atomically | Advisory-lock mechanism | Recovery after a mid-plan crash |
872
872
  |---|---|---|---|
873
- | Postgres | ✓ all DDL atomic | `pg_advisory_lock(KEY)` | |
874
- | MSSQL | ✓ all DDL atomic | `sp_getapplock` | |
875
- | SQLite | all DDL atomic | process-local mutex | |
876
- | **MySQL / MariaDB** | **✗ implicit commit per DDL** | `GET_LOCK('voltro_migration', N)` | but SEPARATE from DDL |
873
+ | Postgres | ✓ one transaction | `pg_advisory_lock(KEY)` | nothing to recover — rolled back |
874
+ | MSSQL | ✓ one transaction | `sp_getapplock` | nothing to recover — rolled back |
875
+ | **MySQL / MariaDB** | **✗ implicit commit per DDL** | `GET_LOCK('voltro_migration', N)` | resume ledger |
876
+ | **SQLite / Turso** | **✗ per statement** | process-local mutex | resume ledger |
877
877
 
878
878
  This is the operationally heaviest cross-dialect difference. The rest of the page covers what changes.
879
879
 
880
- ## Postgres / MSSQL / SQLite transactional happy path
880
+ **SQLite is on the non-atomic side, and the reason is Turso.** SQLite the engine *can* do transactional DDL. The applier does not use it, because SQLite and Turso share one dialect token and Turso rejects DDL inside its default transaction — the applier cannot wrap one without wrapping the other. Both therefore take the per-statement path and the resume ledger below.
881
+
882
+ **On Postgres one class of operation is still not covered by the transaction:** `online-required` ops (`CREATE INDEX CONCURRENTLY`, the shadow-column type swap) are *rejected* inside a transaction, so they run after the commit. They are ledgered like a MySQL plan.
883
+
884
+ ## Postgres / MSSQL — transactional happy path
881
885
 
882
886
  A multi-step plan runs inside one `BEGIN ... COMMIT`. Mid-flight failure rolls EVERYTHING back; the next plan diff is identical to the pre-apply one. There's nothing to resume — re-running the apply re-runs the plan from scratch.
883
887
 
884
888
  The advisory-lock variants serialise concurrent applies — two operators running `voltro db apply` against the same DB at the same time go through serially.
885
889
 
886
- ## MySQL / MariaDBre-diff-to-recover territory
890
+ **The lock is scoped to your configured schema.** With `DB_SCHEMA` set, the lock key (Postgres) / lock name (MySQL, MSSQL) is derived from the schema, so two apps sharing one database in different schemas do not serialise or defer — each other's migrations and trigger repairs. Without `DB_SCHEMA` (or with `DB_SCHEMA=public`) every instance takes one stable framework-wide key, which is what makes a rolling deploy safe: old and new replicas contend on the same lock. On MySQL/MariaDB `GET_LOCK` is server-wide; setting `DB_SCHEMA` to your database name un-shares the lock between two apps on one server.
891
+
892
+ **Transient DDL failures are retried, per the dialect's own predicate.** A `CREATE TABLE IF NOT EXISTS` that meets SQLite/Turso's schema lock (`database is locked` / `SQLITE_BUSY`), a MySQL lock-wait timeout, or a Postgres/MSSQL deadlock victim during the boot auto-migrate is retried with bounded attempts and exponential backoff instead of failing the boot on the first attempt — only statements that are safe to re-run, and on Postgres/MSSQL as a fresh transaction (their deadlock classes roll the whole transaction back). `VOLTRO_MIGRATION_DDL_RETRIES` moves the retry count (default 4; `0` disables).
893
+
894
+ ## MySQL / MariaDB / SQLite / Turso — the resume ledger
887
895
 
888
896
  Every DDL statement implicitly commits. A 5-op plan on MySQL is effectively 5 separate "atomic statements" with the prior ones already committed when a later one fails. If op 5 of 5 fails, ops 1–4 stay applied:
889
897
 
@@ -896,13 +904,36 @@ plan applying (mysql, env=prod):
896
904
  5. add-index audit_logs(actorId) ✗ ER_DUP_KEYNAME
897
905
  ```
898
906
 
899
- There is NO `--resume` / `--abort` flag and NO per-op partial-status rowthe applier records a `_voltro_migration_plans` row only on a fully successful apply. Recovery is to re-run the apply once the cause is fixed:
907
+ The `_voltro_migration_plans` row is still written only on a fully successful applythat row means "this schema is live", and a half-applied plan is not. What the applier DOES write as it goes is a per-operation **resume ledger**, `_voltro_migration_ops`:
908
+
909
+ - every operation that will run outside a transaction is inserted `pending` **before any DDL runs**, so a crash on operation 1 still leaves the whole intended sequence on disk;
910
+ - each row flips to `started` immediately before its statement and `applied` immediately after;
911
+ - the rows are **deleted** once the apply converges and the plan row lands. The ledger is a work queue, not a history — the history is `_voltro_migration_plans.operations`.
912
+
913
+ There is still no `--resume` / `--abort` flag, because there is nothing to choose. Recovery is to re-run the apply once the cause is fixed:
900
914
 
901
915
  ```sh
902
916
  voltro db apply
903
917
  ```
904
918
 
905
- `voltro db apply` re-introspects the live DB and diffs the declared schema against the (half-applied) live shape, so it emits ONLY the ops still missing — ops 1–4 are already in the DB and don't reappear in the diff. If someone finished op 5 out of band (psql, a corrective hot-fix), the re-diff sees it as present and skips it too. If the failed op is no longer the right answer because the schema was edited in response, the next `voltro db plan` already reflects the new declared shape — nothing to abort.
919
+ The next apply finds the ledger, says so in the log, and continues the interrupted run:
920
+
921
+ ```
922
+ [voltro:migrate] migration resume: found an interrupted run (plan_msocz71h_x0qq5d) — 4 of 5 operation(s) completed, in flight: add-index index:audit_logs.audit_logs_actorId_idx
923
+ [voltro:migrate] migration resume: continuing run plan_msocz71h_x0qq5d — replaying 1 operation(s), 4 already applied
924
+ ```
925
+
926
+ Three things are worth knowing about how it decides.
927
+
928
+ **The ledger cannot be atomic with the DDL it records** — on MySQL the statement commits itself, so there is always a window where the DDL landed and the `applied` flip did not. That window is not eliminated, it is BOUNDED: the ledger is written strictly sequentially, so **at most one operation can be `started`**, and it is the only one whose outcome is unknown. That one is resolved by asking the planner — the fresh diff was computed against the live database moments ago, so an operation it no longer mentions has already taken effect. Completed operations are never re-attempted.
929
+
930
+ **If you edited the schema in response to the failure, the recorded plan is dropped** and the freshly-diffed one is applied instead — the old plan aims at a target nobody wants any more. The interruption is still logged, and the artefacts below are still reconciled first.
931
+
932
+ **Two operations are repaired rather than re-diffed.** The Postgres online type change (shadow-column swap) and the SQLite table rebuild both build a temporary object and swap it into place, and interrupted mid-swap they leave a live schema that means something ELSE to a differ — a half-finished shadow swap looks like a *missing column*, and the plan a blind re-diff produces for that is `add-column`, which succeeds and loses the data sitting in `<col>__old`. The applier reconciles `<col>__shadow` / `<col>__old` and `<table>__voltro_rebuild` from their observable state before anything else reads the schema. If a shadow-swap state cannot be classified, the apply **refuses**, changes nothing, and names the three columns to inspect.
933
+
934
+ **What has not changed: convergence still gates the fingerprint.** After the DDL — resumed or not — the applier re-plans against the live schema and refuses to record a fingerprint while anything remains. A resumed run is held to exactly the same standard as a fresh one, and an apply that does not converge KEEPS its ledger, because an unfinished run's record is the only thing that tells the next boot it is looking at a half-migrated schema.
935
+
936
+ If someone finished the failed op out of band (a `mysql` shell, a corrective hot-fix), the fresh diff sees it as present and it is skipped.
906
937
 
907
938
  ## SQLite — table rewrite mechanic
908
939
 
@@ -954,7 +985,7 @@ The DDL emitter under `@voltro/database/src/migrate.ts` is one of the densest cr
954
985
  [voltro:dev] auto-migrate: applied 3 op(s) in 412ms [safe=3 needs-default=0 needs-backfill=0 rename=0 lossy=0] fingerprint=8f507ba1e1aadad5
955
986
  ```
956
987
 
957
- For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply to finish):
988
+ For MySQL the line notes the non-atomic-DDL constraint (a failed op leaves earlier ops committed; re-run apply and the resume ledger continues from there):
958
989
 
959
990
  ```
960
991
  [voltro:dev] auto-migrate: planning schema dialect=mysql env=dev tables=22 (implicit-commit DDL — re-run apply after a mid-plan failure)
@@ -1302,6 +1333,36 @@ If `down` throws, the rollback is treated as failed — the schema stays
1302
1333
  in the half-rolled-back state + the operator handles it manually. The
1303
1334
  framework can't auto-recover from a broken inverse.
1304
1335
 
1336
+ ## `voltro serve` refuses to boot while any are pending
1337
+
1338
+ Serve's schema guard is a DECLARATIVE fingerprint diff — the declared schema
1339
+ against the last applied plan. A file-based migration exists for the changes a
1340
+ state diff cannot infer: a data move, a backfill, a cross-table rewrite. Those
1341
+ move **no fingerprint at all**, so the guard passed and production ran
1342
+ un-migrated with nothing said.
1343
+
1344
+ On a real deploy environment (`NODE_ENV=production` / `staging`), against a SQL
1345
+ store, `voltro serve` now refuses to boot while any migration file has never run
1346
+ against that database, and names the pending ids:
1347
+
1348
+ ```text
1349
+ serve: refusing to boot — 2 pending file-based migration(s) have never run
1350
+ against this database. They perform the changes a schema diff cannot infer
1351
+ (data moves, backfills, table splits), so the declarative fingerprint check
1352
+ below cannot see them.
1353
+
1354
+ Run them from your pre-deploy job — `voltro db migrate .` (schema + files)
1355
+ or `voltro db files .` (files alone).
1356
+ ```
1357
+
1358
+ It does **not** apply them, and that is deliberate: a rolling deploy starts N
1359
+ replicas, each would try, and the migration lock turns that into N-1 processes
1360
+ blocked on boot. `VOLTRO_AUTO_MIGRATE=0` bypasses this exactly as it already
1361
+ bypassed the fingerprint check — one switch for "no boot-time schema checks".
1362
+
1363
+ A local `voltro serve` is untouched: `voltro dev` applies migrations there, so a
1364
+ preview serve has nothing to report.
1365
+
1305
1366
  ## Remote databases: boot will not apply them unattended
1306
1367
 
1307
1368
  `voltro dev` applies pending migration files at boot. Against a local
@@ -1521,9 +1582,25 @@ The applier:
1521
1582
  1. Introspects the live DB and plans the diff fresh (it does NOT ingest
1522
1583
  a plan file — the diff is computed against live at apply time)
1523
1584
  2. Refuses (exit 2) if any op is blocked, or refuses (exit 3) if
1524
- `NODE_ENV=production` — so the apply runs in a one-shot job with
1525
- `NODE_ENV` unset / `staging`, holding migration credentials, NOT in
1526
- the serving process
1585
+ `NODE_ENV=production` — so a bare apply runs in a one-shot job with
1586
+ `NODE_ENV=staging`, holding migration credentials, NOT in the serving
1587
+ process
1588
+
1589
+ > **`NODE_ENV` unset is no longer "not production".** Every `voltro db …`
1590
+ > and `voltro migrate` invocation resolves an unset `NODE_ENV` to
1591
+ > `production`, exactly as `voltro serve` and `voltro start` do — so a bare
1592
+ > `voltro db apply` in a pipeline that forgot the variable now refuses (exit
1593
+ > 3) instead of silently applying an un-reviewed diff to production. Set
1594
+ > `NODE_ENV=development` for a local database; use the `--plan` path below
1595
+ > for a real one.
1596
+ >
1597
+ > This is not only about the refusal. `_voltro_traces` and `_voltro_undo_log`
1598
+ > are created only when tracing / undo capture are on, and both are *on
1599
+ > unless production* — so an apply with `NODE_ENV` unset used to DECLARE two
1600
+ > tables the serving container did not. The declared set is what the schema
1601
+ > fingerprint hashes, so the apply recorded a fingerprint the container could
1602
+ > not reproduce and `voltro serve` refused to boot with `prod-mismatch`,
1603
+ > telling you to run the apply you had just run.
1527
1604
  3. Acquires the advisory lock + executes the plan
1528
1605
  4. Records the result in `_voltro_migration_plans` with
1529
1606
  `source: 'auto-diff'` + `notes: 'PR #1234 — add user emails'`
@@ -1554,6 +1631,31 @@ up-to-date DB is a clean no-op (`schema is up to date — nothing to
1554
1631
  apply`). That's what makes the apply safe to run in every pod of a
1555
1632
  stateless deploy.
1556
1633
 
1634
+ Both spellings of the flag work: `--plan plan.json` and `--plan=plan.json`.
1635
+
1636
+ ### The FIRST deploy, against an empty database
1637
+
1638
+ Nothing special is required, and the plan you review is the whole story: on a
1639
+ database with no tables, `voltro db plan --json` includes the framework's own
1640
+ `_voltro_*` tables (the migration ledger, api keys, kv, outbox, traces …) plus
1641
+ `actors`, alongside your own. They are part of the declared schema, so they are
1642
+ planned, classified and applied by exactly the same code as your tables — expect
1643
+ a first-deploy plan to be ~20 operations larger than the diff you wrote.
1644
+
1645
+ Two consequences worth knowing:
1646
+
1647
+ - The reviewed plan is complete. `db apply --plan` creates nothing beside it, so
1648
+ the fingerprint the plan was generated against is still the live schema when
1649
+ the guard checks it. (It did not used to be: the ledger tables were created
1650
+ before the fingerprint was taken, so the first deploy of every new database
1651
+ refused with `the live schema has drifted` one second after the plan was
1652
+ generated. Fixed.)
1653
+ - One table is deliberately absent from the plan: `_voltro_migration_ops`, the
1654
+ crash-resume ledger. It has to exist before the very first plan runs — the plan
1655
+ that creates everything else — so `voltro db apply` creates it itself, under the
1656
+ migration lock. A live `_voltro_*` table your schema does not declare is never
1657
+ planned for a drop, so it does not show up in the next diff either.
1658
+
1557
1659
  ## Apply timing relative to deploy
1558
1660
 
1559
1661
  Two orderings, both common:
@@ -3036,12 +3138,68 @@ voltro db apply --note 'PR #1234'
3036
3138
 
3037
3139
  4. Re-deploy. The new boot's fingerprint check passes.
3038
3140
 
3039
- Note: the boot-mismatch message itself prints
3040
- `voltro db apply --plan plan.json`, but `--plan` is not a real flag
3041
- the prod apply is a plain `voltro db apply` that recomputes the diff.
3141
+ `--plan` IS a real flag, and it is the better answer here. This note used to
3142
+ say it was not, and told you to run a plain `voltro db apply` instead which
3143
+ recomputes the diff and therefore applies something nobody reviewed. The claim
3144
+ came from a defect, not from the design: the app root was resolved as "the
3145
+ first argument that does not start with `-`", so `--plan plan.json` handed the
3146
+ plan FILE to schema discovery (`no schema files found, root: …/plan.json`)
3147
+ while `--plan=plan.json` worked. Both spellings work now.
3148
+
3149
+ Prefer the reviewed form in a pipeline:
3150
+
3151
+ ```sh
3152
+ voltro db plan --json > plan.json # review this in the PR
3153
+ voltro db apply --plan plan.json # apply exactly it, fingerprint-guarded
3154
+ ```
3155
+
3156
+ A plain `voltro db apply` stays correct for a developer machine, where the diff
3157
+ you would review is the one you just wrote.
3042
3158
 
3043
3159
  If the cause is drift (someone DDL'd prod manually), see [drift.md](./drift.md) for reconciliation paths.
3044
3160
 
3161
+ ### If the two processes are the same image: compare their `env:` blocks
3162
+
3163
+ A fingerprint mismatch does not always mean the schema changed. The declared
3164
+ table set is what gets hashed, and until 0.35.0 three RUNTIME flags could move
3165
+ it — so a pre-deploy migrate Job and the pods it feeds could disagree while
3166
+ running identical code against one database.
3167
+
3168
+ That was reported as a green migrate job followed by every pod in
3169
+ CrashLoopBackOff. The chart gave the Job its own `env:` list (`NODE_ENV`,
3170
+ `DB_*`) while `CDC: "0"` lived in the pods' block, because change data capture
3171
+ reads as a runtime concern. Measured on mariadb at `NODE_ENV=production`, each
3172
+ flag flipped alone:
3173
+
3174
+ | flag | effect on the DECLARED set |
3175
+ | --- | --- |
3176
+ | `CDC=0` | removes `_voltro_cdc_offsets` |
3177
+ | `VOLTRO_UNDO=on` | adds `_voltro_undo_log` |
3178
+ | `VOLTRO_TRACING_PERSIST=all` | adds `_voltro_traces` |
3179
+
3180
+ **From 0.35.0 none of them does.** `_voltro_cdc_offsets` follows the DIALECT, so
3181
+ a mariadb/mssql app declares it either way; the other two are declared in
3182
+ `app.config.ts` and the env vars only choose what a process captures:
3183
+
3184
+ ```ts
3185
+ export default {
3186
+ // Both default to on outside production. Declare them when you want the
3187
+ // table in a production schema — `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`
3188
+ // alone is no longer enough, because your migrate job does not carry them.
3189
+ schema: { undo: true, traces: true },
3190
+ }
3191
+ ```
3192
+
3193
+ Setting a capture flag ON without the declaration now refuses the boot and names
3194
+ the field, rather than writing to a table nobody created.
3195
+
3196
+ The refusal also prints which of the three tables this process decided and from
3197
+ which input, so the comparison is a glance rather than a hash diff. Do compare
3198
+ the two `env:` blocks anyway if you use plugins: a plugin's `extendSchema.tables`
3199
+ is your code and may read anything, which is the one part the framework cannot
3200
+ guarantee for you.
3201
+
3202
+
3045
3203
  ## "duplicate column name in plan" (logically invalid plan)
3046
3204
 
3047
3205
  ```
@@ -3170,13 +3328,19 @@ so a failure THERE can leave the index half-built; re-apply finishes it.)
3170
3328
 
3171
3329
  **MySQL and MariaDB** (and sqlite / turso) implicit-commit every DDL
3172
3330
  statement, so THERE a plan that fails on op N leaves ops 1..N-1 committed.
3173
- There is no `--resume` / `--abort` flag and no per-op partial-status
3174
- tracking the apply records a row only on full success.
3175
-
3176
- Recovery is just to re-run the apply: `voltro db apply` re-diffs the
3177
- declared schema against the current (half-applied) live shape and emits
3178
- only the ops that are still missing. Fix the cause of the failed op
3179
- first (e.g. the `ER_DUP_KEYNAME` that stopped op N), then:
3331
+ The `_voltro_migration_plans` row is still written only on full success
3332
+ that row means "this schema is live". What the apply DOES write as it goes
3333
+ is a per-operation **resume ledger** (`_voltro_migration_ops`): every op is
3334
+ recorded before any DDL runs, flipped to `started` before its statement and
3335
+ `applied` after, and the rows are deleted once the apply converges. There is
3336
+ still no `--resume` / `--abort` flag because there is nothing to choose.
3337
+
3338
+ Recovery is just to re-run the apply. It finds the ledger, logs
3339
+ `migration resume: found an interrupted run …`, reconciles any half-finished
3340
+ shadow-column swap or table rebuild, skips the ops that already took effect,
3341
+ and replays the rest — including a `.backfill()` that was only partly done,
3342
+ which a plain re-diff cannot express. Fix the cause of the failed op first
3343
+ (e.g. the `ER_DUP_KEYNAME` that stopped op N), then:
3180
3344
 
3181
3345
  ```sh
3182
3346
  voltro db apply --note 'completing partial apply after fixing op N'
@@ -3361,6 +3525,40 @@ db apply: change triggers converged (1501 statement(s))
3361
3525
 
3362
3526
  The mirror case is reported the same way: a `.nonReactive()` table that still carries a trigger keeps paying `REPLICA IDENTITY FULL` and a `NOTIFY` on every write for a subscription nobody receives. `db apply` removes both.
3363
3527
 
3528
+ **Boot converges them too, on both boot paths.** `voltro dev` and `voltro serve`
3529
+ run the same check-and-repair at startup, so a schema-only restore or a
3530
+ `CDC=0` → `CDC=1` flip no longer waits for someone to notice:
3531
+
3532
+ ```text
3533
+ reactive triggers: converged at boot — installed 500, removed 0 (1501 statement(s))
3534
+ ```
3535
+
3536
+ Three things about it are worth knowing before you deploy a fleet:
3537
+
3538
+ - **It does not queue.** The repair takes the migration advisory lock with
3539
+ `pg_try_advisory_lock` and SKIPS if anything holds it, so N replicas booting
3540
+ together produce one repairing and N-1 logging `another instance … is
3541
+ converging it`. A concurrent `voltro db apply` holds the same lock, so the two
3542
+ can never run each other's DDL. The lock is scoped to your configured
3543
+ `DB_SCHEMA`, so `another instance` really means an instance of YOUR
3544
+ deployment — a second app sharing the database in a different schema takes a
3545
+ different key and neither defers the other.
3546
+ - **It never fails a boot.** A check that cannot run warns and the process
3547
+ continues; reactivity may be degraded, and that is still better than a
3548
+ diagnostic taking the app down.
3549
+ - **It is a tunable**, `reactiveTriggers` in `app.config.ts`, default `'repair'`:
3550
+
3551
+ ```ts
3552
+ export default defineApiApp({
3553
+ store: 'postgres',
3554
+ reactiveTriggers: 'report', // 'repair' (default) · 'report' · 'off'
3555
+ })
3556
+ ```
3557
+
3558
+ `VOLTRO_REACTIVE_TRIGGERS` overrides the field. `VOLTRO_AUTO_MIGRATE=0`
3559
+ downgrades `'repair'` to `'report'` — that variable means "this boot issues no
3560
+ DDL", and it is deliberately not read as "and say nothing".
3561
+
3364
3562
  ## When the fix hint doesn't match reality
3365
3563
 
3366
3564
  The fix hints come from the planner's classification logic — they should always be actionable. If you see one that doesn't make sense given your code:
@@ -1,6 +1,6 @@
1
1
  # Database
2
2
 
3
- > Spin up an isolated branch of a tenant's dataa namespace snapshot/restore by default (dialect-agnostic, no vendor lock) or a Neon copy-on-write fast-path on Postgres then bind to it and tear it down. The basis for per-PR preview environments.
3
+ > Branch the live schema and REHEARSE your migration on it apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path).
4
4
 
5
5
 
6
6
 
@@ -9,73 +9,178 @@
9
9
  <!-- source: en/database/branching.md -->
10
10
  ## Data branching
11
11
 
12
- _Spin up an isolated branch of a tenant's dataa namespace snapshot/restore by default (dialect-agnostic, no vendor lock) or a Neon copy-on-write fast-path on Postgres then bind to it and tear it down. The basis for per-PR preview environments._
12
+ _Branch the live schema and REHEARSE your migration on it apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path)._
13
13
 
14
- Data branching creates an **isolated copy of a tenant's data** you can read and
15
- write without touching the source the basis for per-PR preview environments and
16
- safe "try a migration against real-shaped data" workflows. The default mechanism
17
- is a **namespace snapshot/restore** (works on every dialect, no vendor lock); on
18
- Postgres backed by Neon, a **copy-on-write branch** fast-path is used instead.
14
+ Data branching creates an **isolated copy of a schema** you can read, write and
15
+ migrate without touching the source. The headline use is not the branch — every
16
+ serverless-Postgres vendor sells one of those it is what Voltro can do WITH a
17
+ branch that a vendor cannot: **rehearse your pending migration on it and tell you
18
+ what it would do.**
19
+
20
+ ## `voltro db branch` — the migration rehearsal
21
+
22
+ ```bash
23
+ voltro db branch --pr 128 # branch the live schema, rehearse, report, drop it
24
+ voltro db branch --pr 128 --seed copy # …with the parent's rows copied in
25
+ voltro db branch --pr 128 --keep # leave the branch standing to poke at
26
+ voltro db branch --pr 128 --json # machine-readable, for a PR comment
27
+ ```
28
+
29
+ What it does, in order:
30
+
31
+ 1. **Branches the LIVE schema** into a throwaway namespace (`br_pr128_<app>`).
32
+ 2. **Checks fidelity** — plans the declared schema against the branch AND against
33
+ the parent, and aborts if the two disagree. A branch that is not a faithful
34
+ copy rehearses a different migration from the one you are about to run, and
35
+ saying nothing about that would be worse than not rehearsing at all.
36
+ 3. **Plans your migration** against the branch and classifies every operation.
37
+ 4. **Executes it there**, including the destructive operations.
38
+ 5. **Re-plans.** An empty re-plan is the verdict; a migration that applies and
39
+ then re-proposes itself forever is not a migration.
40
+ 6. **Drops the branch** (unless `--keep`), even when the apply failed.
41
+
42
+ ```
43
+ branch rehearsal · br_pr128_shop · mechanism namespace
44
+ branched 18 table(s), replayed 3 foreign key(s)
45
+ plan: 2 operation(s), 1 lossy, 0 refused
46
+ • add-column members [safe]
47
+ ✗ drop-column members [lossy]
48
+
49
+ ⚠ 1 operation(s) DESTROY DATA. They were executed on the branch (it is
50
+ disposable) so they are rehearsed, but production refuses them until you set
51
+ VOLTRO_DESTRUCTIVE_OK — naming the tables, not `1`.
52
+
53
+ ✓ applied on the branch, and the re-plan is EMPTY (the migration converges).
54
+ branch br_pr128_shop torn down.
55
+ ```
56
+
57
+ ### Lossy operations are executed, not skipped
58
+
59
+ Production refuses a `drop-column` until a human acknowledges it. A branch that
60
+ is about to be dropped has no such reason — and refusing there would mean the one
61
+ operation most likely to fail is the one operation never rehearsed. So the
62
+ rehearsal unblocks lossy operations on the branch, runs them, and leads the report
63
+ with every one of them. That report is the thing you paste into the PR.
64
+
65
+ Operations the planner refuses **anywhere** (`needs-rename-annotation`,
66
+ `multi-step`) are NOT executed — the plan is reported as `blocked` instead.
67
+
68
+ ### Exit codes
69
+
70
+ | code | meaning |
71
+ |---|---|
72
+ | `0` | applied on the branch and converged, nothing lossy |
73
+ | `2` | a REVIEW signal: the plan destroys data, or the planner refuses part of it |
74
+ | `1` | the rehearsal could not answer — it failed, the branch was not faithful, or the migration did not converge |
75
+
76
+ `lossy` is deliberately not an error. A `drop-column` in a PR is a normal,
77
+ intentional thing; making it exit `1` trains people to pass `--force`, and the
78
+ next real failure goes with it.
79
+
80
+ ### What `--seed` does and does not rehearse
81
+
82
+ `--seed empty` (the default) branches the SCHEMA only. That is enough for every
83
+ structural question and costs nothing. It does **not** rehearse anything
84
+ data-dependent: a `NOT NULL` meeting existing NULLs, a backfill meeting real
85
+ values, a unique constraint meeting duplicates, or the row-count threshold that
86
+ promotes an operation to `online-required`. Use `--seed copy` for those — it
87
+ copies every row, which is fast on a small database and slow on a large one.
88
+
89
+ ## Mechanisms — what actually works
90
+
91
+ Be precise here, because the vendor landscape invites over-claiming. The branch
92
+ **plan** is dialect-agnostic. The shipped **executor** is not.
93
+
94
+ | mechanism | when | status |
95
+ |---|---|---|
96
+ | `namespace` | Postgres — a schema per branch | shipped, and what `voltro db branch` uses |
97
+ | `neon-cow` | Postgres on Neon, `seed: 'copy'` | plan shipped; **executed by the cloud control plane**, not the CLI |
98
+
99
+ - **MySQL, MariaDB, SQLite and SQL Server are not supported by
100
+ `voltro db branch`.** `makeNamespaceBranchExecutor` emits `CREATE SCHEMA`,
101
+ `CREATE TABLE … (LIKE … INCLUDING ALL)` and `"`-quoted identifiers, none of
102
+ which those engines accept. The command refuses with that reason rather than
103
+ sending Postgres syntax at them. Writing your own `BranchExecutor` for another
104
+ dialect is the supported path — the plan is already portable.
105
+ - **Neon copy-on-write is a call to Neon's branch API** and needs a Neon token,
106
+ which the CLI does not hold. `voltro db branch` names the mechanism and refuses,
107
+ pointing at `--prefer namespace`. The cloud control plane executes it.
108
+ - **There is no Supabase mechanism and no template-database mechanism.** Neither
109
+ exists in the codebase.
19
110
 
20
111
  ## The plan/execute split
21
112
 
22
113
  Branching is a **plan** (what to do) + an **executor** (do it), mirroring the
23
- migration engine. The plan is pure + inspectable; the executor performs the I/O.
114
+ migration engine. The plan is pure and inspectable; the executor performs the I/O.
24
115
 
25
116
  ```ts
26
117
  import { planBranch, resolveBranchMechanism } from '@voltro/database'
27
118
 
28
- // Pick the mechanism: 'neon-cow' when seeding a copy on a Neon URL,
29
- // 'namespace' (schema-per-branch) otherwise.
30
119
  const mechanism = resolveBranchMechanism({ seed: 'copy', dbUrl: connectionString })
31
120
  const steps = planBranch({
32
121
  mechanism,
33
- branchId: 'branch_acme_pr128',
122
+ branchId: 'br_pr128_shop',
34
123
  tableNames,
35
124
  seed: 'copy',
36
- parentNamespace: 'public', // namespace mechanism: the schema to snapshot from
125
+ parentNamespace: 'public',
37
126
  })
38
- // steps: e.g. [{ kind: 'neon-branch-create', … }] or per-table namespace snapshot steps
39
127
  ```
40
128
 
41
129
  ## Provision + tear down
42
130
 
43
131
  `provisionBranch` / `teardownBranch` run a plan through an injected
44
- `BranchExecutor` (the dialect/Neon adapter). Injected so the lifecycle is
45
- unit-testable with a recording executor — no live DB:
132
+ `BranchExecutor`. Injected so the lifecycle is unit-testable with a recording
133
+ executor — no live database:
46
134
 
47
135
  ```ts
48
- import { provisionBranch, teardownBranch } from '@voltro/database'
136
+ import { provisionBranch, teardownBranch, makeNamespaceBranchExecutor } from '@voltro/database'
49
137
 
50
138
  const result = await provisionBranch(
51
- { appSlug: 'acme', prNumber: 128, seed: 'copy', tableNames, dbUrl: connectionString },
52
- executor,
139
+ {
140
+ appSlug: 'shop', prNumber: 128, seed: 'copy', tableNames,
141
+ parentNamespace: 'public',
142
+ foreignKeys, // see below — LIKE does not copy these
143
+ indexNames, // see below — LIKE renames these
144
+ dbUrl: connectionString,
145
+ },
146
+ makeNamespaceBranchExecutor({ run: (sql) => client.query(sql) }),
53
147
  )
54
- // result: { branchId, mechanism, steps, … } — for 'neon-cow' also the
55
- // branch's own connection string to bind the preview store to.
56
148
 
57
- // …run the preview against result, then:
58
149
  await teardownBranch(result.branchId, result.mechanism, executor)
59
150
  ```
60
151
 
61
- `admitBranch` gates whether a branch may be served (e.g. it exists + is ready)
62
- so a request never binds to a half-provisioned branch.
152
+ `admitBranch` enforces the storage-cost caps (a TTL and a maximum number of live
153
+ branches) before a new one is provisioned.
63
154
 
64
- ## Mechanisms
155
+ ### Two things `LIKE … INCLUDING ALL` does not carry
65
156
 
66
- | mechanism | when | how |
67
- |---|---|---|
68
- | `namespace-snapshot` | any dialect (default) | snapshot the tenant's namespace → restore into a new branch namespace |
69
- | `neon-branch` | Postgres on Neon | a copy-on-write Neon branch (instant, storage-shared) |
157
+ Both were found by pointing the rehearsal at a real Postgres and watching a
158
+ converged schema propose work. They are properties of Postgres, not of Voltro's
159
+ emission, and a branch missing either is not a copy:
160
+
161
+ - **Foreign keys are not copied.** There is no `INCLUDING` clause that copies
162
+ them. A branch without them accepts writes production rejects.
163
+ - **Index names are re-derived** from the table and columns. Measured on pg 17:
164
+ `byApiKeyTenant` came back as `_voltro_api_keys_tenantId_idx`, and
165
+ `_voltro_idempotency_scope_key_uq` as `…_scope_key_idx`. The migration planner
166
+ compares indexes by name, so every custom-named index reads as a different
167
+ index.
70
168
 
71
- The mechanism is resolved from the connection, so the same `planBranch` call
72
- produces the right steps for the deployment's database dialect-agnostic by
73
- default, with the Neon fast-path when it's available. No code path is locked to a
74
- vendor.
169
+ `provisionBranch` replays both (`foreignKeys` + `indexNames`, sourced from the
170
+ parent's introspected snapshot). `voltro db branch` does it for you, and refuses
171
+ to report anything about your migration if the branch still differs from the
172
+ parent.
173
+
174
+ ## Branch-per-PR in CI
175
+
176
+ ```yaml
177
+ - run: voltro db branch --pr ${{ github.event.number }} --json > rehearsal.json
178
+ continue-on-error: true # exit 2 is a review signal, not a build failure
179
+ - run: node scripts/comment-rehearsal.mjs rehearsal.json
180
+ ```
75
181
 
76
- > Branching copies **data**, not the schema definition — apply migrations to a
77
- > branch the same way you would to any store. Teardown is explicit; a branch
78
- > left bound holds its namespace/Neon branch until torn down.
182
+ The `--json` report carries `operations`, `lossy`, `blocked`, `residual`,
183
+ `converged` and `infidelity` everything a PR comment needs, already classified.
79
184
 
80
185
 
81
186
 
@@ -281,11 +386,22 @@ recoveryNote: text().encrypted(), // encrypted at rest, but the o
281
386
  apiToken: text().encrypted().serverOnly(), // secret at rest AND never to a client
282
387
  ```
283
388
 
284
- Enforcement: the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
285
- columns from every returned row **automatically** — you declare the exposure
286
- policy once at the schema and can't forget it on a handler. For a hand-written
287
- query, omit the column from the `output` schema (and don't put it in the returned
288
- object).
389
+ Enforcement runs in **both directions**, because "never crosses the wire" is not
390
+ a one-way claim:
391
+
392
+ - **Outbound** the [`crud.*` read helpers](/docs/data/crud) strip `.serverOnly()`
393
+ columns from every returned row **automatically**. You declare the exposure
394
+ policy once at the schema and can't forget it on a handler. For a hand-written
395
+ query, omit the column from the `output` schema (and don't put it in the
396
+ returned object).
397
+ - **Inbound** — `crud.create` / `crud.update` **refuse** an input that sets a
398
+ `.serverOnly()` column, with `ServerOnlyColumnWrite` naming it, and write
399
+ nothing. A column the client may not read must not be one the client can set:
400
+ accepting it is mass assignment. It is refused rather than silently stripped
401
+ because a stripped field makes an attack indistinguishable from a no-op. When
402
+ the *server* needs to write one, do it from the handler with
403
+ `ctx.store.insert` / `ctx.store.update` — the refusal is on the generated
404
+ path, which is the one fed straight from client input.
289
405
 
290
406
  A hand-written output is the case `crud.*` cannot cover, so an audit checks it:
291
407
  a wire-reachable query whose `source` table carries a `.serverOnly()` column that
@@ -260,9 +260,27 @@ The `sql` tag is imported from the server-only `@voltro/database/sql` subpath
260
260
  **Raw queries aren't tracked by the reactive engine** — the planner can't infer which tables an arbitrary SQL string touches. If you want a subscription to invalidate on a raw read's tables, declare them explicitly:
261
261
 
262
262
  ```ts
263
- ctx.store.raw!<{ }>(sql`…`, { dependsOn: ['events'] })
263
+ ctx.store.raw!<{ n: number }>(sql`SELECT count(*) AS n FROM events`, { dependsOn: ['events'] })
264
264
  ```
265
265
 
266
+ The tables can also live on the fragment itself (`{ ...frag, dependsOn: ['events'] }`), which is handy when the fragment is built somewhere else. They are taken at face value — the framework records what you declare and never checks it against the SQL.
267
+
268
+ **If you forget, the framework tells you.** A raw read taken while a live query's handler runs, with nothing declared, warns once at subscribe time and names both the query and the SQL:
269
+
270
+ ```
271
+ [voltro] reports.summary: a raw SQL read in this live query declares no
272
+ dependsOn, so no write can invalidate it — every subscriber keeps its first
273
+ result until it reconnects. Declare the tables it reads:
274
+ ctx.store.raw(fragment, { dependsOn: ['orders'] }) — or on the fragment itself.
275
+ The read: SELECT sum(total) FROM orders WHERE tenant = ?
276
+ ```
277
+
278
+ Only the STATIC text is logged; the bound values never are.
279
+
280
+ The diagnostic keeps a bounded record: one request remembers up to 32 distinct raw reads (deduplicated by SQL text), tunable with `VOLTRO_RAW_READ_TRACKING_LIMIT`. Reads past the bound are dropped rather than remembered — a handler that raw-reads in a loop must not turn a warning into a memory leak. Raise it only if a handler issues many distinct raw reads and you want the warning to name a later one.
281
+
282
+ **Where `dependsOn` works, and where it can only warn.** It drives recomputation for a query whose handler returns a **computed value** — the shape whose handler is genuinely re-run on a change (see [Subscriptions](/docs/data/subscriptions)). The declared tables join the query's own `source:`; they never replace it. A handler that returns a query **descriptor** is different: a change re-runs the descriptor's query, not your handler, so the raw result is not refreshed. There the warning is the whole answer — return a computed value if the raw read has to stay live.
283
+
266
284
  ## Tenant scoping (implicit)
267
285
 
268
286
  If the table has the `tenant()` mixin, every `select` auto-merges `WHERE tenantId = ctx.subject.tenantId`. You don't write it; the runtime injects it. To opt out (admin queries crossing tenants), use `.unscoped()`: