@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
@@ -41,6 +41,29 @@ Verify in the boot log:
41
41
  That's it. Reads now flow to the replica unless RYW pins them back.
42
42
  Writes always go to primary.
43
43
 
44
+ ## Replica pools take the primary's connection settings
45
+
46
+ A replica is a **connection pool in the same process**, and it is configured by
47
+ the same environment variables the primary reads — `DB_MAX_CONNECTIONS`,
48
+ `PG_SSL`, `DB_SCHEMA`, `DB_STATEMENT_TIMEOUT_MS`, `DB_ACQUIRE_TIMEOUT_MS`. The
49
+ replica URL decides only WHICH server it talks to.
50
+
51
+ That matters for two reasons an operator has to plan for:
52
+
53
+ - **Connection budget.** `DB_REPLICA_URLS` with two entries means this process
54
+ opens **three** pools of `DB_MAX_CONNECTIONS` each, not one. The boot line
55
+ says so:
56
+
57
+ ```
58
+ db pool: max=10 per replica (DB_MAX_CONNECTIONS) → max × 3 = 30 per pod × 4 replicas = up to 120 connections.
59
+ This process opens 3 pools of that size — 1 primary + 2 read replica(s) (DB_REPLICA_URLS), each with its own DB_MAX_CONNECTIONS.
60
+ ```
61
+
62
+ - **TLS.** `PG_SSL=require` now applies to replica connections as well. A
63
+ `?sslmode=require` in the replica URL still works and still wins over the
64
+ driver default, but an explicit `PG_SSL` outranks it — so the TLS decision is
65
+ made once, for every connection the process opens.
66
+
44
67
  ## Read-your-writes (RYW)
45
68
 
46
69
  After a mutation commits, the subject who wrote is **pinned to
@@ -319,6 +342,43 @@ will NOT reach clients on other replicas. Add @voltro/plugin-broadcast (Redis /
319
342
 
320
343
  When BOTH a native path and the broadcast plugin are wired (e.g. postgres + broadcast), both stay active — the own-origin skip dedups, so there's no double-emit. The native path is the primary; the bus is harmless redundancy.
321
344
 
345
+ ## sqlite and the memory store behind replicas
346
+
347
+ Neither dialect has a cross-instance path, and neither can get one: a local
348
+ database file and an in-process store are, by construction, **this process's**.
349
+ Putting several replicas in front of one is not a reactivity gap — the replicas
350
+ do not share a database at all, so each one is also reading its own data.
351
+
352
+ On a laptop that is the correct, normal configuration, which is why the framework
353
+ said nothing about it for a long time. It now warns when it can see evidence of
354
+ an **orchestrator**:
355
+
356
+ ```
357
+ [voltro:serve] this app runs on 'sqlite' — a local database file — but POD_IP is set
358
+ (Kubernetes), which means several replicas. Neither dialect has ANY cross-instance
359
+ change capture: a client connected to replica A never sees a write made on replica B,
360
+ for every table, and each replica is also reading its OWN data…
361
+ ```
362
+
363
+ The evidence is one of `REPLICA_COUNT > 1`, `KUBERNETES_SERVICE_HOST`, `POD_IP`,
364
+ `POD_NAME`, `FLY_ALLOC_ID`, `FLY_MACHINE_ID`, `ECS_CONTAINER_METADATA_URI[_V4]`,
365
+ `K_REVISION`, `CONTAINER_APP_REPLICA_NAME` or `RENDER_INSTANCE_ID`. `HOSTNAME`,
366
+ `NODE_ENV` and `PORT` are deliberately **not** evidence — every single-instance
367
+ container sets those too, and a warning that fires on a laptop gets filtered out
368
+ before it reaches the deployment where it is true.
369
+
370
+ Two ways to silence it, and they are not equivalent:
371
+
372
+ - `REPLICA_COUNT=1` — you are telling the framework there is exactly one process.
373
+ This is the honest one, and it is the only positive evidence *against* that
374
+ exists, so it outranks every platform signal.
375
+ - Declaring `@voltro/plugin-broadcast` with a real provider — not because a bus
376
+ fixes it (the replicas still have separate databases), but because declaring
377
+ one means you have already thought about the question.
378
+
379
+ The real fix is postgres or mysql/mariadb: a shared database with a native
380
+ cross-instance change path.
381
+
322
382
  ## Where it lives in the codebase
323
383
 
324
384
  - `voltro/packages/plugin-broadcast` — the plugin, the `BroadcastProvider` contract, the redis / nats / memory providers, `attachBroadcastBus`.
@@ -373,6 +373,7 @@ import { TableValidationFailed } from '@voltro/runtime'
373
373
 
374
374
  export const createUser = defineMutation({
375
375
  name: 'users.create',
376
+ guards: [{ scope: 'users:write' }],
376
377
  input: Schema.Struct({ email: Schema.String }),
377
378
  output: Schema.Struct({ id: Schema.String }),
378
379
  target: { table: 'users', op: 'insert' },
@@ -74,21 +74,68 @@ table. If you want one tenant's rows, say so in your own predicate.
74
74
  |---|---|---|
75
75
  | `boot` | On every `voltro dev` boot, **only if the fingerprint changed** | — |
76
76
  | `manual` | Only via `voltro db seed --id <name>` or the dashboard | — |
77
- | `onTenantCreate` | **Not wired yet** declared and validated, never triggered | |
78
- | `onSchemaChange` | **Not wired yet** declared and validated, never triggered | `watchedTables` |
79
- | `cron` | **Not wired yet** declared and validated, never triggered | `cron` |
80
-
81
- Only `boot` and `manual` actually execute today. The other three lifecycles
82
- validate at definition time (a `cron` seed without a `cron` field throws, an
83
- `onSchemaChange` seed without `watchedTables` throws) and are registered so the
84
- dashboard can list them but nothing fires them. A seed declared with one of
85
- them will never run, silently.
86
-
87
- Until they are wired, express the same intent with a primitive that does run: a
88
- `*.cron.tsx` schedule whose handler does the seeding, or an explicit `voltro db
89
- seed --id <name>` from your migration/provisioning step. This table said
90
- otherwise until 0.10.0, which is exactly the kind of promise that costs someone
91
- a debugging afternoon it is corrected here rather than quietly dropped.
77
+ | `onTenantCreate` | Every time a tenant namespace is provisioned, **into that namespace** | a store with namespace isolation |
78
+ | `onSchemaChange` | After a schema apply that touched a watched table (`voltro dev`'s boot auto-migrate, `voltro db apply`, `voltro migrate`) | `watchedTables` |
79
+ | `cron` | On the cron expression, through the coordinated scheduler one firing fleet-wide | `cron` |
80
+
81
+ ### `onTenantCreate`
82
+
83
+ Fires from `provisionTenantNamespace`, after the namespace DDL lands and before
84
+ provisioning resolves so a caller that awaits it gets a tenant whose tables
85
+ **and** reference data exist, or an error. Three properties worth knowing:
86
+
87
+ - **Steps run scoped to the new namespace**, never the shared tables. A store
88
+ without namespace isolation (sqlite, a single-schema deployment) makes the run
89
+ **refuse** rather than fall back the fallback would write one tenant's
90
+ fixture into every tenant's data. On a single-namespace deployment, declare
91
+ the seed `lifecycle: 'boot'` instead.
92
+ - **No fingerprint skip.** A new namespace has none of the data, whatever
93
+ another tenant's run recorded. The ledger row is keyed `<seedId>@<namespace>`,
94
+ so N tenants produce N rows in `_voltro_seeds`.
95
+ - **A failure fails provisioning.** Unlike a boot seed, which logs and lets the
96
+ server come up, a failed tenant-create seed rejects the provisioning call — a
97
+ tenant whose namespace exists and whose data does not, reported as success, is
98
+ worse than a loud error. Re-run provisioning (it is idempotent) or
99
+ `voltro db seed --id <name>` once the cause is fixed.
100
+
101
+ ### `onSchemaChange`
102
+
103
+ Fires from the migration applier, so it covers every path that applies a schema:
104
+ `voltro dev`'s boot auto-migrate, `voltro db apply` (bare and `--plan`) and
105
+ `voltro migrate`. Every seed whose `watchedTables` intersect the tables the apply
106
+ actually changed runs once, after the apply.
107
+
108
+ Three properties worth knowing:
109
+
110
+ - **Strictly post-apply.** It runs after the audit row is written, so on a
111
+ transactional dialect the DDL has already committed and the seed talks to its
112
+ own `DataStore` rather than the migration's connection.
113
+ - **It cannot fail the migration.** The schema landed; a fixture that throws is
114
+ logged and recorded in `_voltro_seeds`. A successful apply is never reported as
115
+ failed because reference data did not load.
116
+ - **`voltro serve` does not run it** — for the same reason it does not run boot
117
+ seeds. Serve never applies a schema, so there is no schema change for it to
118
+ react to; the pre-deploy `voltro db apply` is where it happens.
119
+
120
+ > This seam was **declared and never called** until 0.34.0: the hook installed,
121
+ > the seeds were discovered, listed and ledgered, and nothing ran. If you
122
+ > declared an `onSchemaChange` seed before that release, expect it to fire on
123
+ > your next apply.
124
+
125
+ ### `cron`
126
+
127
+ `cron` seeds are projected into real schedules — the same coordinated scheduler
128
+ `*.cron.tsx` uses, so one firing happens fleet-wide instead of one per replica.
129
+ Both `voltro dev` and `voltro serve` register them at boot and log the schedule
130
+ names they created.
131
+
132
+ The schedule is named `seed:<id>`, which is also the name its runs appear under
133
+ in `_voltro_schedule_runs` and in the dashboard — distinct from a `*.cron.tsx`
134
+ namespace, so a schedule and a seed may share an id without colliding.
135
+
136
+ A cron firing runs the seed **unconditionally**. The fingerprint skip that makes
137
+ a `boot` seed cheap does not apply here: the trigger is the clock, not a change
138
+ in the source.
92
139
 
93
140
  ```ts
94
141
  defineSeed({
@@ -96,11 +143,24 @@ defineSeed({
96
143
  name: 'Rebuild search index nightly',
97
144
  lifecycle: 'cron',
98
145
  cron: '0 3 * * *', // required for lifecycle: 'cron'
146
+ timezone: 'Europe/Berlin', // optional; defaults to an explicit 'UTC'
99
147
  steps: ({ step }) => [ /* … */ ],
100
148
  })
101
149
  ```
102
150
 
103
- `defineSeed` validates at definition time: `cron` lifecycle without a `cron` field throws, `onSchemaChange` without `watchedTables` throws, and a seed with zero steps throws.
151
+ `timezone` is defaulted rather than required (unlike `defineSchedule`'s, where
152
+ it is mandatory) — reference data rarely cares about a local wall clock. The
153
+ default is an explicit `'UTC'`, never the container's clock.
154
+
155
+ `defineSeed` validates at definition time: `cron` lifecycle without a `cron` field throws, `onSchemaChange` without `watchedTables` throws, and a seed with zero steps throws. The cron EXPRESSION is validated at boot, so a typo fails the boot naming the seed rather than never firing.
156
+
157
+ > **An app with any `*.seed.ts` gets the two schedule ledger tables**
158
+ > (`_voltro_schedule_runs`, `_voltro_schedule_claims`), even without a cron seed.
159
+ > Whether a seed is a cron seed is a field INSIDE the file, and `voltro migrate`
160
+ > never imports your modules — so the table set is decided from the FILENAME, and
161
+ > it has to be decided identically by `voltro dev`, `voltro serve`, `voltro db
162
+ > apply` and `voltro migrate` or the schema fingerprint diverges. Two empty ledger
163
+ > tables is the price of that agreement.
104
164
 
105
165
  ## Steps
106
166
 
@@ -138,6 +198,7 @@ voltro db seed --store memory # explicit opt-in: in-memory smoke run
138
198
 
139
199
  - **Reference data** (must exist in every environment): `lifecycle: 'boot'`, idempotent via `upsertByUnique`. Safe in production.
140
200
  - **Demo data** (populate a fresh dashboard): `lifecycle: 'boot'` in dev, or `lifecycle: 'manual'` so it's operator-triggered and never auto-runs in prod.
201
+ - **Per-tenant starter data** (a new tenant's default categories, roles, settings): `lifecycle: 'onTenantCreate'`, which runs scoped to the tenant's own namespace as it is provisioned.
141
202
 
142
203
  See [migrations](/docs/database/migrations) for schema changes — seeds populate data, migrations shape the tables.
143
204
 
@@ -237,6 +298,49 @@ It applies to the **runtime query path only**. Migrations (`voltro db apply`) ru
237
298
 
238
299
  **Wired for postgres today** (the default dialect), where it maps to the server-side `statement_timeout` — a real, server-enforced cancel (SQLSTATE `57014`), not a client-side disconnect that leaves the query running. Other dialects accept the variable but currently ignore it, and the reasons are honest rather than incidental: `@effect/sql-mssql` exposes only a connection-establishment timeout, not a per-request one; MySQL/MariaDB's `max_execution_time` bounds `SELECT`s only (writes stay unbounded), which would be a misleading half-guarantee; and SQLite is in-process with a single connection, so there is no pool to protect. Unset (or any non-postgres dialect) = no timeout.
239
300
 
301
+ ### Acquire timeout — bound the wait for a free connection
302
+
303
+ `DB_STATEMENT_TIMEOUT_MS` above bounds a query the **server** is running. Nothing bounded a query the client had not sent yet: a request arriving when every pooled connection is busy waited — with no error, no retry and no log line — until something else finished. Those are the two halves of "a request is stuck", and only one of them was covered.
304
+
305
+ Every dialect that can bound an acquire now does, **by default**:
306
+
307
+ | Dialect | Default | What is bounded |
308
+ |---|---|---|
309
+ | postgres | 10 s | the whole acquire — waiting in the pool's queue *and* connecting |
310
+ | mysql / mariadb | 10 s connect · 100 queued waiters | connecting; the wait is bounded by queue **length**, see below |
311
+ | mssql | 10 s | connecting (and the boot probe) |
312
+ | sqlite / turso | — | one in-process connection, no pool to exhaust |
313
+
314
+ An exhausted pool now fails with an error that **names the pool** — `Failed to acquire connection` — at the moment the pool is the cause, instead of surfacing as unexplained latency somewhere with no connection information in it.
315
+
316
+ Move it with `DB_ACQUIRE_TIMEOUT_MS`, in milliseconds. `0` restores the driver's unbounded wait; a negative or non-numeric value falls back to the 10 s default rather than to no bound at all:
317
+
318
+ ```sh
319
+ DB_ACQUIRE_TIMEOUT_MS=3000 # 0 = the driver's unbounded wait
320
+ DB_ACQUIRE_QUEUE_LIMIT=2000 # mysql / mariadb only, OPT-IN — read the hazard below
321
+ ```
322
+
323
+ Both are read on every command that opens a pool — `voltro dev`, `voltro serve`, `voltro migrate` and every `voltro db …` subcommand alike — so the variable means one thing across your deployment. (`DB_STATEMENT_TIMEOUT_MS` above is deliberately runtime-only: a migration runs legitimately long *statements*. An acquire bound fires when no connection is free at all, which a migration has no more reason to wait forever for than a request does.)
324
+
325
+ That "every command" is literal, and it is worth stating because it has not always been true. Every pool the CLI opens gets its configuration from **one** resolver, so a knob cannot be honoured by one command and ignored by the next. The only two things the command changes are the two named here: `DB_STATEMENT_TIMEOUT_MS` (runtime only) and `DB_DIRECT_URL` / `DB_MIGRATE_URL` (migration only). Everything else — pool size, `DB_SCHEMA`, TLS (`PG_SSL`), the acquire bounds — resolves identically everywhere, including in the two places that open a bare postgres client rather than a pool (the web process's ISR cache and its CDC listener).
326
+
327
+ The same value is `ConnectionConfig.acquireTimeoutMs` when you build a layer yourself:
328
+
329
+ ```typescript
330
+ import { postgresDialect } from '@voltro/sql-postgres'
331
+
332
+ const layer = postgresDialect.makeSqlLayer({
333
+ url: process.env.DB_URL!,
334
+ acquireTimeoutMs: 3_000, // 0 = the driver's unbounded wait
335
+ })
336
+ ```
337
+
338
+ **mysql/mariadb cannot express a time bound on the waiting half, and the length bound that exists is opt-in.** `mysql2`'s pool queues the waiting caller with no timer at all, so there is nothing to set. The one bound the driver has is a LENGTH — `ConnectionConfig.acquireQueueLimit` / `DB_ACQUIRE_QUEUE_LIMIT`, mysql2's `queueLimit`. **The framework does not set it for you**, and the reason is worth understanding before you do:
339
+
340
+ A time bound self-throttles. The acquire fails only once the wait has elapsed, so nothing can retry it faster than the timeout. A length bound is free: past the limit mysql2 rejects the acquire *synchronously*, so any caller that retries a failed acquire without a delay retries in the same tick — forever. The event loop is never reached again, which means no timer fires and the queue whose depth caused the rejection can never drain. The symptom is a process pinned at 100% CPU with no error, no log line, and an idle database.
341
+
342
+ That caller is not hypothetical: the workflow cluster releases shards one statement per shard (300 by default) and retries a failed release immediately. So set `DB_ACQUIRE_QUEUE_LIMIT` only if you know nothing in your process retries an acquire without backoff, and set it well above the peak concurrency of anything that might. Unset — the default — you get mysql2's unbounded queue: callers wait rather than fail. mssql's pool exposes neither knob, so only establishment is bounded there — stated rather than papered over, for the same reason `DB_STATEMENT_TIMEOUT_MS` is left unwired on the dialects that cannot enforce it honestly.
343
+
240
344
  ## Local development — bring up all five
241
345
 
242
346
  The framework ships a docker-compose at `voltro/test/docker-compose.yml` that brings up postgres + mysql + mariadb + mssql on distinct ports so per-dialect tests can run side-by-side and the dev fixture never clashes with your starter postgres on `:5432`:
@@ -378,6 +482,8 @@ The framework wraps `pg` (`node-postgres`) via Effect's typed connection layer.
378
482
  | `PG_MAX_CONNECTIONS` | `10` | pool ceiling. Tune up for high concurrency; CPU-bound workloads rarely benefit past ~2× cores. |
379
483
  | `PG_SSL` | unset | `'require'` (TLS without certificate verification — what production deployments behind RDS / Cloud SQL / Supabase want) / `'disable'` (force plaintext). Any other value — including libpq's `'prefer'`, which node-postgres cannot express (the driver has no TLS-then-plaintext fallback) — fails at boot instead of silently downgrading to plaintext. A `?sslmode=require\|disable` query on `DB_URL` works too; an explicit `PG_SSL` wins when both are set. |
380
484
 
485
+ `PG_SSL` is read by **every** command that opens a connection — `voltro dev`, `voltro serve`, `voltro start`, `voltro migrate` and every `voltro db …` subcommand — so TLS is not something one command negotiates and the next one skips.
486
+
381
487
  ## CDC — LISTEN/NOTIFY
382
488
 
383
489
  Every table gets an AFTER trigger that emits a `framework_changes` notification on each insert / update / delete. The dispatcher LISTENs on that channel once per process and fans out to subscribers in-memory.
@@ -409,10 +515,63 @@ Trade-offs of the LISTEN/NOTIFY path:
409
515
  - ✅ Sub-frame latency (60fps UIs feel real-time).
410
516
  - ✅ Zero polling load even on idle tables.
411
517
  - ❌ Requires a long-lived connection per process. Connection-pooled deploys must use a sidecar listener or pgbouncer in session mode.
412
- - ❌ NOTIFY payloads are capped at 8KB. The trigger sends the full row images (`row_to_json(OLD)`/`row_to_json(NEW)`) so subscribers get the pre/post values directly; when a wide row would exceed the cap the trigger drops the body and sends an ID-only notification (the subscriber re-queries), so a very wide reactive row degrades gracefully rather than failing.
518
+ - ❌ NOTIFY payloads are capped at 8000 bytes. The trigger sends the full row images (`row_to_json(OLD)`/`row_to_json(NEW)`) so subscribers get the pre/post values directly; a wide row that would exceed the cap falls back to a key-only notification. See the delivery guarantee below — the fallback is not free, and it is not the same for a subscription as it is for a tap.
413
519
 
414
520
  Set `CDC=0` to disable + force the inline-emit path (single-process only, no cross-process fan-out). Useful for tests + single-binary deploys.
415
521
 
522
+ ### Oversized rows — what is guaranteed
523
+
524
+ A row whose JSON image exceeds ~8000 bytes (a document, a large `json()` column,
525
+ an embedded array) cannot travel in a NOTIFY payload. The trigger keeps the
526
+ change and the **primary key**, drops the images, and the CDC consumer re-reads
527
+ the row from the database before the event reaches anything. What each consumer
528
+ gets:
529
+
530
+ | Change | What is delivered | `event.oversized` |
531
+ |---|---|---|
532
+ | insert / update | the row **re-read from the database** | `'rehydrated'` |
533
+ | delete | the **primary key only** — a tombstone | `'tombstone'` |
534
+ | key missing, re-read failed, or the row is already gone | both images null | `'unrecovered'` |
535
+
536
+ Read the marker before you treat an image as a snapshot. Three limits are real
537
+ and cannot be engineered away:
538
+
539
+ - **A re-read returns the row as it is NOW.** If a second write lands between
540
+ the change and the re-read, this event carries the newer state — and the
541
+ second change delivers it again. The stream is convergent, not
542
+ point-in-time. Postgres keeps no copy of an image the transport dropped.
543
+ - **`old` is null on an oversized update, and pk-only on an oversized delete.**
544
+ There is nowhere to read a pre-image from. A tombstone is enough to REMOVE the
545
+ row from a search index, an analytics mirror or a CDC stream; it is not a
546
+ record of what the row contained, and `@voltro/plugin-versioning` writes
547
+ `data: null` for one rather than a fabricated empty snapshot.
548
+ - **`'unrecovered'` means the content is gone.** No retry can bring it back —
549
+ it was never delivered. Subscriptions are unaffected (they re-query); taps
550
+ miss that row until the next write to it or a re-seed.
551
+
552
+ Every fallback is counted as `voltro_cdc_oversized_total{outcome=…}` (scrapeable
553
+ via `@voltro/plugin-prometheus` at `/metrics`, or `GET
554
+ /_voltro/inspect/metrics`), the first one per table is logged at `warn`, and
555
+ every `unrecovered` one is logged at `error`. **Alert on
556
+ `outcome="unrecovered"`** — a non-zero rate means the deployment is losing
557
+ changes for its taps.
558
+
559
+ Tunables (options on the postgres store, or environment):
560
+
561
+ | Env | Default | Meaning |
562
+ |---|---|---|
563
+ | `VOLTRO_CDC_REHYDRATE_TIMEOUT_MS` | `5000` | total budget for recovering one oversized change. The LISTEN consumer is serial, so this also bounds how long one oversized row can hold up the change stream. |
564
+ | `VOLTRO_CDC_REHYDRATE_RETRIES` | `2` | re-reads after the first attempt, inside that budget. |
565
+
566
+ The re-read is issued against the schema the write landed in, so it is correct
567
+ under namespace (schema-per-tenant) isolation.
568
+
569
+ **This lives in the database, so it has to be applied.** The trigger function is
570
+ DDL: a database created before this shipped still carries the old body, which
571
+ drops the key and makes every oversized change `'unrecovered'`. `voltro db
572
+ apply` replaces it (`voltro dev` reports it at boot as trigger drift, and the
573
+ `error` log line names the same remedy).
574
+
416
575
  ## Workflow cluster
417
576
 
418
577
  `@effect/cluster`'s `SqlRunnerStorage` uses `pg_advisory_lock(key)` to claim shard ownership. The framework wires this transparently — set `DB_DIALECT=postgres` + provide a SqlClient layer and `workflowEngineLayer({ runnerStorage: 'sql' })` does the rest.
@@ -504,9 +663,25 @@ DB_HOST=… DB_PORT=… DB_USER=… DB_PASSWORD=… DB_DATABASE=…
504
663
  # at migration time.
505
664
  ```
506
665
 
666
+ ## TLS — honoured, or refused
667
+
668
+ Ask for TLS in the connection URL:
669
+
670
+ ```sh
671
+ DB_URL=mysql://app:app@db.internal:3306/app?sslmode=require # TLS, certificate not verified
672
+ DB_URL=mysql://app:app@db.internal:3306/app?ssl=true # the provider-style alias — same thing
673
+ DB_URL=mysql://app:app@db.internal:3306/app?sslmode=disable # explicit plaintext
674
+ ```
675
+
676
+ `require` maps to mysql2's `{ rejectUnauthorized: false }` — the connection is **encrypted, the server certificate is not authenticated**. That is the mode managed providers expect, and it is the same meaning `?sslmode=require` has on the postgres dialect.
677
+
678
+ **Anything else fails at boot**, on purpose: `prefer`, `allow`, `verify-ca`, `verify-full`, a CA-profile name, `?ssl=yes`. mysql2 either sends an SSLRequest or it does not, so `prefer` is not expressible; and a verification mode cannot round-trip through the cross-dialect boolean `ssl` field, so honouring the word would mean quietly giving you something weaker than it names. A boot failure is the correct answer to a TLS request that cannot be satisfied.
679
+
680
+ > **This changed.** Before this release the mysql/mariadb dialect had **no TLS path at all** — `?ssl=true` was parsed off the URL and discarded, and the connection went out in plaintext with no warning. If your URL carries `?ssl=` or `?sslmode=`, that request is now real: verify your server accepts TLS before deploying.
681
+
507
682
  ## Driver: `@effect/sql-mysql2`
508
683
 
509
- Wraps the `mysql2` driver (Node.js mysql client). The framework's `MysqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
684
+ Wraps the `mysql2` driver (Node.js mysql client). The framework's `MysqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10), with a 10 s bound on **establishing** a connection. Waiting for a busy one is **not** bounded: mysql2 has no timer for that phase, and the length ceiling it does offer (`ConnectionConfig.acquireQueueLimit`) is opt-in because mysql2 rejects past the limit synchronously — which turns any zero-delay acquire retry into an event-loop-starving spin. See the [acquire-timeout table](../dialects) for the full reasoning before you set it.
510
685
 
511
686
  The driver returns BOOLEAN as `0|1` (TINYINT(1) is MySQL's underlying type). The framework's `decodeRowsFromSchema` post-processor converts back to `true|false` for any column the schema registry declares as `boolean()`. Without this, every reactive subscription opening on a BOOLEAN column would see `1` instead of `true` and fail framework Schema validators.
512
687
 
@@ -709,6 +884,8 @@ makeMysqlStore({ sqlLayer, variant: 'mariadb', changeStrategy: 'cdc', cdcConfig
709
884
 
710
885
  The variant flows through to per-operation getters (`supportsInsertReturning`, `supportsDeleteReturning`, `supportsUpdateReturning`) and to the JSON-agg compiler's dialect branch.
711
886
 
887
+ TLS and the pool bounds are shared with MySQL, driver-for-driver: `?sslmode=require` / `?ssl=true` encrypts (certificate not verified), an unsupported mode fails at boot rather than falling back to plaintext, and the acquire wait is bounded by queue length because mysql2 has no time-based bound for it. See [MySQL → TLS](./mysql) and the [acquire-timeout table](../dialects).
888
+
712
889
  ## `UPDATE … RETURNING` does NOT exist — anywhere
713
890
 
714
891
  Despite MariaDB's broad RETURNING support — `INSERT … RETURNING *` since 10.5, `DELETE … RETURNING *` since 10.0 — there is **no UPDATE … RETURNING in any MariaDB version**. The framework's first attempt at supportsReturning treated the whole RETURNING family as one flag and emitted `UPDATE … RETURNING *` on mariadb, which fails with a parse error.
@@ -914,9 +1091,21 @@ DB_URL=mssql://sa:<password>@localhost:11433/voltro_test
914
1091
 
915
1092
  The framework's docker-compose at `voltro/test/docker-compose.yml` brings up MSSQL on `:11433` with an `mssql-init` sidecar that runs `CREATE DATABASE voltro_test` once the server is healthy. mssql doesn't have a `docker-entrypoint-initdb.d` equivalent, so the framework synthesizes one.
916
1093
 
1094
+ ## TLS — honoured, or refused
1095
+
1096
+ ```sh
1097
+ DB_URL=mssql://sa:<password>@sql.internal:1433/app?sslmode=require # encrypt, don't verify the certificate
1098
+ DB_URL=mssql://sa:<password>@sql.internal:1433/app?encrypt=1 # tedious' own spelling — same thing
1099
+ DB_URL=mssql://sa:<password>@sql.internal:1433/app?sslmode=disable # explicit plaintext
1100
+ ```
1101
+
1102
+ `require` maps to tedious `encrypt: true` + `trustServerCertificate: true` — encrypted, server certificate not authenticated, which is what lets a Developer-edition instance's self-signed certificate work. Unsupported modes (`prefer`, `verify-ca`, `verify-full`, `?encrypt=yes`) **fail at boot** rather than connecting with something weaker than they name.
1103
+
1104
+ > **This changed.** `@effect/sql-mssql` defaults `encrypt` to **false** (it overrides tedious' own `true`), and the framework never read the `ssl` config or the URL query — so every mssql session was plaintext, while a hard-coded `trustServerCertificate: true` made the configuration look TLS-aware. Unset still means plaintext (unchanged); a TLS request in the URL is now real.
1105
+
917
1106
  ## Driver: `@effect/sql-mssql`
918
1107
 
919
- Wraps `tedious` (the Node.js TDS driver). The framework's `MssqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
1108
+ Wraps `tedious` (the Node.js TDS driver). The framework's `MssqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10), with a 10 s bound on establishing a connection (`ConnectionConfig.acquireTimeoutMs`). Waiting for a BUSY pooled connection is not bounded here — the client's pool exposes no such knob; see the [acquire-timeout table](../dialects) for what each dialect can enforce.
920
1109
 
921
1110
  Tedious returns BIT as proper boolean (✓) and NVARCHAR as string. JSON columns are NVARCHAR(MAX) under the hood — driver returns the raw string. The framework's `decodeRowsFromSchema` JSON.parses any column declared as `json()` in the schema registry; without this, every reactive subscription reading a JSON column would see strings and fail framework Schema validators.
922
1111
 
@@ -47,6 +47,43 @@ await ctx.store.transactional(async (tx) => {
47
47
 
48
48
  `transactional()` **must not be nested** — a nested invocation throws to surface a design mistake. Use a single top-level `transactional()` per unit of work.
49
49
 
50
+ ## Typed errors survive a transaction
51
+
52
+ A `Data.TaggedError` thrown inside a transaction reaches the caller — and the browser client — **as itself**: `_tag`, payload and prototype intact, so a mutation's declared `error:` union still matches.
53
+
54
+ ```ts
55
+ import { Data } from 'effect'
56
+
57
+ class NoteNotFound extends Data.TaggedError('NoteNotFound')<{ noteId: string }> {}
58
+
59
+ // apps/api/mutations/notes.rename.mutation.ts — auto-transactional
60
+ export default async (input, ctx) => {
61
+ const note = await ctx.store.query({ table: 'notes', /* … */ })
62
+ if (!note) throw new NoteNotFound({ noteId: input.id }) // arrives typed on the client
63
+ // …
64
+ }
65
+ ```
66
+
67
+ On the client:
68
+
69
+ ```ts
70
+ const res = await client.notes.rename({ id, title })
71
+ if (res.error?._tag === 'NoteNotFound') { /* this branch fires */ }
72
+ ```
73
+
74
+ This holds on **every** dialect and on **both** tenancy topologies — shared-schema and namespace isolation — because all four dialect stores and both postgres entry points run through one shared transaction bracket. Never match on `error.message` to identify a transaction failure; the `_tag` is the contract.
75
+
76
+ ## Automatic retry on transient conflicts
77
+
78
+ A transaction that fails with a transient contention error is retried automatically: exponential backoff from 10 ms, up to 3 retries (4 attempts total). What counts as transient is per-dialect — postgres `serialization_failure` (40001) and `deadlock_detected` (40P01), mysql/mariadb deadlock and lock-wait timeout, mssql deadlock victim (1205), sqlite `SQLITE_BUSY`/`SQLITE_LOCKED`, and Turso's MVCC write-write conflict.
79
+
80
+ Two properties worth knowing:
81
+
82
+ - **A conflict raised by the COMMIT itself is retried too.** Under `SERIALIZABLE`, the engine can only detect some conflicts at commit time — those are caught and replayed like any other.
83
+ - **Each attempt gets a fresh transaction and a fresh event buffer.** A retried attempt's queued ChangeEvents are discarded with it, so subscribers see exactly one event set: the winning attempt's.
84
+
85
+ Because the body can run more than once, keep `transactional()` bodies **idempotent** — no counters incremented in JS, no external calls (see the anti-patterns below).
86
+
50
87
  ## Optimistic concurrency
51
88
 
52
89
  The fluent update builder carries an `.expectVersion(n)` guard: the update only matches rows whose `version` equals `n`, and throws `OptimisticLockError` when none match (the row was concurrently changed). Add a `version` column to the table to use it.
@@ -134,6 +171,37 @@ array is a no-op that returns `[]`.
134
171
  see each new row's delta — identical to N single inserts. Inside a
135
172
  `transactional()` the per-row events queue until commit and drop on a throw.
136
173
 
174
+ ### Large arrays are chunked for you — and stay all-or-nothing
175
+
176
+ Every engine caps what ONE statement may carry, and the caps are far apart:
177
+
178
+ | Dialect | Bind parameters per statement | Rows per `VALUES` |
179
+ | --- | --- | --- |
180
+ | postgres | 65 535 | — |
181
+ | mysql / mariadb | 65 535 | — |
182
+ | **mssql** | **2 098** | **1 000** |
183
+ | sqlite / turso | 32 766 | — |
184
+
185
+ `INSERT … VALUES` binds one parameter per **column per row**, so the row limit
186
+ is `floor(cap / columns)` — a 12-column table caps at 5 461 rows on postgres and
187
+ at **174** on mssql. Past that the driver refuses with its own error about a
188
+ limit you never chose.
189
+
190
+ `insertMany` splits the array for you at that boundary. Two properties are
191
+ guaranteed:
192
+
193
+ - **A fitting array is still ONE statement.** Nothing changes for the normal
194
+ case — no extra round-trips, no behaviour difference.
195
+ - **A chunked insert is still all-or-nothing.** When the call is not already
196
+ inside a `transactional()`, the chunks run in one transaction the framework
197
+ opens, so a failure in the last chunk rolls back the earlier ones. Without
198
+ that, chunking would quietly add partial-success-on-failure to a call that
199
+ never had it.
200
+
201
+ There is nothing to configure. If a SINGLE row is wider than the cap (a
202
+ 3 000-column table on mssql) the engine's own error is what you get — a row
203
+ cannot be split.
204
+
137
205
  ## `upsert` — insert or update on conflict
138
206
 
139
207
  The 90% case: "make this row exist with these values; if it already