@voltro/cli 0.33.0 → 0.34.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 (337) hide show
  1. package/CHANGELOG.md +1801 -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-BICVGnEC.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
  11. package/dist/appPort-B_HpJ_ck.js +48 -0
  12. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  13. package/dist/baselineCommand-DIttzO8A.js +227 -0
  14. package/dist/bin.js +71 -28
  15. package/dist/build-CE7Ca9gm.js +711 -0
  16. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  17. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  18. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  19. package/dist/checkCommand-YZDmkAGd.js +2 -0
  20. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  21. package/dist/cliError-BmdYnghb.js +10 -0
  22. package/dist/cliOutput-D1tSBoRM.js +15 -0
  23. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  24. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  25. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  26. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  27. package/dist/codegen-CYM3Zqrf.js +605 -0
  28. package/dist/codegen-ChBi_hVa.js +2 -0
  29. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  30. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  31. package/dist/commandRunner-BLAEFLjp.js +47 -0
  32. package/dist/commands-gutsz-Ac.js +808 -0
  33. package/dist/connectionConfig-UFlIEiys.js +66 -0
  34. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  35. package/dist/dataCommand-qL0r7fPO.js +535 -0
  36. package/dist/dataProfile-dW-PsfLB.js +15 -0
  37. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
  38. package/dist/dbCommand-CpdKLeQq.js +2 -0
  39. package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
  40. package/dist/dev-CoG-ZPx8.js +3 -0
  41. package/dist/devActivity-Dx_3nnGv.js +100 -0
  42. package/dist/devActivity.js +1 -1
  43. package/dist/dialectDriver-CgXnDfec.js +39 -0
  44. package/dist/discover-C9XKJDco.js +25 -0
  45. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  46. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
  47. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  48. package/dist/e2eCmd-BRabZww-.js +147 -0
  49. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  50. package/dist/envCommand-DPgdV1Bq.js +60 -0
  51. package/dist/evalCommand-6RUfPen4.js +118 -0
  52. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  53. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  54. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  55. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  56. package/dist/generateCommand-oibemh97.js +147 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.js +4 -3
  59. package/dist/infoCommand-BJw9nLUR.js +60 -0
  60. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  61. package/dist/inspect-C_T_WGvl.js +2 -0
  62. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  63. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  64. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  65. package/dist/loadEnv-D9nEOClM.js +44 -0
  66. package/dist/logFileSink-C_D2wRN1.js +105 -0
  67. package/dist/logsCmd-CCca3KRZ.js +260 -0
  68. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  69. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  70. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  71. package/dist/migrate-CBwOt_iV.js +83 -0
  72. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  73. package/dist/newCommand-COWOJ1_E.js +156 -0
  74. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  75. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  76. package/dist/pageConvention-cEiRxdab.js +5 -0
  77. package/dist/privacyCommand-C-Df56U_.js +146 -0
  78. package/dist/projectScaffold-DmzEKHib.js +2 -0
  79. package/dist/projectScaffold-LMMtaavR.js +814 -0
  80. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  81. package/dist/renderProfile-1OWWAAtx.js +81 -0
  82. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  83. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  84. package/dist/scheduleCmd--jksTrf6.js +69 -0
  85. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  86. package/dist/schemaIr-UJybUUZW.js +103 -0
  87. package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
  88. package/dist/seedRunner-TFHHiToI.js +329 -0
  89. package/dist/serveCommand-B_isw7q4.js +1647 -0
  90. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  91. package/dist/serveEntry.js +5 -5
  92. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  93. package/dist/start-9LiUOfES.js +1087 -0
  94. package/dist/start-B-9Nsp-S.js +3 -0
  95. package/dist/startEntry.js +2 -2
  96. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  97. package/dist/storageCommand-Co6NfLqN.js +42 -0
  98. package/dist/templates-De8IR5-c.js +102 -0
  99. package/dist/test-CI6iDsYc.js +115 -0
  100. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  101. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  102. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  103. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  104. package/dist/updateCommand-Bkptutss.js +585 -0
  105. package/dist/updateCommand-us1_hdIC.js +2 -0
  106. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
  107. package/dist/webDev-Cg-fFiyd2.js +2 -0
  108. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  109. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  110. package/dist/webhooksCommand-CID96Rga.js +267 -0
  111. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  112. package/package.json +179 -17
  113. package/templates/AGENTS.core.md +58 -3
  114. package/templates/AGENTS.md +64 -7
  115. package/templates/agent-docs/_index.md +6 -4
  116. package/templates/agent-docs/_manifest.json +22 -5
  117. package/templates/agent-docs/ai.md +370 -0
  118. package/templates/agent-docs/authentication.md +265 -31
  119. package/templates/agent-docs/caching.md +6 -0
  120. package/templates/agent-docs/cli.md +794 -50
  121. package/templates/agent-docs/data.md +550 -11
  122. package/templates/agent-docs/database/migrations.md +174 -25
  123. package/templates/agent-docs/database/misc.md +156 -40
  124. package/templates/agent-docs/database/querying.md +19 -1
  125. package/templates/agent-docs/database/scaling.md +60 -0
  126. package/templates/agent-docs/database/schema.md +1 -0
  127. package/templates/agent-docs/database/seedsdialects.md +208 -19
  128. package/templates/agent-docs/database/transactions.md +68 -0
  129. package/templates/agent-docs/deployment.md +69 -3
  130. package/templates/agent-docs/introduction.md +87 -16
  131. package/templates/agent-docs/local-first-mobile.md +79 -4
  132. package/templates/agent-docs/multi-tenancy.md +95 -20
  133. package/templates/agent-docs/observability.md +58 -3
  134. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  135. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  136. package/templates/agent-docs/plugins/audit.md +37 -1
  137. package/templates/agent-docs/plugins/auth-social.md +143 -0
  138. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  139. package/templates/agent-docs/plugins/auth.md +131 -6
  140. package/templates/agent-docs/plugins/billing.md +132 -15
  141. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  142. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  143. package/templates/agent-docs/plugins/duckdb.md +1 -1
  144. package/templates/agent-docs/plugins/flags.md +132 -0
  145. package/templates/agent-docs/plugins/governance.md +105 -7
  146. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  147. package/templates/agent-docs/plugins/presence.md +13 -2
  148. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  149. package/templates/agent-docs/plugins/search.md +157 -6
  150. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  151. package/templates/agent-docs/plugins/webhooks.md +105 -0
  152. package/templates/agent-docs/plugins.md +150 -18
  153. package/templates/agent-docs/reference.md +60 -3
  154. package/templates/agent-docs/releases.md +1117 -0
  155. package/templates/agent-docs/routing.md +43 -25
  156. package/templates/agent-docs/scheduling.md +10 -0
  157. package/templates/agent-docs/schema-driven-ui.md +92 -12
  158. package/templates/agent-docs/security.md +426 -0
  159. package/templates/agent-docs/templates/apibackends.md +87 -18
  160. package/templates/agent-docs/templates/appshells.md +32 -14
  161. package/templates/agent-docs/templates/overview.md +13 -8
  162. package/templates/agent-docs/testing.md +211 -14
  163. package/templates/agent-docs/whats-new.md +1700 -104
  164. package/templates/agent-docs/workflows.md +130 -14
  165. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  166. package/templates/apps/api-ai/package.json +8 -7
  167. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  168. package/templates/apps/api-auth/package.json +9 -8
  169. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  170. package/templates/apps/api-backend/package.json +12 -8
  171. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  172. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  173. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  175. package/templates/apps/api-backend-deactivation/package.json +8 -7
  176. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  177. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  178. package/templates/apps/api-backend-mail/package.json +9 -8
  179. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  180. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  181. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mariadb/package.json +10 -9
  183. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  185. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-sqlite/package.json +9 -8
  187. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  189. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  190. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  191. package/templates/apps/api-backend-storage/package.json +9 -8
  192. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  193. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  194. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  195. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  196. package/templates/apps/api-cms/app.config.ts +19 -0
  197. package/templates/apps/api-cms/authz.ts +63 -0
  198. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  199. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  201. package/templates/apps/api-cms/package.json +11 -10
  202. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  203. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  204. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  205. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  206. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  207. package/templates/apps/api-collab/package.json +9 -8
  208. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  209. package/templates/apps/api-data-advanced/package.json +9 -8
  210. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  211. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  212. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  213. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  214. package/templates/apps/api-durable/package.json +9 -8
  215. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  216. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  217. package/templates/apps/api-feature-flags/package.json +10 -9
  218. package/templates/apps/api-governance/README.md +30 -8
  219. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  220. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  221. package/templates/apps/api-governance/database/schema.ts +16 -4
  222. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  223. package/templates/apps/api-governance/package.json +9 -8
  224. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  225. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  226. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  227. package/templates/apps/api-kv/package.json +9 -8
  228. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  229. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  230. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  231. package/templates/apps/api-moderation/package.json +9 -8
  232. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  233. package/templates/apps/api-observability/package.json +9 -8
  234. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  235. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  236. package/templates/apps/api-ratelimit/package.json +9 -8
  237. package/templates/apps/api-rbac/package.json +9 -8
  238. package/templates/apps/api-rest/package.json +8 -7
  239. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  240. package/templates/apps/api-saas/package.json +12 -11
  241. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  242. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  243. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  244. package/templates/apps/api-saas-starter/authz.ts +75 -0
  245. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  246. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  247. package/templates/apps/api-saas-starter/package.json +15 -11
  248. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  249. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  250. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  251. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  252. package/templates/apps/api-search/package.json +9 -8
  253. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  254. package/templates/apps/api-status/README.md +10 -3
  255. package/templates/apps/api-status/app.config.ts +8 -3
  256. package/templates/apps/api-status/authz.ts +5 -3
  257. package/templates/apps/api-status/package.json +9 -8
  258. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  259. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  260. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  261. package/templates/apps/api-status/tests/status.test.ts +9 -1
  262. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  263. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  264. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  265. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  266. package/templates/apps/api-versioning/package.json +9 -8
  267. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  268. package/templates/apps/api-webhooks/package.json +10 -9
  269. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  270. package/templates/apps/changelog/package.json +8 -6
  271. package/templates/apps/edge-functions/package.json +2 -2
  272. package/templates/apps/frontend-admin/package.json +10 -8
  273. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  274. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  275. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  276. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  277. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  278. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  279. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  281. package/templates/apps/frontend-app/app.config.ts +4 -3
  282. package/templates/apps/frontend-app/package.json +11 -8
  283. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  284. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  285. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  286. package/templates/apps/frontend-auth/package.json +10 -8
  287. package/templates/apps/frontend-blank/package.json +9 -7
  288. package/templates/apps/frontend-cms/package.json +11 -9
  289. package/templates/apps/frontend-collab/package.json +12 -9
  290. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  291. package/templates/apps/frontend-contact/package.json +9 -7
  292. package/templates/apps/frontend-dashboard/package.json +9 -7
  293. package/templates/apps/frontend-docs/package.json +9 -7
  294. package/templates/apps/frontend-i18n/package.json +8 -6
  295. package/templates/apps/frontend-landing/package.json +9 -7
  296. package/templates/apps/frontend-portal/package.json +10 -8
  297. package/templates/apps/frontend-saas/app.config.ts +10 -6
  298. package/templates/apps/frontend-saas/package.json +10 -8
  299. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  300. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  301. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  303. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  304. package/templates/apps/frontend-spa/package.json +9 -7
  305. package/templates/apps/frontend-ssr/package.json +9 -7
  306. package/templates/apps/frontend-ssr-api/package.json +10 -8
  307. package/templates/apps/frontend-static-blog/package.json +8 -6
  308. package/templates/apps/frontend-status/package.json +10 -8
  309. package/templates/apps/mobile-app/README.md +1 -0
  310. package/templates/apps/mobile-app/package.json +4 -2
  311. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  312. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  313. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  314. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  315. package/templates/baselines/bare/.env.example +14 -0
  316. package/templates/baselines/bare/baseline.json +4 -4
  317. package/templates/baselines/compose/.env.example +14 -0
  318. package/templates/baselines/compose/README.md +1 -1
  319. package/templates/baselines/compose/baseline.json +5 -5
  320. package/templates/baselines/compose-mariadb/.env.example +14 -0
  321. package/templates/baselines/compose-mariadb/README.md +1 -1
  322. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  323. package/templates/baselines/helm/.env.example +14 -0
  324. package/templates/baselines/helm/baseline.json +4 -4
  325. package/dist/apiBuild-C-x9YacA.js +0 -2
  326. package/dist/checkCommand-DRovTKza.js +0 -2
  327. package/dist/commands-CJfepbm4.js +0 -11541
  328. package/dist/dbCommand-b1gum4td.js +0 -2
  329. package/dist/dev-iiMtlkfs.js +0 -3
  330. package/dist/devActivity-BhIu6ncs.js +0 -159
  331. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  332. package/dist/inspect-mmBuRXmy.js +0 -2
  333. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  334. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  335. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  336. package/dist/start-C0koT0UO.js +0 -1084
  337. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -15,11 +15,11 @@ The Voltro CLI is the single entry point for scaffolding, dev, build, and run. I
15
15
 
16
16
  ## Command quick-reference
17
17
 
18
- The dispatcher routes `voltro <command> [args]` to the matching subcommand and passes the rest through. The full set:
18
+ The dispatcher routes `voltro <command> [args]` to the matching subcommand and passes the rest through. **`voltro help` is the authority** — it prints the registry itself, so it cannot drift from what your installed CLI dispatches. The table below is every command that registry exposes, grouped by purpose:
19
19
 
20
20
  | Group | Commands |
21
21
  |---|---|
22
- | [Scaffolding](/docs/cli/scaffolding) | `create-project`, `add-app`, `list-templates`, `new` (scaffold one primitive — `query` / `mutation` / `action` / `workflow` / `page`) |
22
+ | [Scaffolding](/docs/cli/scaffolding) | [`init`](/docs/cli/scaffolding#voltro-init) (initialise the current directory as a workspace root — no apps), `create-project`, `add-app`, `list-templates`, `new` (scaffold one primitive — `query` / `mutation` / `action` / `workflow` / `page`) |
23
23
  | Packages | `package` (`create` / `publishable` / `private` / `status`), `create-package` |
24
24
  | [Dev](/docs/cli/dev) | `dev`, `codegen`, `typecheck` (`tsc --noEmit` with the app's own TypeScript), `agents-md`, [`env`](/docs/cli/env) (`check` / `sync` / `types` / `turbo`), [`generate`](/docs/cli/scaffolding) (AI app-builder), `dashboard` (serve the DevTools dashboard standalone; `--port`, `VOLTRO_DASHBOARD_APPS`) |
25
25
  | [Build & run](/docs/cli/build-and-start) | `build`, `start`, `serve` |
@@ -27,20 +27,61 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
27
27
  | [Database](/docs/cli/migrate) | `migrate`, `db` (`plan` / `apply` / `plans` / `drift` / `squash` / `restore-snapshot` / `migrate` / `rollback` / `status` / `seed`), [`evolve`](/docs/database/migrations/rename-and-drop) (schema-evolution copilot — propose a codemod + branch-verified backfill for a rename / retype / split / drop of an existing column or table) |
28
28
  | [Update](/docs/cli/update) | `update` (`--to` / `--dry-run` / `--force` / `--exact`) — bump every `@voltro/*`, install, run the codemods that adapt your source to the new version |
29
29
  | [Data transfer](/docs/cli/data) | `data` (`export` / `import` / `unpack` / `inspect` / `backup` / `restore`) — directory + single-file `.vbundle` bundles, streaming assets, masking, at-rest encryption |
30
- | Ops / infra | `cache` (`status` / `flush` / `invalidate`), `add` (`redis`), `baseline` (`list` / `status` / `set`), `schedule-manifest`, [`storage`](/docs/plugins/storage) (`doctor` / `cors`) |
30
+ | Ops / infra | `cache` (`status` / `flush` / `invalidate`), `add` (`redis`), `baseline` (`list` / `status` / `set`), `schedule` (`run <name>` — fire a registered schedule on demand; `--process` / `--trigger manual\|external` / `--url`), `schedule-manifest`, [`storage`](/docs/plugins/storage) (`doctor` / `cors`) |
31
31
  | AI / data | `embeddings backfill <table> --text <field> --vector <field>` — (re)embed rows the `vectorEmbedding()` mixin missed (pre-existing rows / a model change); `--dry-run` to preview; [`eval`](/docs/ai/agents#evaluating-recorded-runs-voltro-eval) — replay recorded agent runs against golden cases + judge, exit 1 on regression (a deploy gate; `--json` / `--branch` / `--threshold`) |
32
+ | Integrate | [`webhooks`](/docs/plugins/webhooks#voltro-webhooks-consumer--the-package-your-subscribers-install) (`consumer` / `events`) — generate the ZERO-dependency Standard-Webhooks verification package your subscribers install, from your own declared events (`--out` / `--name`); list the events a subscriber can register for (`--json`) |
32
33
  | [Inspect & debug](/docs/cli/inspect) | `inspect`, `logs`, `traces`, `workflows`, `cluster`, `check` |
33
34
  | [Health & surface](/docs/cli/build-and-start) | [`doctor`](/docs/cli/build-and-start) — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); [`capabilities`](/docs/cli/build-and-start) (`--json`) — the export surface read from your installed `@voltro/*`, so it can be verified instead of recalled; `info` (`--json`) — CLI / node / package-manager / dialect + every installed `@voltro/*` version, flagging lockstep skew (exits 1 on skew) |
34
35
  | Harness | `test`, `e2e` |
35
36
  | Cloud | `cloud` (`login` / `whoami` / `projects` / `env` / `import`) |
36
37
  | Secrets | `secret` (`generate [purpose]` — the right var+format per secret; `generate` alone → a generic secret; `list`) |
37
- | Meta | `agents-md`, `version`, `help` |
38
+ | Meta | `agents-md`, `telemetry` (reports that Voltro collects none — no phone-home, nothing to opt out of), `version`, `help` |
38
39
 
39
- Each command takes an optional path argument (the app directory) — defaults to `.` when run from inside an app. `voltro init` scaffolds a project (it delegates to the `create-project` scaffolder, forwarding `--api`/`--web`/`--baseline`/`--cache`). `voltro secret generate [purpose]` prints a cryptographically strong secret — with a purpose (`data-transfer`, `session`, `bundle-key`, `field-encryption`, `storage`, `inspect`) it emits the correct env var + length/format as a paste-ready `NAME=value` (`voltro secret list` shows them all); with no purpose, a generic base64url secret. `voltro telemetry` reports that Voltro collects none. `voltro deploy` shows the deploy paths — self-host via a [baseline](/docs/deployment/baselines) + CI, managed via the control-plane client `voltro cloud` (managed cloud deploy is coming soon — see [Voltro Cloud](/docs/deployment/voltro-cloud)), individual [serverless functions](/docs/deployment/serverless-functions) (`voltro serverless` → self-hosted Node, or Cloudflare / Scaleway), or a [static site](/docs/deployment/static-sites) (`voltro static` → Cloudflare Pages / S3 / Netlify).
40
+ **Two commands are hidden from `voltro help` on purpose** and are not in the
41
+ table: `prune-runtime <deploy-dir>` (trims a deployed web tree's `node_modules`
42
+ to the reachable runtime set — the standalone Dockerfiles run it for you) and
43
+ `_apply-codemods` (the re-exec target `voltro update` uses to run the target
44
+ version's codemods). Both are dispatchable; neither is something you invoke
45
+ directly.
46
+
47
+ Each command takes an optional path argument (the app directory) — defaults to `.` when run from inside an app. `voltro init` initialises the current directory as a workspace root — `pnpm-workspace.yaml`, a root `package.json` with `dev`/`build`/`test`/`typecheck`, a `.gitignore` and `git init`, idempotently and without scaffolding any apps (`voltro create-project <name>` does that, and bootstraps the same root when there isn't one). `voltro secret generate [purpose]` prints a cryptographically strong secret — with a purpose (`data-transfer`, `session`, `bundle-key`, `field-encryption`, `storage`, `inspect`) it emits the correct env var + length/format as a paste-ready `NAME=value` (`voltro secret list` shows them all); with no purpose, a generic base64url secret. `voltro telemetry` reports that Voltro collects none. `voltro deploy` shows the deploy paths — self-host via a [baseline](/docs/deployment/baselines) + CI, managed via the control-plane client `voltro cloud` (managed cloud deploy is coming soon — see [Voltro Cloud](/docs/deployment/voltro-cloud)), individual [serverless functions](/docs/deployment/serverless-functions) (`voltro serverless` → self-hosted Node, or Cloudflare / Scaleway), or a [static site](/docs/deployment/static-sites) (`voltro static` → Cloudflare Pages / S3 / Netlify).
40
48
 
41
49
  ## Help + version
42
50
 
43
- `voltro help` prints the command list; `voltro version` prints the CLI version. These are subcommands, not global flags — there is no global `--version` / `--help` parsing in the dispatcher.
51
+ `voltro help` prints the command list; `voltro version` prints the CLI version. Both are also reachable as bare flags — `voltro --help` / `-h` and `voltro --version` / `-v` handled by the dispatcher before it looks for a subcommand.
52
+
53
+ **Per-command help** is the useful one. `voltro <command> --help` (equivalently `voltro help <command>`) prints that command's usage line, its flags, the environment variables that change what it does, and worked examples:
54
+
55
+ ```bash
56
+ voltro dev --help
57
+ ```
58
+
59
+ ```text
60
+ voltro dev — Start the local development server (auto-discovers routes / mutations).
61
+
62
+ Usage
63
+ voltro dev [app]
64
+
65
+ Examples
66
+ voltro dev
67
+ boot the app in the current directory
68
+ voltro dev apps/acme/api
69
+ boot a specific app
70
+
71
+ Environment
72
+ PORT override the port from app.config.ts
73
+ WATCH=0 run the server directly, with no file-watching supervisor
74
+ WATCH_POLL=1 poll instead of using native FS events (bind-mounts: k8s hostPath / Docker)
75
+
76
+ ```
77
+
78
+ `--help` never runs the command. A command with subcommands of its own (`db`, `cloud`, `workflows`, `inspect`, `update`, …) prints its own richer page instead.
79
+
80
+ ## Startup
81
+
82
+ The CLI loads a command's implementation only when you dispatch it, so `voltro version`, `voltro info` and `voltro new` do not pay for the dev server, the build toolchain or the runtime. The same applies to the `voltro dev` supervisor's respawned child on every save.
83
+
84
+ The CLI's first line is its own — no Node warnings ahead of it.
44
85
 
45
86
  ## Common per-command flags
46
87
 
@@ -57,7 +98,11 @@ Commands like `build` / `start` / `migrate` / `codegen` parse no flags at all
57
98
 
58
99
  ## Common env vars
59
100
 
60
- The CLI reads:
101
+ The framework reads more than a hundred distinct `VOLTRO_*` / `DB_*` / `PG_*`
102
+ variables across its packages, so this is **not** the full set and could not
103
+ usefully be — it is the ones you reach for. Each subsystem's page carries its own; the complete transport-security
104
+ and connection lists live in [Production hardening](/docs/deployment/production-hardening)
105
+ and [Security](/docs/security/overview).
61
106
 
62
107
  | Var | Effect |
63
108
  |---|---|
@@ -72,6 +117,7 @@ The CLI reads:
72
117
  | `VOLTRO_INSPECT_TOKEN` | Bearer for the inspect surface. **Fail-closed:** unset → every request is `401`. `voltro dev` mints one per project; `voltro serve` / `voltro start` mint nothing, so a public deploy is closed by default (set it explicitly to open the surface). |
73
118
  | `VOLTRO_INSPECT_ALLOWED_HOSTS` | Extra `Host` names allowed to reach the **dev** inspect surface, past its DNS-rebinding guard (comma/space-separated). Loopback names + IP literals are always allowed; any other domain name is refused unless listed here — the api counterpart of vite's `allowedHosts`. |
74
119
  | `DB_URL` | Database connection string (falls back to `DB_PRIMARY_URL`; or the discrete `DB_*` / `PG_*` fields). |
120
+ | `DB_ACQUIRE_TIMEOUT_MS` | How long a request may wait for a free pooled connection before failing (default `10000`; `0` restores the driver's own unbounded wait). Read on every command and every dialect that can bound it. `DB_ACQUIRE_QUEUE_LIMIT` is its mysql/mariadb counterpart and is deliberately **unset** by default. |
75
121
  | `VOLTRO_SESSION_SECRET` | Session-cookie signing secret (`@voltro/plugin-auth`). Rotate with zero downtime: move the old value to `VOLTRO_SESSION_SECRET_PREVIOUS` for one session lifetime — cookies signed with either secret keep verifying, and previous-key cookies are re-issued under the new one. |
76
122
  | `VOLTRO_DATA_TRANSFER_SECRET` | Gates the prod data-transfer endpoints (`POST /_voltro/admin/{export,import}`); ≥16 chars or the routes don't mount. |
77
123
  | `VOLTRO_BUNDLE_KEY` | Passphrase for `.vbundle` export encryption (`voltro data export --encrypt`). A DEDICATED key, not the transfer secret. |
@@ -79,6 +125,24 @@ The CLI reads:
79
125
  | `VOLTRO_STORAGE_SECRET` | Signs storage grant tokens (private files); falls back to the session secret if unset. |
80
126
  | `AI_PROVIDER` / `AI_MODEL` / `AI_API_KEY` | Per-provider AI config. |
81
127
 
128
+ ### Transport security — the overrides on by default since 0.34.0
129
+
130
+ The api listener's cross-site check, proxy policy and security headers are **on
131
+ by default** and configured in `app.config.ts`'s `security` block. Each has an
132
+ env override, resolved identically under `dev` and `serve`, with the explicit
133
+ config value always winning:
134
+
135
+ | Var | Effect |
136
+ |---|---|
137
+ | `VOLTRO_ORIGIN_GUARD` | `off` disables the cross-site origin check. Anything else leaves the default `same-origin`. |
138
+ | `VOLTRO_ALLOWED_ORIGINS` | Comma-separated origins allowed past that check — what a web app on a different origin than the api needs. |
139
+ | `VOLTRO_TRUSTED_PROXIES` | Which hops may set `x-forwarded-for` / `x-forwarded-proto`: a comma-separated CIDR/IP list, `private`, `*` (trust the leftmost token), or a hop **count** (`2`). **Unset means the header is ignored entirely** and the socket address wins — set it if you run behind an ingress AND rate-limit or audit per IP. |
140
+ | `VOLTRO_SECURITY_HEADERS` | `off` / `default` / `strict` — the whole header bundle's mode. |
141
+ | `VOLTRO_CSP` | Content-Security-Policy for non-HTML api responses. `off` drops just this one. |
142
+ | `VOLTRO_CSP_HTML` | CSP for HTML responses (under `strict` it defaults to the same policy as `VOLTRO_CSP`). `off` drops just this one. |
143
+ | `VOLTRO_HSTS` | `Strict-Transport-Security` value. `off` drops just this one. |
144
+ | `VOLTRO_MAX_RPC_BODY_BYTES` | Cap on the buffered `POST /rpc` JSON body (default 8 MiB) — an oversized body is refused `413` and never buffered past the cap. File uploads ride plugin routes with their own limits. |
145
+
82
146
  Generate any of the secret vars above with `voltro secret generate <purpose>` (see [`secret`](#command-quick-reference)) — it picks the right length and format. A lower environment's secrets must always differ from production's.
83
147
 
84
148
  ## Workflow patterns
@@ -86,10 +150,10 @@ Generate any of the secret vars above with `voltro secret generate <purpose>` (s
86
150
  ### "I'm starting a new project"
87
151
 
88
152
  ```bash
153
+ mkdir acme && cd acme
89
154
  pnpx voltro create-project acme --api api-backend --web frontend-landing
90
- cd acme
91
155
  pnpm install
92
- pnpm dev # turbo runs voltro dev across every app in the project
156
+ pnpm dev # pnpm -r --parallel dev — voltro dev in every app at once
93
157
  ```
94
158
 
95
159
  ### "I want to add a docs site to my existing project"
@@ -97,7 +161,7 @@ pnpm dev # turbo runs voltro dev across every app in the project
97
161
  ```bash
98
162
  voltro add-app docs --template frontend-docs --to acme
99
163
  pnpm install # picks up the new app's deps
100
- pnpm dev # turbo adds the new app to its parallel boot
164
+ pnpm dev # the new app joins the parallel boot automatically
101
165
  ```
102
166
 
103
167
  ### "I changed my schema and want to apply it"
@@ -266,10 +330,52 @@ The HTTP surface is reachable directly too — e.g. `curl -s localhost:4000/_vol
266
330
  <!-- source: en/cli/scaffolding.md -->
267
331
  ## Scaffolding
268
332
 
269
- _create-project, add-app, list-templates — boot new code with the framework's conventions baked in._
333
+ _init, create-project, add-app, list-templates — boot new code with the framework's conventions baked in._
270
334
 
271
335
  The scaffolder generates new projects + new apps from templates. Each template is a dogfooded reference; what you scaffold is the same shape the Voltro Cloud team uses.
272
336
 
337
+ ## `voltro init`
338
+
339
+ ```bash
340
+ voltro init # takes no arguments — it initialises the CURRENT directory
341
+ ```
342
+
343
+ Turns the directory you are standing in into a **Voltro workspace root**, and
344
+ scaffolds no apps at all. That is the whole distinction from `create-project`:
345
+ `init` prepares the root, `create-project` fills it. They share one
346
+ implementation (`ensureWorkspaceRoot`), so a greenfield `create-project` needs
347
+ no separate `init` — it bootstraps the same root when there isn't one.
348
+
349
+ What it writes, all of it **idempotent and additive**:
350
+
351
+ | File | Behaviour |
352
+ |---|---|
353
+ | `pnpm-workspace.yaml` | Written only when the walk up finds no workspace at all. |
354
+ | `package.json` (root) | Created when missing — `private`, `type: 'module'`, node/pnpm engines, the detected `packageManager`, the four scripts, `typescript` + `@types/node`. When it exists, only the **missing** keys are filled in; a script or a version range you already declared is never rewritten. |
355
+ | `tsconfig.base.json` | Written when missing. Every app + package tsconfig the framework generates extends this exact path, so `tsc` fails before it reads your code without it. |
356
+ | `.gitignore` | Created when missing; otherwise only the entries it does not already cover are appended. Includes `.env.local` — where `voltro dev` mints per-project secrets, and which must never be committed. |
357
+ | `git init` | Only when nothing at or above the directory is already a git working tree. |
358
+
359
+ The four root scripts are the workspace fan-outs:
360
+
361
+ ```json
362
+ { "dev": "pnpm -r --parallel dev", "build": "pnpm -r build",
363
+ "test": "pnpm -r test", "typecheck": "pnpm -r typecheck" }
364
+ ```
365
+
366
+ Two refusals, both deliberate:
367
+
368
+ - **A positional argument is an error** (`init takes no arguments — it
369
+ initialises the current directory`), with a hint pointing at
370
+ `voltro create-project <name>`. `voltro init acme` reads like "make me a
371
+ project called acme", and it is not that command.
372
+ - **It will not nest a second root inside an existing pnpm workspace.** If an
373
+ ancestor already has a `pnpm-workspace.yaml`, it names that root and tells you
374
+ to run `create-project` from there instead.
375
+
376
+ A second run on an already-initialised root prints `is already a Voltro
377
+ workspace root — nothing to do.`
378
+
273
379
  ## `create-project`
274
380
 
275
381
  ```bash
@@ -286,6 +392,7 @@ Bootstraps a new project under `apps/<name>/` with selected templates.
286
392
  | `--baseline=<bare\|compose\|helm>` | prompts (or skip) | Deploy baseline to scaffold (`bare` / `compose` / `helm`). Without it, the interactive prompt lists the available ids. |
287
393
  | `--port-range <start>-<end>` | `5190-5199` | Port range for web apps in this project. Persisted in `project.json`. Also accepts `:` / `..` separators. |
288
394
  | `--no-input` | false | Skip prompts; suitable for CI / scripted scaffolding. |
395
+ | `--no-register` | false | Do not contact the Voltro Cloud control plane. See [registration](#project-registration) below. |
289
396
 
290
397
  What it does:
291
398
 
@@ -304,6 +411,34 @@ pnpm install
304
411
  pnpm dev
305
412
  ```
306
413
 
414
+ ### Project registration
415
+
416
+ `create-project` and `add-app` finish by registering the project with the Voltro Cloud control plane. This is how **self-hosted** use is counted, and the Terms of Service ask for projects and apps to be registered. It is worth knowing exactly when it happens and what it involves, because it is part of your first command.
417
+
418
+ **If you are not logged in, nothing is sent.** No network call is attempted at all, and the scaffolder says so:
419
+
420
+ ```text
421
+ → cloud registration: skipped — not logged in, so nothing was sent from this machine.
422
+ Registration is how SELF-HOSTED use is counted; the Voltro Cloud Terms of Service ask
423
+ for projects and apps to be registered once you have an account.
424
+ Register later: voltro cloud login then voltro cloud scan
425
+ Never ask again: scaffold with --no-register
426
+ ```
427
+
428
+ **If you are logged in**, it prints the destination and the contents before the call:
429
+
430
+ ```text
431
+ → registering project 'acme' with https://cloud.voltro.dev (self-hosted usage tracking, ToS-governed)
432
+ Sends: the project slug, and per app its name, kind, framework version and the NAMES of
433
+ declared primitives (queries, mutations, tables, plugins, …) plus a page count.
434
+ Never sends: source code, row data, environment values or secrets.
435
+ Skip with --no-register.
436
+ ```
437
+
438
+ Pass `--no-register` to skip it entirely — appropriate for offline work, CI, or while evaluating. You can register later with `voltro cloud login` followed by `voltro cloud scan`.
439
+
440
+ This is the only network call the CLI makes on its own behalf; `voltro telemetry` reports the rest of the picture (the framework collects nothing).
441
+
307
442
  ### The seeded agent guide
308
443
 
309
444
  `create-project` / `add-app` (and `voltro dev` on first boot) seed a guide that
@@ -543,6 +678,27 @@ lists the ones that reference none. It learns your own `require*` / `assert*`
543
678
  guard names, so it does not report the call sites of guards you already wrote.
544
679
  See [the authz scan](./build-and-start.md).
545
680
 
681
+ ### When the database is not reachable
682
+
683
+ A refused, unresolvable or rejected database connection is reported as a condition with a fix, not as a framework crash:
684
+
685
+ ```text
686
+ voltro: the database is not reachable at 127.0.0.1:5432 (ECONNREFUSED).
687
+
688
+ No database is configured — none of DB_URL / DB_HOST / PG_HOST is set in the
689
+ environment or in a loaded `.env`, so the framework used its local dev default.
690
+
691
+ Either start one: pnpm db:up (if your project ships a compose file)
692
+ or point at your own: DB_URL=postgres://user:pass@host:5432/dbname
693
+ — put it in `.env` next to app.config.ts, not just in one shell.
694
+
695
+ Re-run with --debug for the full stack.
696
+ ```
697
+
698
+ When a variable **is** set, the message names it and says the address resolved and nothing answered there — which is a different problem from "is postgres running", and points you at the container, VPN or firewall instead. A server that answers and rejects you (wrong password, missing database) is reported as its own case, because the fix is different again.
699
+
700
+ Pass `--debug` (or set `VOLTRO_DEBUG=1`) to get the full Effect stack instead.
701
+
546
702
  ## `voltro dev <appDir>`
547
703
 
548
704
  ```bash
@@ -741,20 +897,22 @@ A `<DevtoolsStringsProvider strings={…}>` mounted above the overlay works too;
741
897
  | `WATCH=0` | Disable filesystem watch. Useful under a parent watcher (Docker volume, devcontainer). |
742
898
  | `VOLTRO_DASHBOARD=off` | Don't auto-launch the dashboard. |
743
899
  | `VOLTRO_INSPECT=off` | Don't expose `/_voltro/inspect/*` endpoints. |
744
- | `PORT=4001` | Override the listen port (api or web). Both kinds read `PORT`, then `app.config.ts` `port:`, then the default. |
900
+ | `PORT=4001` | Override the listen port (api or web). See [Which port an app binds](#which-port-an-app-binds) for the full order. |
745
901
  | `VOLTRO_DASHBOARD_PORT=5180` | Override the auto-launched dashboard port (default `5179`). |
746
902
  | `VOLTRO_LOG_LEVEL=debug` | Verbose framework logs. |
747
903
  | `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY=4` | Max `ssr` pages compiled on demand at once (default `4`). Lower it (`1`/`2`) on a low-memory box if a burst of first-time `ssr` page loads spikes memory; raise it on a big machine. Warm (already-compiled) pages are never throttled. |
748
904
 
749
- ## Multi-app dev (turbo)
905
+ ## Multi-app dev
750
906
 
751
- The `pnpm dev` at the project root delegates to `turbo run dev` which boots every app in parallel:
907
+ The `dev` script the scaffolder writes at the workspace root is plain pnpm no task runner to install:
752
908
 
753
909
  ```bash
754
- pnpm dev # ↳ turbo runs `voltro dev` for every workspace app
910
+ pnpm dev # ↳ pnpm -r --parallel dev — `voltro dev` in every app at once
755
911
  ```
756
912
 
757
- Turbo's TUI mode shows one pane per app with live output. Set `"ui": "tui"` in `turbo.json` (the scaffolder does this for you).
913
+ `pnpm -r` selects by "has a `dev` script", so an app that owns its own dev loop (an Expo `mobile-app`, an `edge-functions` bundle) opts out simply by not defining one. To run a single app: `pnpm --filter @acme/api dev`.
914
+
915
+ If you prefer a task runner for its caching and per-app output panes, adding one is a normal workspace change — nothing in the framework depends on it.
758
916
 
759
917
  ## `voltro codegen <appDir>`
760
918
 
@@ -768,6 +926,24 @@ Regenerates `rpcGroup.generated.ts` (+ the web `.framework/*` entry) from the di
768
926
  - **CI environments** where you want the typed client baked into a tarball before tests run.
769
927
  - **Editor LSP confused** after a discovery pattern changed and the generated file went out of sync.
770
928
 
929
+ ### Staleness is detected, not assumed
930
+
931
+ The generated file carries a `source-fingerprint` of the descriptor tree, so the other commands can tell whether it still matches your code:
932
+
933
+ - **`voltro build`** regenerates it when it is stale. A CI build from a clean checkout never ran `voltro dev`, and it is a file the build can produce itself.
934
+ - **`voltro test`** REFUSES and tells you to run `voltro codegen`:
935
+
936
+ ```text
937
+ voltro test: …/rpcGroup.generated.ts is stale — a descriptor changed since the group was generated.
938
+ The tests would run against the previously generated procedure group, pass, and prove nothing
939
+ about the descriptors you just edited.
940
+ Run `voltro codegen` (or boot `voltro dev` once) and try again.
941
+ ```
942
+
943
+ It refuses rather than regenerating because regenerating means importing your app's modules and config as a side effect of asking to run tests, and silently rewriting a checked-in source file is worse than stopping.
944
+
945
+ The check reads bytes only — no app module is imported — so it costs milliseconds. A generated file written by an older framework version carries no stamp and reads as stale; run `voltro codegen` once.
946
+
771
947
  ## `voltro agents-md`
772
948
 
773
949
  ```bash
@@ -797,8 +973,22 @@ For api apps, Voltro applies its own discovery walker on every save. The pattern
797
973
 
798
974
  `*.tool.tsx` files are not discovered on their own — a tool is imported by the agent that uses it, so it's picked up through the agent file. Hidden dirs, `node_modules`, `dist`, and `.framework` are skipped.
799
975
 
976
+ Those names are matched as whole **path segments**, so a directory called `distribution/` or a file called `distTools.ts` is watched normally.
977
+
800
978
  For web apps, Vite's built-in HMR handles the watch.
801
979
 
980
+ ### Workspace packages are watched too
981
+
982
+ If your api depends on a workspace package (`"@acme/shared": "workspace:*"`), that package's `src/` is watched as well — editing `packages/shared/src/x.ts` restarts the api, exactly as editing a file inside the api would. The dependency set is resolved once at boot from the api's `package.json`, so adding a dependency needs a restart (it needs an install anyway).
983
+
984
+ Only real workspace packages are watched. A published npm dependency resolves inside `node_modules` and is skipped, so an app outside a monorepo watches nothing extra.
985
+
986
+ An edit in a dependency is logged with its package directory, not just the filename:
987
+
988
+ ```
989
+ file changed — restarting file=shared/src/x.ts
990
+ ```
991
+
802
992
  ## Restart triggers
803
993
 
804
994
  API apps restart (full process kill) on a change to **any source file**
@@ -819,6 +1009,19 @@ The restart is a full re-exec — there is no in-process hot-reload of a
819
1009
  handler body; editing a query's executor respawns the child (debounced
820
1010
  80ms, so a burst of saves collapses into one restart).
821
1011
 
1012
+ ### Reading the restart timing
1013
+
1014
+ The completion line is printed when the api **can serve a request** — not when the replacement process was spawned:
1015
+
1016
+ ```text
1017
+ file changed — restarting file=notes.list.query.ts
1018
+ restart complete — api ready ms=1840
1019
+ ```
1020
+
1021
+ The first boot reports the same measurement as `dev server ready`. If a restart never prints its completion line, the child did not come up — look for the crash above it, which the supervisor logs before it goes back to waiting for the next save.
1022
+
1023
+ That number is the whole wait: the process start, the module graph, discovery, codegen, the store connection, the boot schema diff and plugin boot. It is the number to quote if the inner loop feels slow.
1024
+
822
1025
  ### How the old process is stopped
823
1026
 
824
1027
  SIGTERM first. The child runs its teardown — plugin `onDeactivate`, the
@@ -892,6 +1095,32 @@ the env-file chain (app dir → ancestors) and hard-restarts, logging
892
1095
  web closes Vite before the replacement spawns, so the restart can't hit a
893
1096
  port-in-use race.
894
1097
 
1098
+ ## Which port an app binds
1099
+
1100
+ Every command that starts an app listener — `voltro dev`, `voltro serve`,
1101
+ `voltro start`, `voltro dormancy` — resolves the port the same way, in this
1102
+ order:
1103
+
1104
+ 1. `VOLTRO_DASHBOARD_PORT`, and only in the auto-launched dashboard process.
1105
+ 2. `PORT` from the environment.
1106
+ 3. `--port <n>` (`voltro serve`, `voltro dormancy`).
1107
+ 4. `port:` in the app's `app.config.ts`.
1108
+ 5. `4000` for an api app, `5173` for a web app.
1109
+
1110
+ `PORT` deliberately outranks `--port`: every host that assigns a port — a
1111
+ container platform, a PaaS, a Kubernetes Deployment — assigns it through `PORT`,
1112
+ and a `--port` baked into an image's start command must not override the port the
1113
+ host actually routed to.
1114
+
1115
+ A value that is not a port in `1..65535` (`PORT=`, `PORT=8080x`) is **ignored**
1116
+ with a warning naming the variable, and the next source wins. It is not passed to
1117
+ `listen()`: `Number('8080x')` is `NaN`, node reads that as "any free port", and
1118
+ the app would come up healthy at an address nobody can guess.
1119
+
1120
+ `voltro dev` does not take `--port`. Its file-watching supervisor respawns the
1121
+ app as `dev <app>` and drops flags, so a `--port` would silently stop applying at
1122
+ the first file change; set `PORT` for a one-off, or `port:` to keep it.
1123
+
895
1124
  ## Multiple instances on one machine
896
1125
 
897
1126
  Run two api apps + two web apps in parallel? Every app reads the same `PORT` env, so prefer setting each app's `port:` in its own `app.config.ts` and `--cwd`-ing into each — that avoids one shared `PORT` clobbering them all. The dashboard auto-launches once on `:5179`; later instances see it's already up and skip it.
@@ -904,7 +1133,7 @@ voltro dev apps/acme/docs &
904
1133
  voltro dev apps/orbit/web &
905
1134
  ```
906
1135
 
907
- If you must override per-process from the shell, set `PORT` inline on each one (`PORT=4001 voltro dev apps/acme/api`) — but the config-file port is the cleaner path. `pnpm dev` at the repo root handles all of this for you via turbo.
1136
+ If you must override per-process from the shell, set `PORT` inline on each one (`PORT=4001 voltro dev apps/acme/api`) — but the config-file port is the cleaner path. `pnpm dev` at the workspace root handles all of this for you.
908
1137
 
909
1138
  ## Anti-patterns
910
1139
 
@@ -999,7 +1228,7 @@ What it does:
999
1228
  | Flag / env | Notes |
1000
1229
  |---|---|
1001
1230
  | `PORT=8080` | Override the listen port. |
1002
- | `SSR_CACHE=postgres` | Use the Postgres-backed ISR cache. Requires the `PG_*` connection env (`PG_HOST` etc.) to also be set without it, `voltro start` stays on the in-memory cache. Default is in-memory. |
1231
+ | `SSR_CACHE=postgres` | Use the Postgres-backed ISR cache. Requires the web process to also have a database in its environment — `DB_URL` (what the templates set), `DB_PRIMARY_URL`, `DB_HOST` or `PG_HOST`. Without one, `voltro start` **aborts** on `NODE_ENV=production`/`staging` and warns loudly elsewhere; it no longer falls back to the in-memory cache in silence. Default is in-memory. |
1003
1232
  | `VOLTRO_INSPECT=off` | Disable the inspect HTTP endpoints in production. |
1004
1233
  | `VOLTRO_INSPECT_TOKEN=…` | Bearer token guard on the inspect endpoints. |
1005
1234
 
@@ -1026,12 +1255,23 @@ The response includes a `x-voltro-rendered-by` header (`prerender` / `ssr` / `is
1026
1255
 
1027
1256
  ```bash
1028
1257
  SSR_CACHE=memory voltro start # default — per-process, doesn't survive restart
1029
- # Postgres-backed cache — needs the PG_* connection env too:
1258
+ # Postgres-backed cache — the web process needs a database in its env. Any of
1259
+ # the usual variables works; DB_URL is what the templates set.
1260
+ SSR_CACHE=postgres DB_URL=postgres://… voltro start
1030
1261
  SSR_CACHE=postgres PG_HOST=… PG_PORT=… PG_USER=… PG_PASSWORD=… PG_DATABASE=… voltro start
1031
1262
  ```
1032
1263
 
1033
1264
  For multi-instance + horizontal scale → Postgres. The cache table is auto-created on first boot.
1034
1265
 
1266
+ > **This used to recognise `PG_HOST` and nothing else.** An app configured the
1267
+ > documented way — `SSR_CACHE=postgres` plus `DB_URL` — silently got the
1268
+ > per-process memory cache, announced as `isr cache backend: memory
1269
+ > (per-process)`: an info line that reads like the default rather than like a
1270
+ > refusal. Both the cache and the CDC invalidator go through the same connection
1271
+ > resolver as everything else now, so `DB_URL` / `DB_PRIMARY_URL` / `DB_HOST` /
1272
+ > `PG_HOST` all work — and `PG_SSL` comes with them. Asking for the postgres
1273
+ > cache and getting memory is now a boot failure in production, not a log line.
1274
+
1035
1275
  ## Tenant-aware ISR
1036
1276
 
1037
1277
  Pages with `tenantAware: true` get separate cache entries per tenant. The cache key becomes `<pathname>|tenant=<tenantId>`. See [Render modes](/docs/routing/render-modes).
@@ -1040,6 +1280,12 @@ Pages with `tenantAware: true` get separate cache entries per tenant. The cache
1040
1280
 
1041
1281
  For pages with `cacheInvalidatesOn: ['table', …]`, `voltro start` reads Postgres logical replication. Writes to listed tables invalidate every matching cache entry. Requires `SSR_CACHE=postgres` + `wal_level=logical`.
1042
1282
 
1283
+ If routes declare `cacheInvalidatesOn` and the web process has no database in
1284
+ its environment, boot now WARNS and names those routes — they fall back to plain
1285
+ `revalidate` staleness. That gap used to be reported at `debug`, which is
1286
+ invisible at the default level and indistinguishable from live invalidation
1287
+ working.
1288
+
1043
1289
  ## Graceful shutdown
1044
1290
 
1045
1291
  `voltro start` handles SIGTERM:
@@ -1115,6 +1361,35 @@ Docker step) and prints the exact remedy: add a `voltro build .` step before
1115
1361
  `voltro serve .`. Drop it into your image build right after `voltro build` to
1116
1362
  guarantee the artefact is present before the image ships.
1117
1363
 
1364
+ ### The access-decision gate
1365
+
1366
+ Before the authz scan below — which is a heuristic over executor SOURCE — doctor
1367
+ runs the same **gate the boot runs**: every wire-exposed procedure must declare
1368
+ `guards:` or `openAccess:`. It is not advisory and not a ratchet, because a green
1369
+ answer here means the app starts:
1370
+
1371
+ ```
1372
+ access decisions · security.defaultDeny ON
1373
+ ✗ no access decision 3
1374
+ ✓ openAccess, declared on purpose 2
1375
+ pricing.current — public pricing page, reads no caller data
1376
+ status.ping — health probe
1377
+
1378
+ ✗ invoices.list (query)
1379
+ src/api/invoices.query.ts
1380
+ ```
1381
+
1382
+ Every undecided procedure is listed — never a prefix — and the same set is in
1383
+ `voltro doctor --json` under `accessDecisions` for a CI gate:
1384
+
1385
+ ```bash
1386
+ voltro doctor --json | jq '.accessDecisions.undecided[] | {tag, kind, file}'
1387
+ ```
1388
+
1389
+ An app that sets `security: { defaultDeny: false }` still gets the list, marked
1390
+ advisory, and doctor does not fail on it. Detail:
1391
+ [Authorization](/docs/authentication/authorization).
1392
+
1118
1393
  ### The authz scan
1119
1394
 
1120
1395
  `voltro doctor` answers one mechanical question over every executor: **does it
@@ -1612,6 +1887,13 @@ voltro migrate apps/api # explicit app directory (defaults to cwd)
1612
1887
 
1613
1888
  `voltro migrate` forwards its arguments to `voltro db apply`, so the same flags apply; `--create-only` selects the bootstrap-only emitter instead. The diff / plan / apply / drift / squash workflow lives under `voltro db` (see below).
1614
1889
 
1890
+ `--create-only` creates the SAME set of tables every other command declares: your
1891
+ entities, the feature-mix framework tables, the agent-thread tables when the app
1892
+ has an `*.agent.tsx`, and every plugin's `extendSchema.tables` — plus each
1893
+ plugin's `extendSchema.migrations` afterwards. Before 0.34.0 it assembled that
1894
+ set itself and got the last two wrong, so bootstrapping a fresh database the
1895
+ documented way produced one with no plugin tables at all.
1896
+
1615
1897
  For the deep dive on the schema DSL + day-to-day patterns, see [Database / Migrations](/docs/database/migrations).
1616
1898
 
1617
1899
  ## How discovery works
@@ -1654,14 +1936,28 @@ voltro db plan # diff declared schema vs live, color-
1654
1936
  voltro db plan --json # machine-readable for CI / PR comments
1655
1937
  voltro db plan --sql # raw DDL preview
1656
1938
  voltro db plan --against <url> # diff vs a REMOTE env via /_voltro/inspect/migrations
1657
- voltro db apply # execute the plan (dev onlyrefuses NODE_ENV=production)
1939
+ voltro db apply # execute the plan (refuses NODE_ENV=productionand an UNSET NODE_ENV resolves to production)
1658
1940
  voltro db apply --plan plan.json # prod: apply a pre-reviewed plan from CI/CD
1659
1941
  voltro db plans [--limit 20] # plan history from _voltro_migration_plans
1942
+ voltro db branch --pr <n> # REHEARSE the plan on a throwaway branch of the live schema
1660
1943
  voltro db drift # alert if live diverged from the latest applied fingerprint
1661
1944
  voltro db squash --before <iso-date> # consolidate history into one snapshot
1662
1945
  voltro db restore-snapshot <plan-id> # restore soft-dropped columns from a plan
1663
1946
  ```
1664
1947
 
1948
+ > **Every `voltro db …` / `voltro migrate` invocation declares its
1949
+ > environment.** An unset `NODE_ENV` resolves to `production` for these
1950
+ > commands — the same way it does for `voltro serve` and `voltro start` — so a
1951
+ > bare `voltro db apply` with no `NODE_ENV` refuses (exit 3) rather than
1952
+ > applying an un-reviewed diff. Locally: `NODE_ENV=development voltro db apply`,
1953
+ > or put `NODE_ENV=development` in your `.env` (a declared value always wins).
1954
+ > `voltro dev` declares `development` for itself and needs nothing.
1955
+ >
1956
+ > The second reason it matters is not the refusal: `_voltro_traces` and
1957
+ > `_voltro_undo_log` are created only outside production, so a migration command
1958
+ > that resolved the environment differently from the serving process **declared
1959
+ > a different schema** — and the declared set is what the fingerprint hashes.
1960
+
1665
1961
  A separate file-based migration surface (the offline escape hatch) lives alongside it:
1666
1962
 
1667
1963
  ```bash
@@ -1692,9 +1988,26 @@ For zero-downtime deploys with breaking schema changes:
1692
1988
 
1693
1989
  The framework doesn't enforce these — that's an SRE responsibility. See [Migrations](/docs/database/migrations) for the playbook.
1694
1990
 
1991
+ ## Rehearsing a migration before it reaches production — `voltro db branch`
1992
+
1993
+ `voltro db plan` tells you what the diff IS. `voltro db branch` tells you what it
1994
+ DOES: it branches the live schema into a throwaway namespace, applies the plan
1995
+ there (destructive operations included — the branch is disposable, so the
1996
+ operation most likely to fail is the one that actually gets rehearsed), re-plans
1997
+ to prove the migration converges, and drops the branch.
1998
+
1999
+ ```bash
2000
+ voltro db branch --pr 128 --json > rehearsal.json # exit 2 = the plan destroys data
2001
+ ```
2002
+
2003
+ Postgres only, and it says so on the other dialects rather than emitting Postgres
2004
+ syntax at them. Full behaviour, exit codes and the `--seed` trade-off:
2005
+ [Data branching](/docs/database/branching).
2006
+
1695
2007
  ## See also
1696
2008
 
1697
2009
  - [Database / Migrations](/docs/database/migrations) — the schema DSL deep dive
2010
+ - [Data branching](/docs/database/branching) — `voltro db branch` + the branch primitive
1698
2011
  - [Self-hosting](/docs/deployment/self-hosting) — production migration patterns
1699
2012
 
1700
2013
 
@@ -1837,7 +2150,16 @@ curl -s localhost:$PORT/_voltro/inspect/rpc | jq # api: every query / mut
1837
2150
  curl -s localhost:$PORT/_voltro/inspect/metrics | jq # rolling per-tag latency + invocation count
1838
2151
  ```
1839
2152
 
1840
- There is no `/_voltro/inspect/queries` endpoint. The registered GET surface is `app`, `routes` (web-only), `cache` (web-only), `rpc` (api-only), `metrics`, and `subscriptions` (api-only) — `rpc` is the procedure list, `routes` is the web page tree.
2153
+ There is no `/_voltro/inspect/queries` endpoint. The registered GET surface is `app`, `routes` (web-only), `cache` (web-only), `rpc` (api-only), `metrics`, `subscriptions` (api-only), `checks` (api-only) and `agent/tools` (api-only) — `rpc` is the procedure list, `routes` is the web page tree.
2154
+
2155
+ ### Invariant checks + the agent-tool surface
2156
+
2157
+ ```bash
2158
+ curl -s localhost:$PORT/_voltro/inspect/checks | jq # browser-safety, procedure-access, convergence, serverOnly
2159
+ curl -s localhost:$PORT/_voltro/inspect/agent/tools | jq # the exposeAsTool procedures an agent may run
2160
+ ```
2161
+
2162
+ `checks` runs the framework's own invariant checks and answers `pass | fail | unavailable` per check — `unavailable` means THIS process cannot answer it (a deployed `voltro serve` has no source tree to walk) and is never a pass. `agent/tools` lists the policy-admitted agent tools, and its sibling `POST /_voltro/inspect/agent/call` executes one; both are off until `agents: { mcp: true }`, and the call additionally needs the write credential plus an app credential on `x-voltro-agent-authorization`. See the [MCP server](/docs/cli/mcp) page for the full gate list.
1841
2163
 
1842
2164
  What the surface reads:
1843
2165
 
@@ -1988,6 +2310,19 @@ voltro test --coverage --reporter=junit --outputFile=reports/junit.xml
1988
2310
  That covers coverage numbers and a JUnit report for a merge-request widget, which
1989
2311
  is what most pipelines want beyond the exit code.
1990
2312
 
2313
+ **`--coverage` needs a provider package.** vitest ships coverage providers as
2314
+ *optional* peer dependencies, so nothing installs one for you. Every app
2315
+ scaffolded by `voltro create-project` / `voltro add-app` already declares
2316
+ `@vitest/coverage-v8` beside vitest; an older project adds it once:
2317
+
2318
+ ```bash
2319
+ pnpm add -D @vitest/coverage-v8 # or --coverage.provider=istanbul → @vitest/coverage-istanbul
2320
+ ```
2321
+
2322
+ `voltro test` checks for it *before* booting vitest and refuses with that
2323
+ install command, because vitest's own failure (`Cannot find dependency
2324
+ '@vitest/coverage-v8'`) names neither the flag nor the fix.
2325
+
1991
2326
  The framework keeps three decisions for itself and they win over a forwarded
1992
2327
  flag: the **root** (a positional that is an existing directory, which vitest
1993
2328
  would otherwise read as a filter), `--watch`, and `passWithNoTests` — an explicit
@@ -2015,25 +2350,68 @@ voltro e2e apps/web # explicit web app directory
2015
2350
 
2016
2351
  1. `voltro dev` for the api app.
2017
2352
  2. `voltro dev` for the web app.
2018
- 3. Playwright runs `*.e2e.ts` files.
2353
+ 3. Runs every file matching `e2e/**/*.spec.ts`, one process each, as a **plain tsx script** (`node --import tsx <file>`).
2019
2354
  4. Tear down: stops the boot processes.
2020
2355
 
2021
- The e2e files use Playwright's API directly:
2356
+ **There is no test runner and no browser driver here.** A spec is an ordinary TypeScript program: it runs top to bottom, and a non-zero exit code (an uncaught throw, `process.exit(1)`, a failed `node:assert`) is a failed file. The framework ships no `describe`/`it`, no `page` fixture, no reporter, no sharding, and no browser — because what `voltro e2e` actually contributes is the *lifecycle*, and the lifecycle is the same whichever driver you pick.
2357
+
2358
+ Two environment variables are handed to every spec:
2359
+
2360
+ | Variable | Value |
2361
+ |---|---|
2362
+ | `WEB_URL` | `http://localhost:<webPort>` — the booted web app |
2363
+ | `API_URL` | `http://localhost:<apiPort>` — the booted api app |
2364
+
2365
+ A spec that only needs the API is just `fetch` plus `node:assert`:
2022
2366
 
2023
2367
  ```ts
2024
- // apps/web/tests/signup.e2e.ts
2025
- import { test, expect } from '@playwright/test'
2026
-
2027
- test('signup creates a user', async ({ page }) => {
2028
- await page.goto('/signup')
2029
- await page.fill('[name=email]', 'a@b.com')
2030
- await page.fill('[name=password]', 'correct horse battery staple')
2031
- await page.click('button[type=submit]')
2032
- await expect(page).toHaveURL(/\/dashboard/)
2368
+ // apps/web/e2e/signup.spec.ts
2369
+ import assert from 'node:assert/strict'
2370
+
2371
+ const res = await fetch(`${process.env.API_URL}/v1/signup`, {
2372
+ method: 'POST',
2373
+ headers: { 'content-type': 'application/json' },
2374
+ body: JSON.stringify({ email: 'a@b.com', password: 'correct horse battery staple' }),
2033
2375
  })
2376
+
2377
+ assert.equal(res.status, 200)
2378
+ console.log('✓ signup accepted')
2379
+ ```
2380
+
2381
+ To drive a real browser, bring your own driver and launch it inside the spec — the framework does not choose one for you, and does not install one:
2382
+
2383
+ ```ts
2384
+ // apps/web/e2e/signup-browser.spec.ts
2385
+ import assert from 'node:assert/strict'
2386
+ import { chromium } from 'playwright-core' // your dependency, not the framework's
2387
+
2388
+ const browser = await chromium.launch()
2389
+ const page = await browser.newPage()
2390
+ await page.goto(`${process.env.WEB_URL}/signup`)
2391
+ await page.fill('[name=email]', 'a@b.com')
2392
+ await page.fill('[name=password]', 'correct horse battery staple')
2393
+ await page.click('button[type=submit]')
2394
+ await page.waitForURL(/\/dashboard/)
2395
+ assert.ok(page.url().includes('/dashboard'))
2396
+ await browser.close()
2034
2397
  ```
2035
2398
 
2036
- The framework's `voltro e2e` handles the lifecycle for you: it boots the api + web siblings, runs Playwright against them, and tears the processes down on exit. Per-test isolation, fixtures, reporters, and sharding are configured in your Playwright config — `voltro e2e` itself forwards no flags to Playwright.
2399
+ Configure the boot in `app.config.ts`:
2400
+
2401
+ ```ts
2402
+ export default {
2403
+ type: 'web' as const,
2404
+ name: 'web',
2405
+ e2e: {
2406
+ apiDir: '../api', // relative to the web app root
2407
+ pattern: 'e2e/**/*.spec.ts',
2408
+ apiPort: 4000,
2409
+ webPort: 5173,
2410
+ },
2411
+ }
2412
+ ```
2413
+
2414
+ Anything below the browser — a handler, a guard, a REST route, an `Idempotency-Key` replay, the `x-tenant` header — is faster and more precise from a [request-level test](/docs/testing/unit-testing#request-level-testing-maketestapp), which needs no booted process at all. Reach for `voltro e2e` when the thing under test *is* the two processes talking to each other.
2037
2415
 
2038
2416
  ## Securing the inspect surface
2039
2417
 
@@ -2606,20 +2984,22 @@ flag is the one-off override.
2606
2984
  <!-- source: en/cli/mcp.md -->
2607
2985
  ## MCP server (voltro-mcp)
2608
2986
 
2609
- _Wire a running Voltro api into Claude Code / Cursor as an MCP server — read-only tools, resources and prompts over the app's procedures, tables, workflows and their JSON Schemas, over stdio or Streamable HTTP._
2987
+ _Wire a running Voltro api into Claude Code / Cursor as an MCP server — read the app's procedures, tables, workflows and JSON Schemas, EXECUTE the procedures you expose as agent tools under your app's own permissions, and run the framework's invariant checks. Over stdio or Streamable HTTP._
2610
2988
 
2611
2989
  `@voltro/mcp` ships two standalone bins — **`voltro-mcp`** (stdio) and **`voltro-mcp-http`** (Streamable HTTP) — that serve a running api's capability manifest to a coding agent over the Model Context Protocol. The agent can then discover what the backend exposes — every rpc procedure with its input/output JSON Schema, the user tables, the workflows, the schema-driven-UI widget kinds — before writing UI or agent code.
2612
2990
 
2613
- It is **read-only metadata**: nothing here can execute a procedure or return row data. The bins talk to the same `GET /_voltro/inspect/manifest` endpoint the [inspect surface](/docs/cli/inspect) exposes, and honour its token gate. The server advertises three MCP capabilities — **tools**, **resources**, and **prompts**.
2991
+ Discovery is **read-only metadata** and is what you get with nothing configured beyond a URL: the bins talk to the same `GET /_voltro/inspect/manifest` endpoint the [inspect surface](/docs/cli/inspect) exposes, and honour its token gate. Two further surfaces are **off until you turn them on** — executing your app's agent tools, and the invariant checks. Both are covered below. The server advertises three MCP capabilities — **tools**, **resources**, and **prompts**.
2614
2992
 
2615
2993
  ## Setup
2616
2994
 
2617
- Two environment variables, both optional:
2995
+ Four environment variables. The first two cover read-only discovery; the last two are what an executing tool call needs.
2618
2996
 
2619
2997
  | Var | Default | Notes |
2620
2998
  |---|---|---|
2621
2999
  | `VOLTRO_INSPECT_URL` | `http://localhost:4000` | Base URL of the running api. |
2622
- | `VOLTRO_INSPECT_TOKEN` | _(unset)_ | Sent as `Authorization: Bearer <token>` when the inspect surface is gated. |
3000
+ | `VOLTRO_INSPECT_TOKEN` | _(unset)_ | Sent as `Authorization: Bearer <token>`. The inspect surface is fail-closed, so without it every call is a 401. |
3001
+ | `VOLTRO_INSPECT_WRITE_TOKEN` | _(unset)_ | Required to EXECUTE an app tool — a tool call is a non-GET inspect request, and those need a second credential. Read-only discovery does not use it. |
3002
+ | `VOLTRO_AGENT_TOKEN` | _(unset)_ | The **app** credential a tool call executes as. Never the inspect token — see below. |
2623
3003
 
2624
3004
  ### Claude Code
2625
3005
 
@@ -2654,6 +3034,9 @@ claude mcp add voltro --env VOLTRO_INSPECT_URL=http://localhost:4001 -- npx -y @
2654
3034
  | `voltro_get_table` | One table's full column list (types, nullability, FK targets, enums). |
2655
3035
  | `voltro_list_workflows` | The registered durable workflows. |
2656
3036
  | `voltro_list_widgets` | The schema-driven-UI widget kinds. |
3037
+ | `voltro_check_invariants` | The framework's invariant checks against the running app — see below. |
3038
+
3039
+ Plus one `app_<procedure>` tool per procedure your app exposes as an agent tool AND its policy admits — see the next section. Those are the only tools that execute anything.
2657
3040
 
2658
3041
  ## The resources
2659
3042
 
@@ -2675,6 +3058,94 @@ Reusable MCP **prompt templates** that render against the _live_ manifest, so th
2675
3058
  | `explain_table` | `table` | The table's schema + the procedures that read/write it. |
2676
3059
  | `wire_ui_for_procedure` | `tag` | A brief to call one procedure and render its result, embedding its real input/output schema. |
2677
3060
 
3061
+ ## Executing your app's procedures
3062
+
3063
+ A procedure annotated `exposeAsTool` can be **called** by the agent — the tool body is the real rpc handler, run under a `Subject` your app's own auth chain resolved. So the agent's ceiling is that subject's permissions, by construction: there is no second authorization path, because there is no second path. A guard that refuses the subject refuses the agent.
3064
+
3065
+ It is off until you say otherwise, at five independent gates:
3066
+
3067
+ ```ts
3068
+ // app.config.ts
3069
+ export default {
3070
+ agents: {
3071
+ tools: { allow: ['todos.*'], deny: ['*.purge'], includeWrites: true },
3072
+ mcp: true,
3073
+ },
3074
+ }
3075
+ ```
3076
+
3077
+ ```ts
3078
+ // mutations/todos.create.mutation.ts
3079
+ export const descriptor = defineMutation({
3080
+ name: 'todos.create',
3081
+ input: Schema.Struct({ title: Schema.String }),
3082
+ guards: [{ scope: 'todos:write' }],
3083
+ exposeAsTool: { description: 'Create a todo for the signed-in user', confirm: false },
3084
+ })
3085
+ ```
3086
+
3087
+ 1. **`agents.mcp: true`.** Not implied by anything else. Having an inspect token is not consent to let an agent execute procedures.
3088
+ 2. **`VOLTRO_INSPECT_TOKEN`** — the transport is fail-closed; `voltro dev` mints one per project, `voltro serve` mints nothing.
3089
+ 3. **`VOLTRO_INSPECT_WRITE_TOKEN`** + the `x-voltro-inspect-write` header. A tool call is a POST, and every non-GET inspect request already needed a second credential. An existing deployment with only the read token therefore executes nothing.
3090
+ 4. **`VOLTRO_AGENT_TOKEN`** — the app credential the call executes AS, sent on its own `x-voltro-agent-authorization` header. **Required.** The inspect bearer is an operator credential; letting it double as an app identity would be exactly the second authorization path, and running as the anonymous subject instead would execute under a principal nobody chose. With [`apiKeys: true`](/docs/configuration/api-keys) your app already mints a scoped credential for this — scope it to what the agent may do, not to what you may do.
3091
+ 5. **`agents.tools`** — the same `AppToolPolicy` the in-process [`appTools`](/docs/ai/tools) loop takes, so one policy covers both. `deny` beats `allow`; `includeWrites: true` is required before any mutation or action is callable at all.
3092
+
3093
+ Then the app's own guards run. Nothing above replaces them.
3094
+
3095
+ ### `confirm` tools are not mounted here
3096
+
3097
+ `confirm` means a human approves the concrete call before it executes. There is no human in the MCP server's process, and there is no way to produce one: a confirmation carried in the tool's arguments is written by the model, and an MCP client's approval prompt is a property of that client — several harnesses auto-approve. So a `confirm` tool is dropped, with that reason, rather than mounted in the hope that the far side asks.
3098
+
3099
+ Writes confirm by default. An app that wants one callable unattended says so per descriptor (`exposeAsTool: { confirm: false }`) or app-wide (`agents.tools.requireConfirmForWrites: false`) — both are edits a reviewer sees in the diff.
3100
+
3101
+ ### Naming, and what an agent sees
3102
+
3103
+ `todos.create` mounts as `app_todos_create` (MCP tool names are `[A-Za-z0-9_-]`). The tag is resolved back through the listing the server rendered, never by un-mangling the name the model produced, so no amount of argument shaping selects a different procedure. Writes are marked `[WRITE]` in the description — the model has no other signal that one of two tools destroys data.
3104
+
3105
+ Everything that did NOT mount is reported with a reason (`GET /_voltro/inspect/agent/tools` returns `dropped[]`), because a tool silently missing from an agent's set is a support ticket that opens with "the agent says it can't do that".
3106
+
3107
+ ### In the audit trail
3108
+
3109
+ An agent call runs the same plugin interceptor chain as a socket call, so `plugin-audit` records it as usual. It additionally stamps `via: 'agent'` on the write attribution, with the subject id of the **person** the agent acted as — an agent never escalates identity, which is precisely why an unmarked agent write would be indistinguishable from a human one. A change-event tap reads it as `event.via`.
3110
+
3111
+ ### What this does NOT defend against
3112
+
3113
+ Stated rather than implied, because a bound you assume is worse than one you do not have:
3114
+
3115
+ - **Prompt injection that steers the model into misusing a tool it IS permitted to run.** The allowlist bounds WHICH tools exist; it cannot bound intent. Tool results are your app's data, and app data can contain instructions.
3116
+ - **A client holding all three credentials.** It can call any admitted tool with any arguments. The bound is the subject's permissions — which is the design, and the reason to scope `VOLTRO_AGENT_TOKEN` narrowly.
3117
+ - **Call rate.** `maxPerRun` is reported for a client to honour; honouring it is the client's. Use [`plugin-ratelimit`](/docs/plugins/ratelimit) on the procedure for a bound that holds regardless of who is calling.
3118
+
3119
+ ## Verifying your own work
3120
+
3121
+ `voltro_check_invariants` runs the framework's own invariant checks against the **running** app and returns a machine-readable verdict — the loop that turns "I generated some code" into "I checked it". `GET /_voltro/inspect/checks` is the same thing over HTTP.
3122
+
3123
+ | Check | Answers |
3124
+ |---|---|
3125
+ | `browser-safety` | Does the generated rpcGroup transitively value-import a server-only module? The finding carries the full **import chain** — a bare specifier says a rule broke, the chain says which shared `lib/` file broke it. |
3126
+ | `procedure-access` | Does every wire-exposed procedure declare `guards:` or `openAccess:`? Runs the same verdict the boot gate runs, including `security.defaultDeny`. |
3127
+ | `schema-convergence` | Has the live schema drifted, and are operations pending? Read from the same snapshot `voltro db plan --against` reads. |
3128
+ | `server-only-exposure` | Does a wire-reachable query declare a `.serverOnly()` column in its output? |
3129
+
3130
+ Each answers `pass`, `fail`, or **`unavailable`** — and `unavailable` is never a pass. Two of these read the source tree, and a deployed `voltro serve` has no generated rpcGroup to walk (frequently no `src/` at all after a `pnpm deploy`), so it reports them `unavailable` **with the reason** rather than omitting them. Three green checks that you cannot distinguish from "nobody looked" would be worse than no answer.
3131
+
3132
+ ```json
3133
+ {
3134
+ "checks": [
3135
+ { "id": "browser-safety", "status": "unavailable",
3136
+ "reason": "this process has no generated rpcGroup to walk — the check reads the SOURCE import graph…" },
3137
+ { "id": "procedure-access", "status": "fail",
3138
+ "summary": "1 wire-exposed procedure(s) declare no access decision",
3139
+ "findings": [{ "tag": "todos.secret", "kind": "query", "file": "queries/todos.secret.query.ts" }],
3140
+ "fix": "give each one either `guards: [{ scope: '…' }]` or `openAccess: '<why it is public>'`…" }
3141
+ ],
3142
+ "summary": { "pass": 2, "fail": 1, "unavailable": 1 },
3143
+ "mode": "serve"
3144
+ }
3145
+ ```
3146
+
3147
+ `voltro doctor`'s rule set is deliberately NOT here: it is a source-tree scan with its own allowlist file and exit-code contract, it would answer `unavailable` on the one deployment shape this surface exists to reach, and re-hosting it behind HTTP would be a second implementation of a large thing. Run the command, on the machine that has the source.
3148
+
2678
3149
  ## Streamable HTTP transport
2679
3150
 
2680
3151
  For clients that speak MCP over HTTP, **`voltro-mcp-http`** serves the same surface over the current **Streamable HTTP** transport (the single-endpoint POST/GET model that replaced the old HTTP+SSE dual-endpoint). One endpoint handles:
@@ -2698,7 +3169,7 @@ The manifest is read through a TTL-cached source (~10 seconds): a procedure you
2698
3169
 
2699
3170
  ## Protocol scope
2700
3171
 
2701
- MCP over JSON-RPC 2.0. `initialize` negotiates the protocol revision (`2025-06-18`, `2025-03-26`, `2024-11-05`) and advertises the `tools`, `resources`, and `prompts` capabilities; methods are `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`. The stdio bin frames this as newline-delimited JSON-RPC; the HTTP bin serves it over Streamable HTTP. Both transports route to the same pure protocol core (`handleMcpRequest`, `callTool`, `listResources`/`readResource`, `listPrompts`/`getPrompt`, `routeHttp`), all exported from `@voltro/mcp`.
3172
+ MCP over JSON-RPC 2.0. `initialize` negotiates the protocol revision (`2025-06-18`, `2025-03-26`, `2024-11-05`) and advertises the `tools`, `resources`, and `prompts` capabilities; methods are `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`. The stdio bin frames this as newline-delimited JSON-RPC; the HTTP bin serves it over Streamable HTTP. Both transports route to the same protocol core, all exported from `@voltro/mcp`: the pure `handleMcpRequest` (`callTool`, `listResources`/`readResource`, `listPrompts`/`getPrompt`) plus `handleMcpRequestAsync`, which handles the two methods that need a round trip to the app — `tools/list` folds in the admitted agent tools, and `tools/call` executes one or runs the invariant checks. With no live connection configured, `handleMcpRequestAsync` behaves exactly like the pure one.
2702
3173
 
2703
3174
 
2704
3175
 
@@ -2707,13 +3178,24 @@ MCP over JSON-RPC 2.0. `initialize` negotiates the protocol revision (`2025-06-1
2707
3178
  <!-- source: en/cli/update.md -->
2708
3179
  ## Update
2709
3180
 
2710
- _voltro update — bump the framework to the latest version and run the codemods that adapt your source to any changed APIs._
3181
+ _voltro update — what the command does and does not do. It bumps and installs; it does not make your app boot. The boot refusals this release ships, and the order you meet them._
2711
3182
 
2712
3183
  `voltro update` upgrades an app to the latest framework release. It does three things in order:
2713
3184
 
2714
3185
  1. **Bump** every `@voltro/*` dependency in `package.json` to the target version.
2715
3186
  2. **Install** with your project's package manager — see [Which package manager](#which-package-manager) below.
2716
- 3. **Run the codemods** shipped with the target version automatic source rewrites for any breaking API change, plus printed manual steps for anything that can't be automated.
3187
+ 3. **Run the codemods** shipped with the target version. A codemod is either a **transform** (rewrites your source) or **`manual`** (prints written steps, only when your app is affected). **Every one of 0.34.0's 21 codemods is `manual`** nothing is rewritten for you, and there is no diff to review afterwards.
3188
+
3189
+ **`voltro update` does not make your app boot.** It moves versions and prints
3190
+ instructions; deciding what those instructions mean for your code is yours.
3191
+ 0.34.0 ships **six boot refusals** — three of them fire on `voltro dev`, before
3192
+ you deploy anything — and `update`, `db apply` and `typecheck` all pass while an
3193
+ app is dead in every one of them. Start with [`voltro doctor`](#start-here-voltro-doctor),
3194
+ then read [the boot refusals](#the-boot-refusals-and-where-you-meet-them).
3195
+
3196
+ For the per-change narrative — what each of the 21 notes is about and why —
3197
+ read [Upgrading to 0.34.0](/docs/releases/upgrading-to-0-34). This page is the
3198
+ command's own contract.
2717
3199
 
2718
3200
  ```bash
2719
3201
  voltro update # bump to the latest published version, install, run codemods
@@ -2729,7 +3211,219 @@ voltro update --codemods-only --from 0.3.0 # re-run codemods 0.3.0
2729
3211
  voltro update --codemods-only --from 0.3.0 --to 0.4.0 # explicit delta
2730
3212
  ```
2731
3213
 
2732
- ### Taking only part of the jump — `--only`
3214
+ ## Start here: `voltro doctor`
3215
+
3216
+ `voltro doctor` is the command that lists what will refuse to boot, and it
3217
+ reports the **exact set the boot refuses on, from the same function** — not a
3218
+ second implementation that can disagree with it.
3219
+
3220
+ ```bash
3221
+ voltro update
3222
+ voltro doctor # every undecided procedure + every unverified webhook, by tag and file
3223
+ voltro doctor --json # accessDecisions.undecided / webhookVerification.unverified — for CI
3224
+ ```
3225
+
3226
+ Run it before you try to start anything. Its two most important sections are the
3227
+ two source-shaped refusals below:
3228
+
3229
+ ```text
3230
+ access decisions · security.defaultDeny ON
3231
+ ✗ no access decision 18
3232
+ ✓ openAccess, declared on purpose 0
3233
+
3234
+ ✗ notes.list (query)
3235
+ api/notes/list.query.ts
3236
+
3237
+
3238
+ `voltro dev` and `voltro serve` REFUSE to boot on these. Give each a decision:
3239
+ `guards: [{ scope: '…' }]`, or `openAccess: '<why anyone may call it>'`.
3240
+ ```
3241
+
3242
+ **What doctor cannot see.** It reads your source tree, so it covers the access
3243
+ decisions and the webhook declarations. The other four refusals are properties
3244
+ of your *environment* — a connection URL, `NODE_ENV`, a migration that has not
3245
+ run against a particular database — and no source scan can predict them. Read
3246
+ the list below for those.
3247
+
3248
+ ## The boot refusals, and where you meet them
3249
+
3250
+ Six of them ship in 0.34.0. `voltro update` succeeds, `voltro db apply`
3251
+ succeeds and `voltro typecheck` succeeds in **all six** — the access decision is
3252
+ a runtime boot gate, not a type error, and the other five are environment facts
3253
+ no compiler is looking at. They are listed here in the order you actually meet
3254
+ them: the first three on your own machine, the last three in a container.
3255
+
3256
+ ### On your laptop — `voltro dev`
3257
+
3258
+ **1. A wire-exposed procedure that decides nothing.** `guards:` used to default
3259
+ to *allowed*, so a `*.query.ts` with no guard was callable by any authenticated
3260
+ session. Every discovered procedure now declares `guards:` or `openAccess:`, or
3261
+ neither `voltro dev` nor `voltro serve` starts.
3262
+
3263
+ ```text
3264
+ [access] 18 wire-exposed procedures declare no access decision, and this app runs with `security.defaultDeny`:
3265
+
3266
+ notes.list (query)
3267
+ api/notes/list.query.ts
3268
+
3269
+
3270
+ Each of these is callable by ANY authenticated session. Give each one a
3271
+ decision — the two are equally acceptable and they are not the same claim:
3272
+
3273
+ guards: [{ scope: 'invoices:read' }] the caller must hold a scope
3274
+ openAccess: 'public pricing, no user data' anyone may call it, and why
3275
+ ```
3276
+
3277
+ Three things worth knowing before you start editing:
3278
+
3279
+ - **`openAccess` takes a reason, not a boolean.** It is what makes "we decided
3280
+ this is open" distinguishable from "nobody looked".
3281
+ - **Do not rubber-stamp with a scope every caller already holds.** That
3282
+ satisfies the gate, reads as protection, and enforces nothing.
3283
+ - **A procedure only other server code calls wants neither.** Mark it
3284
+ `internal: true` and it leaves the wire entirely (and then it must not carry
3285
+ `openAccess` — the definers refuse that combination).
3286
+ - **Your plugins' procedures are not your problem.** The gate reads your app's
3287
+ own discovered files only.
3288
+
3289
+ **The one-field escape hatch**, if you need to ship before you have decided
3290
+ everything:
3291
+
3292
+ ```ts
3293
+ // app.config.ts
3294
+ export default { security: { defaultDeny: false } }
3295
+ ```
3296
+
3297
+ That restores the old default-allow for the WHOLE app, in one place a reviewer
3298
+ can see. There is deliberately no env var for it — an env var is how a security
3299
+ default gets turned off in one CI job and stays off. `voltro doctor` keeps
3300
+ listing the undecided procedures while it is off, marked advisory.
3301
+
3302
+ **2. An incoming webhook that does not say how it authenticates its caller.**
3303
+ An incoming webhook is a public, unauthenticated POST that runs your
3304
+ application code. The transport refuses to mount one that declared nothing:
3305
+
3306
+ ```text
3307
+ incoming webhook '/webhooks/stripe' is mounted without declaring how it authenticates its caller.
3308
+ An incoming webhook is a public POST that runs your application code, so the framework refuses
3309
+ to mount one that nothing verifies. Declare it on the descriptor:
3310
+ · provider: stripeWebhookProvider() — or any provider preset (HMAC + replay window)
3311
+ · signature: { _tag: 'hmac', algorithm: 'hmacSha256', header: 'X-Signature', ... }
3312
+ · verification: 'provider' — the handler verifies with the provider's own SDK
3313
+ · verification: 'none' — deliberately public (gateway / IP allow-list owns it)
3314
+ A signature-verified webhook also needs its shared secret in VOLTRO_WEBHOOK_SECRET_<ID>;
3315
+ the framework mints no secret for you.
3316
+ ```
3317
+
3318
+ `verification: 'none'` is a legitimate answer when a gateway or IP allow-list
3319
+ owns the trust boundary. It has to be *said*, which is the whole change.
3320
+
3321
+ **3. A mysql / mariadb / mssql URL asking for TLS the dialect cannot honour.**
3322
+ Both dialects used to DROP a TLS request rather than reject it, so
3323
+ `DB_URL=mysql://…?ssl=true` connected in plaintext with no warning. Only the two
3324
+ modes the cross-dialect `ssl` boolean can express are accepted; everything else
3325
+ throws where the connection is built — which is `voltro dev`, `voltro serve`,
3326
+ `voltro db apply` and `voltro migrate` alike.
3327
+
3328
+ ```text
3329
+ DB_URL '?sslmode=verify-full' is not supported by the mysql/mariadb dialect —
3330
+ use 'require' (TLS without certificate verification) or 'disable' (plaintext).
3331
+ ```
3332
+
3333
+ | URL says | Result |
3334
+ |---|---|
3335
+ | `?sslmode=require` / `?ssl=true` / `?ssl=1` | TLS, certificate NOT verified |
3336
+ | `?encrypt=1` | same (mssql only — tedious' spelling) |
3337
+ | `?sslmode=disable` / `?ssl=false` / `?ssl=0` | plaintext, explicitly |
3338
+ | `prefer`, `allow`, `verify-ca`, `verify-full`, `?ssl=yes`, a CA-profile name | **throws at boot** |
3339
+
3340
+ If your URL said `?ssl=true` you were being lied to — that connection has been
3341
+ plaintext, and it is real now. **Confirm your server accepts TLS before rolling
3342
+ out.** Check it from the database rather than from the config:
3343
+
3344
+ ```sql
3345
+ -- mysql / mariadb: empty = plaintext, a cipher name = encrypted
3346
+ SHOW STATUS LIKE 'Ssl_cipher';
3347
+ -- mssql: FALSE / TRUE
3348
+ SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID;
3349
+ ```
3350
+
3351
+ postgres, sqlite and turso are unaffected.
3352
+
3353
+ ### In the container — `voltro serve` / `voltro start`
3354
+
3355
+ These three fire only on a deploy environment (`NODE_ENV=production` or
3356
+ `staging`), which is exactly why they are the expensive ones: nothing on your
3357
+ machine reproduces them.
3358
+
3359
+ **4. Pending `migrations/*.migration.ts` that have never run against this
3360
+ database.** It always had to run before serve; what changed is that skipping it
3361
+ is loud. Serve's other schema guard is a declarative fingerprint diff, and a
3362
+ file migration exists for the changes a state diff cannot infer — a data move, a
3363
+ backfill, a cross-table rewrite. Those move no fingerprint, so the guard passed
3364
+ and production ran un-migrated.
3365
+
3366
+ ```text
3367
+ serve: refusing to boot — 3 pending file-based migration(s) have never run against this
3368
+ database. They perform the changes a schema diff cannot infer (data moves, backfills, table
3369
+ splits), so the declarative fingerprint check below cannot see them and would have let this
3370
+ process serve un-migrated data.
3371
+
3372
+ Run them from your pre-deploy job — `voltro db migrate .` (schema + files) or `voltro db files .`
3373
+ (files alone) — or set VOLTRO_AUTO_MIGRATE=0 to bypass every boot schema check. `voltro serve`
3374
+ never applies them itself: a rolling deploy would start N replicas and each would try.
3375
+ ```
3376
+
3377
+ Serve will not apply them for you, deliberately: a rolling deploy starts N
3378
+ replicas, each would try, and the migration lock turns that into N-1 processes
3379
+ blocked on boot. A refusal is recoverable in one command; a fleet wedged behind
3380
+ a lock is not.
3381
+
3382
+ **5. `plugin-search` on the in-memory backend.** The heap-resident index is
3383
+ per-process AND non-durable — it starts empty after every deploy and nothing
3384
+ re-seeds it — so a single replica does not make it correct.
3385
+
3386
+ ```text
3387
+ plugin-search refuses to boot in production on the in-memory backend.
3388
+
3389
+ The memory index lives in THIS process's heap. Two consequences, both silent:
3390
+ • every replica holds a different index, so a result depends on which replica served you;
3391
+ • the index starts EMPTY after every restart/deploy, and nothing re-seeds it automatically.
3392
+
3393
+ Configure a durable engine in app.config.ts:
3394
+ searchPlugin({ backend: { engine: 'typesense', url: …, apiKey: … }, indexes })
3395
+
3396
+ ```
3397
+
3398
+ If your deployment genuinely is one process that calls `backfillIndex` at
3399
+ startup, declare it: `searchPlugin({ singleProcessMemoryIndex: true, indexes })`
3400
+ — a claim the plugin holds you to, not a mute switch. Full reasoning:
3401
+ [the memory backend refuses to boot in production](/docs/plugins/search#the-memory-backend-refuses-to-boot-in-production).
3402
+
3403
+ **6. `SSR_CACHE=postgres` with no database in the web process's environment.**
3404
+ `voltro start` used to select the postgres ISR cache only when `PG_HOST` was
3405
+ set, while every template and every deployment doc configures `DB_URL` — so an
3406
+ app that asked for the shared cache the documented way silently got the
3407
+ per-process memory one, reported at `info` as if it were the default. Both sides
3408
+ go through the connection resolver now, and the mismatch is fatal on a deploy
3409
+ environment:
3410
+
3411
+ ```text
3412
+ SSR_CACHE=postgres, but nothing in the environment names a database (looked for DB_URL,
3413
+ DB_PRIMARY_URL, DB_DIRECT_URL, DB_MIGRATE_URL, DB_HOST, PG_HOST). Refusing to fall back to
3414
+ the per-process memory cache: it is not shared between instances and does not survive a
3415
+ restart, so the pages this process serves would differ from its replicas' with nothing to
3416
+ indicate it.
3417
+ ```
3418
+
3419
+ Either give the web process a `DB_URL`, or drop `SSR_CACHE=postgres` and take
3420
+ the memory cache deliberately. Off a deploy environment it warns and falls back
3421
+ instead. Two knock-on effects with nothing to edit: pages declaring
3422
+ `cacheInvalidatesOn` that had *no* live invalidation now have it (a real change
3423
+ in origin load), and a web process with no database that declares
3424
+ `cacheInvalidatesOn` gets a boot warning naming those routes.
3425
+
3426
+ ## Taking only part of the jump — `--only`
2733
3427
 
2734
3428
  Ids are what `--dry-run` prints:
2735
3429
 
@@ -2803,7 +3497,16 @@ source version.
2803
3497
 
2804
3498
  ## The clean-tree guard
2805
3499
 
2806
- Codemods **rewrite your source**, so you need a clean diff to review afterwards. `voltro update` refuses to run on a dirty git working tree — commit or stash first. Use `--dry-run` to preview without touching anything, or `--force` to override the guard (you accept a mixed diff).
3500
+ `voltro update` refuses to run on a dirty git working tree — commit or stash
3501
+ first. Use `--dry-run` to preview without touching anything, or `--force` to
3502
+ override the guard (you accept a mixed diff).
3503
+
3504
+ The guard is about the writes `update` makes on your behalf: the version bump
3505
+ across every workspace `package.json`, the lockfile the install rewrites, and —
3506
+ in a release that ships one — a **transform** codemod rewriting your source.
3507
+ When the jump's codemods are all `manual`, as 0.34.0's 21 are, `update` writes
3508
+ nothing under `src/` at all, and the work the printed notes describe is a
3509
+ separate commit you author yourself.
2807
3510
 
2808
3511
  `--help` / `-h` is answered *before* the guard, so `voltro update --help` prints the flag list even on a dirty tree. The same holds for `voltro doctor --help`.
2809
3512
 
@@ -2882,27 +3585,68 @@ The same resolved manager is used for the **registry lookup** of the latest vers
2882
3585
 
2883
3586
  ## Codemods
2884
3587
 
2885
- Each breaking public-API change in a release ships a **codemod**. When you update across that release, `voltro update` applies it:
3588
+ Each breaking public-API change in a release ships a **codemod**, and there are exactly two kinds:
3589
+
3590
+ - A **transform codemod** rewrites your source automatically — renamed imports, moved modules, changed component props, restructured call signatures. The rewrite is scoped to files that actually import the affected symbol. Where the affected sites can be found but the fix needs your judgment, it inserts `// TODO(voltro-migration): …` markers so you can locate every spot.
3591
+ - A **manual codemod** prints written steps during the update, `appliesTo`-gated so you see it only when your app is actually affected. It writes nothing.
3592
+
3593
+ **0.34.0's are all manual — 21 of them, zero transforms.** That is not an
3594
+ omission. The largest change in the release asks a question only you can answer
3595
+ ("who may call this procedure?"), and a transform could have answered it
3596
+ mechanically for every procedure in your app — declaring your entire surface
3597
+ open on purpose, in one commit nobody reads, with a reason the tool invented.
3598
+ The framework does not sign that.
2886
3599
 
2887
- - A **transform codemod** rewrites your source automatically — renamed imports, moved modules, changed component props, restructured call signatures. The rewrite is scoped to files that actually import the affected symbol.
2888
- - A **manual codemod** prints written steps during the update, only when your app is affected — for changes that can't be mechanically transformed (a behavior change, a descriptor/executor restructure). Where the affected sites can be found but the fix needs your judgment, a codemod inserts `// TODO(voltro-migration): …` markers so you can locate every spot.
3600
+ So on this jump the output is a wall of text and no diff:
3601
+
3602
+ ```text
3603
+ Manual steps required (could not be automated):
3604
+
3605
+ ▸ 0.34.0/03_procedure-access-decision — Every wire-exposed procedure declares an access decision (`guards:` or `openAccess:`)
3606
+ YOUR APP WILL NOT BOOT UNTIL EVERY WIRE-EXPOSED PROCEDURE DECIDES WHO MAY
3607
+ CALL IT. …
3608
+ ```
2889
3609
 
2890
- Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 0.4.0` runs the `0.3.0` and `0.4.0` codemods in sequence). Review the resulting diff before committing.
3610
+ Read the notes. They are the only artefact the upgrade produces, and each one
3611
+ prints only because your tree matched it.
3612
+
3613
+ Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 → 0.4.0` runs the `0.3.0` and `0.4.0` codemods in sequence).
2891
3614
 
2892
3615
  ## The database is separate
2893
3616
 
2894
- `voltro update` does **not** touch your database. Framework-owned `_voltro_*` tables (workflow runs, schedules, …) are reconciled by the declarative differ, not by codemods: when a release changes one of those tables, your next `voltro db apply` (or `voltro dev` boot, which auto-applies) picks up the change. After an update:
3617
+ `voltro update` does **not** touch your database. Framework-owned `_voltro_*` tables (workflow runs, schedules, …) are reconciled by the declarative differ, not by codemods: when a release changes one of those tables, your next `voltro db apply` (or `voltro dev` boot, which auto-applies) picks up the change.
3618
+
3619
+ Use `voltro db apply` (the declarative diff), not `voltro db migrate` (the imperative file-runner) — only the former reconciles framework tables. If your app also ships `migrations/*.migration.ts`, `voltro db migrate .` runs both halves and is what refusal 4 above asks your pre-deploy job for.
3620
+
3621
+ ## After the update — the checklist
2895
3622
 
2896
3623
  ```bash
2897
3624
  voltro update
2898
- voltro db apply # reconcile any changed framework tables NOT voltro db migrate
2899
- # then run your typecheck to confirm your code compiles against the new API
3625
+ voltro doctor # the one that can fail. Every undecided procedure + unverified webhook.
3626
+ voltro db apply # reconcile any changed framework tables
3627
+ voltro typecheck # your code against the new API surface
3628
+ voltro dev # the first boot that actually exercises the gates
2900
3629
  ```
2901
3630
 
2902
- Use `voltro db apply` (the declarative diff), not `voltro db migrate` (the imperative file-runner) only the former reconciles framework tables.
3631
+ **`update`, `db apply` and `typecheck` all pass on an app that will not start.**
3632
+ That is the shape to internalise: the access decision is a runtime boot gate, not
3633
+ a type error; the webhook declaration is a descriptor property, not a signature;
3634
+ and the environment-shaped refusals are facts about a container you have not
3635
+ started yet. The only two steps in that list that can tell you the truth are
3636
+ `voltro doctor` and an actual boot.
3637
+
3638
+ For a deploy, add the container-side ones to your pre-deploy job before the
3639
+ image rolls:
3640
+
3641
+ ```bash
3642
+ voltro db migrate . # schema diff AND file migrations — refusal 4
3643
+ # and check by hand: the mysql/mssql DB_URL's ?sslmode (3), a durable search
3644
+ # backend (5), and DB_URL on the WEB process if it sets SSR_CACHE=postgres (6)
3645
+ ```
2903
3646
 
2904
3647
  ## Where to read next
2905
3648
 
3649
+ - [Upgrading to 0.34.0](/docs/releases/upgrading-to-0-34) — the per-change narrative behind the 21 notes
2906
3650
  - [Migrate](/docs/cli/migrate) — schema changes end-to-end
2907
3651
  - [Build & start](/docs/cli/build-and-start) — production paths
2908
3652