@voltro/cli 0.32.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 (339) hide show
  1. package/CHANGELOG.md +2006 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -29
  3. package/bin/nodeEnvironment.d.mts +30 -0
  4. package/bin/nodeEnvironment.mjs +158 -0
  5. package/bin/voltro.mjs +69 -5
  6. package/dist/addCommand-BNeoeSxe.js +124 -0
  7. package/dist/addCommand-aXSQveak.js +2 -0
  8. package/dist/agentsMd-BTchIZku.js +2 -0
  9. package/dist/agentsMd-mhQMF1bx.js +254 -0
  10. package/dist/apiBuild-BICVGnEC.js +2 -0
  11. package/dist/{apiBuild-C4uEbs3s.js → apiBuild-DZF_f0_I.js} +46 -46
  12. package/dist/appPort-B_HpJ_ck.js +48 -0
  13. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  14. package/dist/baselineCommand-DIttzO8A.js +227 -0
  15. package/dist/bin.js +71 -28
  16. package/dist/build-CE7Ca9gm.js +711 -0
  17. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  18. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  19. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  20. package/dist/checkCommand-YZDmkAGd.js +2 -0
  21. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  22. package/dist/cliError-BmdYnghb.js +10 -0
  23. package/dist/cliOutput-D1tSBoRM.js +15 -0
  24. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  25. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  26. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  27. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  28. package/dist/codegen-CYM3Zqrf.js +605 -0
  29. package/dist/codegen-ChBi_hVa.js +2 -0
  30. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  31. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  32. package/dist/commandRunner-BLAEFLjp.js +47 -0
  33. package/dist/commands-gutsz-Ac.js +808 -0
  34. package/dist/connectionConfig-UFlIEiys.js +66 -0
  35. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  36. package/dist/dataCommand-qL0r7fPO.js +535 -0
  37. package/dist/dataProfile-dW-PsfLB.js +15 -0
  38. package/dist/dbCommand-B6X0FZbq.js +1621 -0
  39. package/dist/dbCommand-CpdKLeQq.js +2 -0
  40. package/dist/{dev-5ficNnvF.js → dev-CerMd0mW.js} +3155 -3110
  41. package/dist/dev-CoG-ZPx8.js +3 -0
  42. package/dist/devActivity-Dx_3nnGv.js +100 -0
  43. package/dist/devActivity.js +1 -1
  44. package/dist/dialectDriver-CgXnDfec.js +39 -0
  45. package/dist/discover-C9XKJDco.js +25 -0
  46. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  47. package/dist/{checkCommand-dm7OHtPt.js → doctorCommand-CnDXQxfa.js} +520 -1278
  48. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  49. package/dist/e2eCmd-BRabZww-.js +147 -0
  50. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  51. package/dist/envCommand-DPgdV1Bq.js +60 -0
  52. package/dist/evalCommand-6RUfPen4.js +118 -0
  53. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  54. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  55. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  56. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  57. package/dist/generateCommand-oibemh97.js +147 -0
  58. package/dist/index.d.ts +45 -0
  59. package/dist/index.js +4 -3
  60. package/dist/infoCommand-BJw9nLUR.js +60 -0
  61. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  62. package/dist/inspect-C_T_WGvl.js +2 -0
  63. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  64. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  65. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  66. package/dist/loadEnv-D9nEOClM.js +44 -0
  67. package/dist/logFileSink-C_D2wRN1.js +105 -0
  68. package/dist/logsCmd-CCca3KRZ.js +260 -0
  69. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  70. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  71. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  72. package/dist/migrate-CBwOt_iV.js +83 -0
  73. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  74. package/dist/newCommand-COWOJ1_E.js +156 -0
  75. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  76. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  77. package/dist/pageConvention-cEiRxdab.js +5 -0
  78. package/dist/privacyCommand-C-Df56U_.js +146 -0
  79. package/dist/projectScaffold-DmzEKHib.js +2 -0
  80. package/dist/projectScaffold-LMMtaavR.js +814 -0
  81. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  82. package/dist/renderProfile-1OWWAAtx.js +81 -0
  83. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  84. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  85. package/dist/scheduleCmd--jksTrf6.js +69 -0
  86. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  87. package/dist/schemaIr-UJybUUZW.js +103 -0
  88. package/dist/{sdkgen-B_5mHQS2.js → sdkgen-CYJscZC7.js} +111 -209
  89. package/dist/seedRunner-TFHHiToI.js +329 -0
  90. package/dist/serveCommand-B_isw7q4.js +1647 -0
  91. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  92. package/dist/serveEntry.js +5 -5
  93. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  94. package/dist/start-9LiUOfES.js +1087 -0
  95. package/dist/start-B-9Nsp-S.js +3 -0
  96. package/dist/startEntry.js +2 -2
  97. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  98. package/dist/storageCommand-Co6NfLqN.js +42 -0
  99. package/dist/templates-De8IR5-c.js +102 -0
  100. package/dist/test-CI6iDsYc.js +115 -0
  101. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  102. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  103. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  104. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  105. package/dist/updateCommand-Bkptutss.js +585 -0
  106. package/dist/updateCommand-us1_hdIC.js +2 -0
  107. package/dist/{inspectMetrics-BqO4E9G0.js → webDev-CBYvPqQr.js} +1006 -1567
  108. package/dist/webDev-Cg-fFiyd2.js +2 -0
  109. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  110. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  111. package/dist/webhooksCommand-CID96Rga.js +267 -0
  112. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  113. package/package.json +193 -18
  114. package/templates/AGENTS.core.md +58 -3
  115. package/templates/AGENTS.md +64 -7
  116. package/templates/agent-docs/_index.md +6 -4
  117. package/templates/agent-docs/_manifest.json +22 -5
  118. package/templates/agent-docs/ai.md +370 -0
  119. package/templates/agent-docs/authentication.md +265 -31
  120. package/templates/agent-docs/caching.md +6 -0
  121. package/templates/agent-docs/cli.md +794 -50
  122. package/templates/agent-docs/data.md +550 -11
  123. package/templates/agent-docs/database/migrations.md +174 -25
  124. package/templates/agent-docs/database/misc.md +193 -40
  125. package/templates/agent-docs/database/querying.md +19 -1
  126. package/templates/agent-docs/database/scaling.md +60 -0
  127. package/templates/agent-docs/database/schema.md +5 -2
  128. package/templates/agent-docs/database/seedsdialects.md +208 -19
  129. package/templates/agent-docs/database/transactions.md +68 -0
  130. package/templates/agent-docs/deployment.md +156 -4
  131. package/templates/agent-docs/introduction.md +87 -16
  132. package/templates/agent-docs/local-first-mobile.md +79 -4
  133. package/templates/agent-docs/multi-tenancy.md +95 -20
  134. package/templates/agent-docs/observability.md +58 -3
  135. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  136. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  137. package/templates/agent-docs/plugins/audit.md +37 -1
  138. package/templates/agent-docs/plugins/auth-social.md +143 -0
  139. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  140. package/templates/agent-docs/plugins/auth.md +131 -6
  141. package/templates/agent-docs/plugins/billing.md +132 -15
  142. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  143. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  144. package/templates/agent-docs/plugins/duckdb.md +1 -1
  145. package/templates/agent-docs/plugins/flags.md +132 -0
  146. package/templates/agent-docs/plugins/governance.md +105 -7
  147. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  148. package/templates/agent-docs/plugins/presence.md +13 -2
  149. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  150. package/templates/agent-docs/plugins/search.md +157 -6
  151. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  152. package/templates/agent-docs/plugins/webhooks.md +105 -0
  153. package/templates/agent-docs/plugins.md +200 -18
  154. package/templates/agent-docs/reference.md +60 -3
  155. package/templates/agent-docs/releases.md +1117 -0
  156. package/templates/agent-docs/routing.md +43 -25
  157. package/templates/agent-docs/scheduling.md +14 -1
  158. package/templates/agent-docs/schema-driven-ui.md +92 -12
  159. package/templates/agent-docs/security.md +449 -2
  160. package/templates/agent-docs/templates/apibackends.md +87 -18
  161. package/templates/agent-docs/templates/appshells.md +32 -14
  162. package/templates/agent-docs/templates/overview.md +13 -8
  163. package/templates/agent-docs/testing.md +211 -14
  164. package/templates/agent-docs/whats-new.md +1722 -84
  165. package/templates/agent-docs/workflows.md +130 -14
  166. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  167. package/templates/apps/api-ai/package.json +8 -7
  168. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  169. package/templates/apps/api-auth/package.json +9 -8
  170. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  171. package/templates/apps/api-backend/package.json +12 -8
  172. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  173. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  175. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  176. package/templates/apps/api-backend-deactivation/package.json +8 -7
  177. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  178. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  179. package/templates/apps/api-backend-mail/package.json +9 -8
  180. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  181. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  182. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  183. package/templates/apps/api-backend-mariadb/package.json +10 -9
  184. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  185. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  186. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  187. package/templates/apps/api-backend-sqlite/package.json +9 -8
  188. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  189. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  190. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  191. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  192. package/templates/apps/api-backend-storage/package.json +9 -8
  193. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  194. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  195. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  196. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  197. package/templates/apps/api-cms/app.config.ts +19 -0
  198. package/templates/apps/api-cms/authz.ts +63 -0
  199. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  201. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  202. package/templates/apps/api-cms/package.json +11 -10
  203. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  204. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  205. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  206. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  207. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  208. package/templates/apps/api-collab/package.json +9 -8
  209. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  210. package/templates/apps/api-data-advanced/package.json +9 -8
  211. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  212. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  213. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  214. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  215. package/templates/apps/api-durable/package.json +9 -8
  216. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  217. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  218. package/templates/apps/api-feature-flags/package.json +10 -9
  219. package/templates/apps/api-governance/README.md +30 -8
  220. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  221. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  222. package/templates/apps/api-governance/database/schema.ts +16 -4
  223. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  224. package/templates/apps/api-governance/package.json +9 -8
  225. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  226. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  227. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  228. package/templates/apps/api-kv/package.json +9 -8
  229. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  230. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  231. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  232. package/templates/apps/api-moderation/package.json +9 -8
  233. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  234. package/templates/apps/api-observability/package.json +9 -8
  235. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  236. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  237. package/templates/apps/api-ratelimit/package.json +9 -8
  238. package/templates/apps/api-rbac/package.json +9 -8
  239. package/templates/apps/api-rest/package.json +8 -7
  240. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  241. package/templates/apps/api-saas/package.json +12 -11
  242. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  243. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  244. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  245. package/templates/apps/api-saas-starter/authz.ts +75 -0
  246. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  247. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  248. package/templates/apps/api-saas-starter/package.json +15 -11
  249. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  250. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  251. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  252. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  253. package/templates/apps/api-search/package.json +9 -8
  254. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  255. package/templates/apps/api-status/README.md +10 -3
  256. package/templates/apps/api-status/app.config.ts +8 -3
  257. package/templates/apps/api-status/authz.ts +5 -3
  258. package/templates/apps/api-status/package.json +9 -8
  259. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  260. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  261. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  262. package/templates/apps/api-status/tests/status.test.ts +9 -1
  263. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  264. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  265. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  266. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  267. package/templates/apps/api-versioning/package.json +9 -8
  268. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  269. package/templates/apps/api-webhooks/package.json +10 -9
  270. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  271. package/templates/apps/changelog/package.json +8 -6
  272. package/templates/apps/edge-functions/package.json +2 -2
  273. package/templates/apps/frontend-admin/package.json +10 -8
  274. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  275. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  276. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  277. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  278. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  279. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  281. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  282. package/templates/apps/frontend-app/app.config.ts +4 -3
  283. package/templates/apps/frontend-app/package.json +11 -8
  284. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  285. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  286. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  287. package/templates/apps/frontend-auth/package.json +10 -8
  288. package/templates/apps/frontend-blank/package.json +9 -7
  289. package/templates/apps/frontend-cms/package.json +11 -9
  290. package/templates/apps/frontend-collab/package.json +12 -9
  291. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  292. package/templates/apps/frontend-contact/package.json +9 -7
  293. package/templates/apps/frontend-dashboard/package.json +9 -7
  294. package/templates/apps/frontend-docs/package.json +9 -7
  295. package/templates/apps/frontend-i18n/package.json +8 -6
  296. package/templates/apps/frontend-landing/package.json +9 -7
  297. package/templates/apps/frontend-portal/package.json +10 -8
  298. package/templates/apps/frontend-saas/app.config.ts +10 -6
  299. package/templates/apps/frontend-saas/package.json +10 -8
  300. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  301. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  303. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  304. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  305. package/templates/apps/frontend-spa/package.json +9 -7
  306. package/templates/apps/frontend-ssr/package.json +9 -7
  307. package/templates/apps/frontend-ssr-api/package.json +10 -8
  308. package/templates/apps/frontend-static-blog/package.json +8 -6
  309. package/templates/apps/frontend-status/package.json +10 -8
  310. package/templates/apps/mobile-app/README.md +1 -0
  311. package/templates/apps/mobile-app/package.json +4 -2
  312. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  313. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  314. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  315. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  316. package/templates/baselines/bare/.env.example +14 -0
  317. package/templates/baselines/bare/baseline.json +4 -4
  318. package/templates/baselines/compose/.env.example +14 -0
  319. package/templates/baselines/compose/README.md +1 -1
  320. package/templates/baselines/compose/baseline.json +5 -5
  321. package/templates/baselines/compose-mariadb/.env.example +14 -0
  322. package/templates/baselines/compose-mariadb/README.md +1 -1
  323. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  324. package/templates/baselines/helm/.env.example +14 -0
  325. package/templates/baselines/helm/baseline.json +4 -4
  326. package/dist/apiBuild-OJEjtwcn.js +0 -2
  327. package/dist/checkCommand-CwMrzAgV.js +0 -2
  328. package/dist/commands-C0nEePif.js +0 -11457
  329. package/dist/dbCommand-By__Ev0R.js +0 -2
  330. package/dist/dbCommand-ifOMafuG.js +0 -1311
  331. package/dist/dev-rc3fwPSZ.js +0 -3
  332. package/dist/devActivity-BhIu6ncs.js +0 -159
  333. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  334. package/dist/inspect-mmBuRXmy.js +0 -2
  335. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  336. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  337. package/dist/serveCommand-PxMmn96o.js +0 -1578
  338. package/dist/start-D1-8eKrO.js +0 -1084
  339. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -300,9 +300,9 @@ cause.
300
300
  | `VOLTRO_STORAGE_SECRET` | optional (`@voltro/plugin-storage`) | Signs private-file grant tokens; falls back to the session secret if unset |
301
301
  | `AI_PROVIDER` | when using `@voltro/ai` | `anthropic` / `openai` / `mock` |
302
302
  | `AI_API_KEY` | with `AI_PROVIDER` | Provider's API key |
303
- | `SSR_CACHE` | optional | `memory` (default) or `postgres` (needs `PG_*` set too) for the ISR cache |
303
+ | `SSR_CACHE` | optional | `memory` (default) or `postgres` for the ISR cache. `postgres` needs a database in the WEB process's env (`DB_URL` / `DB_HOST` / `PG_HOST`) without one it aborts the boot in production rather than falling back to memory |
304
304
  | `VOLTRO_API_ORIGIN` / `VOLTRO_API_ORIGIN_<NAME>` | split web/api deploy with SSR pages | The api's internal origin the WEB pod uses for SSR `ctx.query` (`http://api.<ns>.svc.cluster.local`). Per-api `_<NAME>` (name upper-cased) wins over the shared one and over `serverUrl`. Never sent to the browser |
305
- | `PORT` | optional | App's listen port (overrides `app.config.ts.port`) |
305
+ | `PORT` | optional | App's listen port. Outranks `--port` and `app.config.ts` `port:` — a platform that assigns a port sets this one, so it has to win. Unset, the app binds its declared `port:`, then 4000 (api) / 5173 (web). |
306
306
  | `VOLTRO_INSPECT` | optional | `off` to disable `_voltro/inspect/*` in prod |
307
307
  | `VOLTRO_INSPECT_TOKEN` | recommended | Bearer token guard on inspect endpoints |
308
308
  | `VOLTRO_DASHBOARD_APPS` | dashboard only | JSON `[{name?,url,token?}]` — target apps + inspect tokens the deployed DevTools dashboard shows (served at runtime from `/api/dashboard/config`) |
@@ -833,6 +833,8 @@ VOLTRO_SESSION_SECRET_PREVIOUS=<old> # still verifies live cookies
833
833
 
834
834
  Keep `_PREVIOUS` in place for one session-TTL window, then drop it. Existing cookies verify against `previous` until they naturally expire — no live session is invalidated.
835
835
 
836
+ > **If you embed the framework's middleware yourself** rather than booting through `voltro serve`, there is no boot gate to catch a missing secret. In that case a presented `voltro:session` cookie that cannot be verified now logs, once per process and at error level, that no credential-expiry bound is being imposed — so realtime subscriptions on that connection will not be cut off when the session expires. It is a diagnostic, not a refusal: a stale `voltro:session` cookie from another app on the same host is a normal thing for a browser to carry, and failing the request would turn that into a denial of service.
837
+
836
838
  ## 2. Kubernetes health probes
837
839
 
838
840
  `voltro serve` exposes two unauthenticated endpoints, both handled **before** any rate-limit interceptor:
@@ -1002,12 +1004,21 @@ indexed query instead of scanning every table). Force a full re-introspect with
1002
1004
 
1003
1005
  ## 4. Request limits & DoS
1004
1006
 
1005
- `POST /rpc` (the buffered JSON endpoint the SSR loaders use) rejects an over-`Content-Length` body with **413** *before* buffering it into memory:
1007
+ `POST /rpc` (the buffered JSON endpoint the SSR loaders use) is capped, and the cap is enforced **as bytes arrive**:
1006
1008
 
1007
1009
  ```sh
1008
1010
  VOLTRO_MAX_RPC_BODY_BYTES=8388608 # default 8 MiB
1009
1011
  ```
1010
1012
 
1013
+ A declared `Content-Length` over the cap is refused up front, so an honest client
1014
+ gets its **413** without uploading anything — a courtesy, not the enforcement,
1015
+ since a `Transfer-Encoding: chunked` body declares no length at all. The byte
1016
+ counter is the enforcement: it stops accumulating the moment the running total
1017
+ crosses the limit, drains the rest of the upload rather than dropping the
1018
+ connection, and answers **413**. Both shapes therefore end in the same status
1019
+ code, and the refusal is logged under the `voltro:security` scope with the cap
1020
+ and the byte count at which the server stopped reading.
1021
+
1011
1022
  Scope: this guards the `/rpc` JSON path only. File uploads ride separate storage routes with their own `limits.maxBytes`, and WebSocket frames are capped by the `ws` library default (100 MiB).
1012
1023
 
1013
1024
  **Put per-IP rate limiting and the primary body-size cap at the ingress** — that is the correct layer: it holds per-IP state and works across replicas, which an in-process limit can't.
@@ -1020,6 +1031,51 @@ nginx.ingress.kubernetes.io/limit-rps: "20"
1020
1031
 
1021
1032
  For **app-level** throttling (per-subject / per-tenant, e.g. an expensive action), use `@voltro/plugin-ratelimit` and the plugin `onHttpRequest` interceptor seam. It complements the ingress cap — it does not replace it.
1022
1033
 
1034
+ **There is no rate limit in the box.** The body cap above is the only request guard the runtime applies by default; `@voltro/plugin-ratelimit` is opt-in, so an app that has not installed and configured it has no per-IP, per-API-key or per-tenant cap on `/rpc` at all. The one on-by-default throttle anywhere in the framework is `plugin-auth`'s [brute-force lockout](/docs/authentication/passwords#brute-force-lockout), which covers sign-in credential attempts and nothing else. Treat the ingress limit as required, not as belt-and-braces.
1035
+
1036
+ ### Per-IP limits need a trusted proxy
1037
+
1038
+ The address the runtime rate-limits, geo-blocks and audits by is
1039
+ `socket.remoteAddress` — **not** `x-forwarded-for`, which any client can write.
1040
+ Behind an ingress that means every request counts against the proxy's address —
1041
+ and every `sessions.ipAddress` row records the proxy — until you declare the hop:
1042
+
1043
+ ```ts
1044
+ // app.config.ts
1045
+ export default {
1046
+ security: {
1047
+ trustedProxies: ['private'], // or: ['loopback'] · ['10.0.0.0/8'] · ['2'] · ['*']
1048
+ },
1049
+ }
1050
+ ```
1051
+
1052
+ The same setting decides whether `x-forwarded-proto` is believed, which is what
1053
+ lets the runtime emit HSTS behind a TLS-terminating load balancer. Override it
1054
+ on a running deployment with `VOLTRO_TRUSTED_PROXIES=private` (comma-separated).
1055
+
1056
+ ### Cross-site protection and its allowlist
1057
+
1058
+ Every state-changing request — `POST /rpc`, the `/ws` upgrade, every REST route
1059
+ projected from a `publicApi:` mutation, everything in `apiConfig.restRoutes`,
1060
+ and `POST /v1/api-keys` — refuses a browser request whose `Origin` is neither
1061
+ the `Host` it was sent to nor an allowlisted origin. **A split web/api
1062
+ deployment must declare its web origin** or the browser gets a 403 on every
1063
+ mutation, REST write and socket:
1064
+
1065
+ ```ts
1066
+ // app.config.ts
1067
+ export default {
1068
+ security: {
1069
+ allowedOrigins: ['https://app.example.com'],
1070
+ },
1071
+ }
1072
+ ```
1073
+
1074
+ The environment override is `VOLTRO_ALLOWED_ORIGINS` (comma-separated).
1075
+ Server-to-server callers (SSR loaders, mobile SDKs, other services) send no
1076
+ `Origin` and are unaffected. Full detail in
1077
+ [Security](/docs/security/overview#cross-site-requests-are-refused).
1078
+
1023
1079
  ## 5. Multi-tenant isolation
1024
1080
 
1025
1081
  Tables carrying the `tenant()` mixin are auto-scoped to the request's tenant — nothing more to do there. The gap is the **anonymous** request that matches no auth strategy: by default it resolves to a tenant-less anonymous Subject that can read any non-`tenant()` table across the DB.
@@ -1079,6 +1135,12 @@ sink, stops accepting new connections while **finishing already-accepted
1079
1135
  requests**, and closes the SQL connection pool **last** (waiting for in-flight
1080
1136
  transactions). Verified under concurrent load — accepted requests complete.
1081
1137
 
1138
+ The **transactional-outbox worker** is part of that sequence: its poll timer and
1139
+ its change subscription are released, and a delivery already in flight is
1140
+ awaited, before the pool closes. An outbox row that was still pending is not
1141
+ lost — it is durable, and the next process's first pass picks it up, which is
1142
+ one of the three reasons that poll exists.
1143
+
1082
1144
  **But the app cannot drain a rolling update by itself.** The runtime
1083
1145
  (`NodeRuntime`) owns the `SIGTERM` signal and closes the HTTP listener promptly —
1084
1146
  so a request that *arrives* during shutdown is refused. Failing readiness from
@@ -1168,13 +1230,38 @@ scaled. An operator's second pod died on `Connection timed out` for exactly
1168
1230
  this. `voltro serve` now prints the number and the arithmetic at boot:
1169
1231
 
1170
1232
  ```
1171
- db pool: max=10 per replica (DB_MAX_CONNECTIONS) × 4 replicas = up to 40 connections.
1233
+ db pool: max=10 per replica (DB_MAX_CONNECTIONS) + 1 = 11 × 4 replicas = up to 44 connections.
1234
+ Check that against your database's limit. PLUS 1 outside the pool (CDC LISTEN consumer) —
1235
+ those do not come out of the pool budget, they come out of the DATABASE's.
1172
1236
  ```
1173
1237
 
1174
1238
  Set `REPLICA_COUNT` from your deployment (Helm: `{{ .Values.replicaCount }}`) and
1175
1239
  the line does the multiplication for you; without it the line still names the
1176
1240
  formula. `voltro dev` deliberately does not print it — one process, no replicas.
1177
1241
 
1242
+ **Some connections are not in the pool, and the count is per process.** A
1243
+ connection running `LISTEN` cannot be returned to a pool, so the driver opens a
1244
+ standalone one. There are three such places and a full deployment can hold all
1245
+ three:
1246
+
1247
+ | Process | Connection | When |
1248
+ |---|---|---|
1249
+ | api `voltro serve` | CDC `LISTEN` consumer | `changeStrategy: 'cdc'` (the default on postgres) |
1250
+ | web `voltro start` | ISR invalidator `LISTEN` | any page declares `cacheInvalidatesOn` |
1251
+ | web `voltro start` | postgres ISR cache client | `SSR_CACHE=postgres` |
1252
+
1253
+ The third is not a `LISTEN` at all, which is why counting `LISTEN` rows in
1254
+ `pg_stat_activity` undercounts. Each process prints its own number in the boot
1255
+ line above — including `No connections outside the pool in this process` when
1256
+ there are none, so "counted, zero" is distinguishable from "not counted".
1257
+
1258
+ **And a rolling update needs the surge pod's connections too.** A budget sized
1259
+ for `replicaCount` is exactly full at steady state and short during every
1260
+ deploy: `maxSurge` adds a pod that opens a full pool of its own. If that pod
1261
+ cannot connect it never becomes ready, so the rollout does not complete and the
1262
+ cluster stays at the higher pod count — the deploy cannot free itself. Size for
1263
+ `(replicaCount + maxSurge) × (DB_MAX_CONNECTIONS + out-of-pool)`.
1264
+
1178
1265
  **`POD_IP` is each replica's identity, not only a workflow setting.**
1179
1266
 
1180
1267
  ```yaml
@@ -1203,6 +1290,67 @@ operator's own guidance, and better than ours was: a value taken from the
1203
1290
  release namespace cannot be left out of one environment's config file, because
1204
1291
  there is no file to forget.
1205
1292
 
1293
+ ### The framework's own background pollers
1294
+
1295
+ Two framework tasks ride the coordinated scheduler and write a row into
1296
+ `_voltro_schedule_claims` on every tick they win: the workflow **admission
1297
+ drainer** and the offloaded-**inference dispatcher**.
1298
+
1299
+ **Where a peer replica's write is visible here, they do not poll at all.** Each
1300
+ runs one tick at startup — not optional; it is what finds work a previous
1301
+ process left behind — and then stops until something arrives. The wake comes
1302
+ from the change events their queue tables already emit, which is the same
1303
+ mechanism the rest of the framework's reactivity runs on. Measured against a
1304
+ real Postgres on a deployment that never uses either queue: **2 claim rows in
1305
+ five minutes**, one per task, both written at boot.
1306
+
1307
+ That "where" is the whole condition, and it is satisfied by Postgres
1308
+ LISTEN/NOTIFY or by a broadcast broker (Redis/NATS — which a multi-replica
1309
+ deployment already runs for cross-replica reactivity). Without either, a
1310
+ *remote* replica's enqueue produces no local event, so stopping would mean
1311
+ sleeping through it. There the tasks back off to a ceiling instead:
1312
+
1313
+ ```sh
1314
+ VOLTRO_POLL_CEILING_MS=30000 # how long an arrival can wait when NOTHING woke the task
1315
+ ```
1316
+
1317
+ Nothing is lost in that case either — the replica that enqueued always sees its
1318
+ own write inline and drains it itself. What the ceiling covers is the narrower
1319
+ case of a *crashed* writer's lease being reclaimed by someone else.
1320
+
1321
+ A number worth knowing before you tune anything: on a two-replica deployment
1322
+ that had never enqueued into either queue, these two tasks accounted for **99.3 %
1323
+ of the claim ledger** — 2 506 rows an hour against 18 from the app's own eight
1324
+ schedules. A fixed interval has no way to learn a queue is empty. That is what
1325
+ changed; the ceiling is the fallback, not the fix.
1326
+
1327
+ ### Tuning the cadence
1328
+
1329
+ The intervals are declarable, with defaults most apps never change:
1330
+
1331
+ ```ts
1332
+ // app.config.ts
1333
+ export default {
1334
+ scheduling: {
1335
+ admissionDrainMs: 1000, // workflow admission drainer
1336
+ inferenceTickMs: 250, // offloaded-inference dispatcher
1337
+ cancelSweepMs: 2000, // cancelOn sweep
1338
+ pollCeilingMs: 30000, // idle ceiling, where nothing can wake a task
1339
+ },
1340
+ }
1341
+ ```
1342
+
1343
+ Each has a matching env var — `VOLTRO_ADMISSION_DRAIN_MS`,
1344
+ `VOLTRO_INFERENCE_TICK_MS`, `VOLTRO_CANCEL_SWEEP_MS`, `VOLTRO_POLL_CEILING_MS` —
1345
+ which **overrides** the config field, the same way `VOLTRO_TENANT_ISOLATION`
1346
+ overrides `tenancy.isolation`. The config is what a project declares; the env var
1347
+ is what an operator changes on a running deployment without a rebuild.
1348
+
1349
+ Lowering an interval does **not** make anything more responsive: an arrival
1350
+ already wakes the task at once. It only bounds the case where nothing announced
1351
+ the work. A value of `0` or a non-number is ignored rather than honoured — a
1352
+ zero interval would turn an idle task into a spin.
1353
+
1206
1354
  ### Workflow failover across replicas
1207
1355
 
1208
1356
  On a **SQL store** (postgres / mysql / mariadb / mssql), durable workflows survive a replica crash: completed `step({...})` activities are checkpointed in the cluster journal, so when a replica dies mid-run, a **surviving replica takes over the run and continues it from the last completed step** — it replays the completed steps rather than re-running them. (On sqlite the engine is single-process — durable within one replica, no cross-replica failover.) Two requirements:
@@ -1229,6 +1377,10 @@ Lower the lease for **faster failover**, at the cost of **false-positive reclaim
1229
1377
  - [ ] Serving pods run `voltro serve` (not `voltro dev`), with `VOLTRO_AUTO_MIGRATE=0`
1230
1378
  - [ ] Schema applied by a pre-deploy Job / initContainer (`voltro db apply`), not in the serving pod
1231
1379
  - [ ] `VOLTRO_MAX_RPC_BODY_BYTES` sane; ingress caps body size + per-IP rate
1380
+ - [ ] `VOLTRO_TRUSTED_PROXIES` set if you run behind an ingress AND rate-limit per IP
1381
+ - [ ] `VOLTRO_ALLOWED_ORIGINS` set if the web app is on a different origin than the api
1382
+ - [ ] Security headers reviewed (`VOLTRO_SECURITY_HEADERS`, `VOLTRO_CSP`); HSTS reaching the browser over https
1383
+ - [ ] Every `*.webhook.tsx` declares its verification, and each signature-verified one has its `VOLTRO_WEBHOOK_SECRET_<ID>`
1232
1384
  - [ ] `auth.anonymousTenantRequired: true` (unless the app serves anonymous public data)
1233
1385
  - [ ] `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME` pointed at your collector
1234
1386
  - [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
@@ -23,27 +23,52 @@ This guide gets you from zero to a running stack in under a minute.
23
23
 
24
24
  ## Scaffold a project
25
25
 
26
- `create-project` runs **inside an existing pnpm workspace** it walks up from your current directory looking for `pnpm-workspace.yaml` and writes the project into that workspace's `apps/` directory. Run it from the root of your workspace:
26
+ A Voltro monorepo is a pnpm workspace. `create-project` walks up from your current directory looking for `pnpm-workspace.yaml` and **when there is none, it creates the workspace root right there** before scaffolding. So an empty directory is a perfectly good starting point:
27
27
 
28
28
  ```bash
29
+ mkdir acme && cd acme
30
+
29
31
  pnpx voltro create-project acme \
30
32
  --api api-backend \
31
33
  --web frontend-landing \
32
34
  --port-range 5190-5199
33
35
  ```
34
36
 
35
- This creates `apps/acme/` inside the workspace with an `api/` (Voltro backend) and a `web/` (landing page) app. The project name is kebab-cased (so `Acme` becomes `acme`). The `--port-range` is recorded in `project.json` so every new app added later gets a unique port without you thinking about it.
37
+ You get:
38
+
39
+ ```text
40
+ acme/
41
+ pnpm-workspace.yaml # the package globs + the install-script decisions
42
+ package.json # dev / build / test / typecheck (plain `pnpm -r` scripts)
43
+ .gitignore # incl. .env.local, where `voltro dev` mints your secrets
44
+ .git/ # unless you were already inside a repo
45
+ AGENTS.md + CLAUDE.md # the agent guide, seeded per project
46
+ apps/acme/
47
+ api/ # Voltro backend
48
+ web/ # landing page
49
+ project.json # the project's port range + app map
50
+ ```
51
+
52
+ The project name is kebab-cased (so `Acme` becomes `acme`). The `--port-range` is recorded in `project.json` so every new app added later gets a unique port without you thinking about it.
53
+
54
+ Three things worth knowing about this first run:
55
+
56
+ - **The install-script question is already answered.** pnpm refuses to finish an install that has an undecided `postinstall` (`ERR_PNPM_IGNORED_BUILDS`), and a Voltro workspace pulls three — all transitive, none of them anything you picked. `pnpm-workspace.yaml` ships the answers with a reason on each line: `esbuild: true` (vite's compiler binary), `@parcel/watcher` and `msgpackr-extract` `false` (optional native accelerators with pure-JS fallbacks, so your first install needs no C++ toolchain). Change your mind with `pnpm approve-builds`.
57
+ - **Already have a workspace?** Nothing is overwritten. An existing `pnpm-workspace.yaml` is left alone, and only root scripts you *don't* already define are filled in. To prepare a directory without scaffolding anything yet, run `voltro init` — it creates the same workspace root and stops there.
58
+ - **It registers the project with the cloud control plane** (self-hosted tracking) unless you pass `--no-register`. Offline, in CI, or just not interested: `--no-register` skips the network call entirely.
36
59
 
37
60
  ## Boot it
38
61
 
39
- Install and run from the **workspace root** — there is no top-level `acme/` directory to `cd` into; the project lives under `apps/acme/`:
62
+ Install and run from the **workspace root** — there is no top-level `acme/api` to `cd` into; the project lives under `apps/acme/`:
40
63
 
41
64
  ```bash
42
65
  pnpm install
43
66
  pnpm dev
44
67
  ```
45
68
 
46
- The dev orchestrator boots every app in parallel with HMR. By default:
69
+ The root `dev` script is `pnpm -r --parallel dev`: it runs every workspace package that has a `dev` script, at once. No task runner to install — and apps that own their own dev loop (an Expo mobile app, a serverless bundle) simply don't define `dev`, so they opt out by construction. To run one app on its own, `pnpm --filter @acme/api dev`.
70
+
71
+ By default:
47
72
 
48
73
  - `api` listens on `:4000` (RPC over WebSocket on `/ws`)
49
74
  - `web` listens on the first port in your range (e.g. `5190`)
@@ -153,10 +178,10 @@ useMutation('billing', 'invoices.pay')
153
178
  Every procedure descriptor declares a globally unique `name`. That name is the **RPC tag**:
154
179
 
155
180
  ```ts
156
- defineQuery({ name: 'todos.list', /* ... */ })
157
- defineMutation({ name: 'todos.create', /* ... */ })
158
- defineAction({ name: 'support.ping', /* ... */ })
159
- defineStream({ name: 'agent.run', /* ... */ })
181
+ defineQuery({ name: 'todos.list', guards: [{ scope: 'todos:read' }], /* ... */ })
182
+ defineMutation({ name: 'todos.create', guards: [{ scope: 'todos:write' }], /* ... */ })
183
+ defineAction({ name: 'support.ping', guards: [{ scope: 'support:diagnostics' }], /* ... */ })
184
+ defineStream({ name: 'agent.run', guards: [{ scope: 'agents:run' }], /* ... */ })
160
185
  ```
161
186
 
162
187
  Hooks use the API name plus RPC tag. There is no hand-written client SDK per endpoint.
@@ -172,6 +197,34 @@ Procedures are split into two files:
172
197
 
173
198
  This keeps the wire contract importable from the client while server code stays server-only.
174
199
 
200
+ ## Every Procedure Declares Who May Call It
201
+
202
+ Dropping a descriptor into the tree **puts it on the wire**. So each one carries an access decision, and a descriptor that carries none is **refused at boot** — by `voltro dev`, by `voltro serve`, and by `voltro doctor` as a preflight. Exactly one of three:
203
+
204
+ ```ts
205
+ export const invoiceList = defineQuery({
206
+ name: 'invoices.list',
207
+ guards: [{ scope: 'invoices:read' }], // the caller must hold a scope
208
+ /* ... */
209
+ })
210
+
211
+ export const pricing = defineQuery({
212
+ name: 'pricing.current',
213
+ openAccess: 'public pricing page — reads no caller data', // anyone may, and why
214
+ /* ... */
215
+ })
216
+
217
+ export const stampAudit = defineMutation({
218
+ name: 'auditLog.stamp',
219
+ internal: true, // not on the wire at all
220
+ /* ... */
221
+ })
222
+ ```
223
+
224
+ `openAccess` takes a **reason, not a boolean**, and that is the whole design: it is what makes *"we decided this is open"* distinguishable from *"nobody looked"*. Without it, the only way to satisfy the gate would be to invent a guard — and the guard people invent is one every caller already holds, which reads as protection and enforces nothing.
225
+
226
+ The one to expect first: your **first self-written procedure file** will not boot until it has one of these three. Full rules in [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).
227
+
175
228
  ## The Main Primitives
176
229
 
177
230
  | Primitive | Use it for | Client hook | Durable? | Reactive? |
@@ -208,8 +261,11 @@ const { data: todos } = useSubscription('app', 'todos.list', {})
208
261
  When a mutation commits, the runtime emits change events. Queries whose `source` matches the changed table can update automatically.
209
262
 
210
263
  ```ts
211
- defineQuery({ name: 'todos.list', source: 'todos', /* ... */ })
212
- defineMutation({ name: 'todos.create', target: { table: 'todos', op: 'insert' }, /* ... */ })
264
+ defineQuery({ name: 'todos.list', source: 'todos', guards: [{ scope: 'todos:read' }], /* ... */ })
265
+ defineMutation({
266
+ name: 'todos.create', target: { table: 'todos', op: 'insert' },
267
+ guards: [{ scope: 'todos:write' }], /* ... */
268
+ })
213
269
  ```
214
270
 
215
271
  There is no `refetch` as the normal path. The subscription lives for the lifetime of the component.
@@ -287,11 +343,14 @@ You don't need Docker for dev — Voltro ships an in-memory store. You'll want D
287
343
  The CLI runs from the registry without a global install:
288
344
 
289
345
  ```bash
346
+ mkdir my-app && cd my-app
290
347
  pnpx voltro create-project my-app
291
348
  ```
292
349
 
293
350
  This is the smallest blast radius — `pnpx` fetches the latest CLI release into the local cache and discards it after.
294
351
 
352
+ An empty directory is enough: `create-project` writes the pnpm workspace root (`pnpm-workspace.yaml`, a root `package.json` with `dev`/`build`/`test`/`typecheck`, a `.gitignore`, `git init`) when it can't find one above you. Inside an existing workspace it adds nothing but the project. `voltro init` does the workspace-root half on its own, for when you want the repo prepared before you pick templates.
353
+
295
354
  ### 2. Per-project dependency
296
355
 
297
356
  Once your project exists, the CLI is already a `dependency` of the `api` app's `package.json`, so:
@@ -300,7 +359,7 @@ Once your project exists, the CLI is already a `dependency` of the `api` app's `
300
359
  pnpm --filter @my-app/api exec voltro dev
301
360
  ```
302
361
 
303
- The api template's own `dev` script is `voltro dev .`, so `pnpm dev` in the app runs the CLI directly. If your workspace ships a turbo dev pipeline, `pnpm dev` at the repo root delegates to it. You rarely call the filtered form directly.
362
+ The api template's own `dev` script is `voltro dev .`, so `pnpm dev` in the app runs the CLI directly. At the workspace root, `pnpm dev` is `pnpm -r --parallel dev` every app at once. You rarely call the filtered form directly.
304
363
 
305
364
  ### 3. Global install
306
365
 
@@ -318,7 +377,7 @@ voltro version
318
377
  voltro list-templates
319
378
  ```
320
379
 
321
- The second command prints the app templates the CLI can scaffold. You'll see the `api-backend*` templates (`api-backend`, `api-backend-mail`, `api-backend-mariadb`, `api-backend-storage`) and the `frontend-*` templates (`frontend-blank`, `frontend-docs`, `frontend-landing`) that's everything in place.
380
+ The second command prints the app templates the CLI can scaffold **dozens** of them, across four kinds: `api-*` backends, `frontend-*` web apps, an `edge-functions` serverless bundle, and `mobile-app` (Expo). The output is generated from the templates the installed CLI actually ships, so it is the authority on what your version can scaffold; do not go by a list in a doc. For what each one demonstrates and when to pick it, see the [app-template reference](/docs/reference/templates).
322
381
 
323
382
  ## Editor setup
324
383
 
@@ -363,13 +422,13 @@ Voltro replaces router and registry config with **filesystem conventions**. Drop
363
422
 
364
423
  | Suffix | What it is | Wired into |
365
424
  |---|---|---|
366
- | `*.query.ts` | Browser-safe reactive query descriptor: `defineQuery({ name, source, input, output })`. | Typed RPC group + client metadata. |
425
+ | `*.query.ts` | Browser-safe reactive query descriptor: `defineQuery({ name, source, input, output, guards })`. | Typed RPC group + client metadata. |
367
426
  | `*.query.server.ts` | Server executor for the matching query descriptor. | Reactive subscription runtime. |
368
- | `*.mutation.ts` | Browser-safe mutation descriptor: `defineMutation({ name, target, input, output, error })`. | Typed RPC group + auto-optimistic metadata. |
427
+ | `*.mutation.ts` | Browser-safe mutation descriptor: `defineMutation({ name, target, input, output, error, guards })`. | Typed RPC group + auto-optimistic metadata. |
369
428
  | `*.mutation.server.ts` | Server executor for the matching mutation descriptor. | Transactional mutation runner. |
370
- | `*.action.ts` | Browser-safe action descriptor: `defineAction({ name, input, output, error })`. | Typed RPC group. |
429
+ | `*.action.ts` | Browser-safe action descriptor: `defineAction({ name, input, output, error, guards })`. | Typed RPC group. |
371
430
  | `*.action.server.ts` | Server executor for the matching action descriptor. | Non-transactional action runner. |
372
- | `*.stream.ts` | Browser-safe one-shot stream descriptor: `defineStream({ name, input, element, error })`. | Typed streaming RPC group. |
431
+ | `*.stream.ts` | Browser-safe one-shot stream descriptor: `defineStream({ name, input, element, error, guards })`. | Typed streaming RPC group. |
373
432
  | `*.stream.server.ts` | Server executor returning an Effect `Stream`. | Plain server-to-client element streams. |
374
433
  | `*.workflow.tsx` | A durable Effect workflow. Survives restarts. | `@effect/workflow` runtime. |
375
434
  | `*.agent.tsx` | Browser-safe AI agent **descriptor**: `defineAgent({ name, input })`. Codegen-typed routes. | Agent runtime + client types. |
@@ -381,6 +440,18 @@ Voltro replaces router and registry config with **filesystem conventions**. Drop
381
440
 
382
441
  Procedure descriptors are intentionally separate from server executors. Descriptor files are safe for browser imports and codegen; `.server.ts` files can import the database, file system, SDK clients, secrets, and other server-only modules. Each descriptor has exactly one matching `.server.ts` file with the same primitive suffix. Workflows follow the same split: a browser-safe `*.workflow.tsx` descriptor (importing `workflow` from `@voltro/workflow/define`) paired with a `*.workflow.server.tsx` executor.
383
442
 
443
+ ### The access decision is not optional
444
+
445
+ `guards` appears in all four procedure signatures above because dropping a file into the tree **puts it on the wire**, and a wire-exposed procedure has to say who may call it. Exactly one of three:
446
+
447
+ ```ts
448
+ guards: [{ scope: 'notes:read' }] // the caller must hold a scope
449
+ openAccess: 'public pricing page — reads no caller data' // anyone may call it, and why
450
+ internal: true // not on the wire at all
451
+ ```
452
+
453
+ A descriptor that declares none of them is **refused at boot** — by `voltro dev`, by `voltro serve`, and by `voltro doctor` as a preflight. This is the one field a newly-created procedure file is most likely to be missing, and the failure is a boot refusal naming the file rather than a subtle runtime surprise. Full rules, including `openAccess`'s required reason and the per-app `security.defaultDeny` switch: [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).
454
+
384
455
  The browser-safe rule is **transitive**, and that is where it usually breaks. The codegen pulls every descriptor (and every workflow descriptor) value-level into `rpcGroup.generated.ts`, which the web client loads — so a descriptor plus *everything it imports* must stay free of server-only code (`node:*`, the `database` handle, `@voltro/ai`, cluster, plugins, `@voltro/protocol/session`). The classic mistake is not a literal `import 'node:crypto'` but a descriptor importing a shared typed-error or helper from a `lib/` file that *also* imports the database — which drags the whole schema graph into the browser bundle. Keep typed errors, Schemas, and pure helpers in files with zero server imports; put DB-backed guards in `.server.ts`. A leak shows up as the web app fetching hundreds of modules / tens of MB on first load, or crashing with `Module "node:crypto" has been externalized for browser compatibility`.
385
456
 
386
457
  ### Declaring a shared file browser-safe: `*.client.ts`
@@ -23,11 +23,13 @@ imports it directly. The React wrappers live behind `@voltro/local-first/react`
23
23
  > [`crdtText()` database column](#the-crdttext-database-column) (with its
24
24
  > authoritative server-side merge on the write path), the
25
25
  > [`SyncClient`](#the-syncclient-bi-directional-wire) that drives the queue over a
26
- > transport, [presence/awareness](#presence--awareness) via `usePresence`,
27
- > [durable IndexedDB persistence](#durable-persistence), and the
26
+ > transport, [`useCrdtText`](#a-collaborative-text-field-usecrdttext) the React
27
+ > binding for a collaborative text field — [presence/awareness](#presence--awareness)
28
+ > via `usePresence`, [durable IndexedDB persistence](#durable-persistence), and the
28
29
  > [`localFirst` table mixin](#the-localfirst-table-mixin). What remains is a thin
29
30
  > [runtime binding](#whats-shipped-vs-a-runtime-seam) to provisioned infra
30
- > (a broker at scale, a running app's transport) not un-built framework code.
31
+ > (a broker at scale) plus the two app-specific tags `useCrdtText` is pointed at
32
+ > not un-built framework code.
31
33
 
32
34
  ## CRDT text: `crdtText` + `mergeCrdtStates`
33
35
 
@@ -139,6 +141,69 @@ Everything below the two transport functions — the drain loop, retry/attempt
139
141
  counting, optimistic local merge, durable persistence — is in the client and
140
142
  tested against an in-memory dispatcher that mirrors the server's merge.
141
143
 
144
+ ## A collaborative text field: `useCrdtText`
145
+
146
+ `useCrdtText` is the React binding over that wire — one `crdtText()` cell, bound
147
+ to the mutation that writes it and the reactive query that streams it:
148
+
149
+ ```tsx
150
+ import { useMutation, useSubscription } from '@voltro/client'
151
+ import { useCrdtText } from '@voltro/local-first/react'
152
+
153
+ function BodyEditor({ id }: { id: string }) {
154
+ const row = useSubscription<{ body: Uint8Array | null }>('app', 'documents.byId', { id })
155
+ const save = useMutation<{ id: string; body: Uint8Array }>('app', 'documents.setBody')
156
+
157
+ const body = useCrdtText({
158
+ cell: { table: 'documents', id, column: 'body' },
159
+ remote: row.data?.body ?? null, // what the server currently holds
160
+ push: (w) => save.mutate({ id: w.id, body: w.update }), // deliver a local edit
161
+ })
162
+
163
+ return (
164
+ <>
165
+ <textarea value={body.text} onChange={(e) => body.setText(e.target.value)} />
166
+ {body.synced ? null : <em>saving… ({body.outstanding})</em>}
167
+ </>
168
+ )
169
+ }
170
+ ```
171
+
172
+ The hook owns one `SyncClient` per `(table, id, column)` cell — created and
173
+ closed with the component — re-renders on a local edit, an ack or incoming
174
+ merged state, and folds the streamed row back in. It returns `text`,
175
+ `insert(index, text)`, `delete(index, length)`, `setText(next)`, the encoded
176
+ `state`, `outstanding` / `synced`, and `setOnline`.
177
+
178
+ <Callout type="warn">
179
+ **`setText` is a span diff, and that is the whole point.** A `<textarea>` hands
180
+ you the entire new string, so the obvious binding is "clear the document,
181
+ insert the new text" — a delete-all/insert-all, which is exactly the
182
+ last-write-wins behaviour a CRDT is chosen to prevent: two people typing in
183
+ different paragraphs each erase the other's, and the text looks right on
184
+ whichever peer typed last. `useCrdtText` diffs the common prefix and suffix and
185
+ emits ONE delete plus ONE insert, so an edit outside the changed span survives.
186
+ The diff is exported as `crdtTextEdit(before, after)` if you drive the document
187
+ yourself.
188
+ </Callout>
189
+
190
+ Two things stay yours to name, because nothing can derive them: **which
191
+ mutation** writes the column and **which query** streams the row. Voltro
192
+ generates no per-table CRUD surface, so the hook takes those two as `push` and
193
+ `remote` — the same shape as `usePresence`'s injected channel. Everything under
194
+ them (client lifecycle, optimistic merge, offline queue, bounded retry, durable
195
+ persistence, the edit encoding) is framework code.
196
+
197
+ The descriptors on the other end declare the column as bytes-over-base64 —
198
+ `Uint8Array` in the handler, a base64 string on the wire:
199
+
200
+ ```ts
201
+ import { Schema } from 'effect'
202
+
203
+ // documents.byId output (and documents.setBody input)
204
+ body: Schema.NullOr(Schema.Uint8ArrayFromBase64)
205
+ ```
206
+
142
207
  ## Presence & awareness
143
208
 
144
209
  `usePresence(roomId, self, { channel })` publishes this peer's ephemeral state
@@ -168,6 +233,16 @@ Redis/NATS at scale (both already shipped). `createInMemoryPresenceChannel()` is
168
233
  the local/test transport. Join/leave, announce-back discovery, cursor
169
234
  propagation, and TTL expiry live in the pure `createPresenceRoom` the hook wraps.
170
235
 
236
+ > **Two different hooks share the name `usePresence`, and they are not
237
+ > interchangeable.** THIS one (`@voltro/local-first/react`) is peer-to-peer
238
+ > awareness over a pub/sub channel — `usePresence(roomId, self, { channel })` →
239
+ > `{ presence, others, setPresence }` — for high-frequency cursor/selection
240
+ > state that must never touch the database.
241
+ > [`@voltro/plugin-presence`](/docs/plugins/presence)'s is a server-backed
242
+ > roster — `usePresence(channel, options)` → the list of members whose heartbeat
243
+ > is fresh, plus a `useTyping` indicator, through the app's own rpc. Reach for
244
+ > the plugin for "who is here"; reach for this one for "where is their cursor".
245
+
171
246
  ## The offline sync queue
172
247
 
173
248
  `useSyncQueue()` is a reactive view over a **pure, tested reducer**: writes made
@@ -252,7 +327,7 @@ code already speaks:
252
327
 
253
328
  | Runtime seam | What it binds | Why it's a binding, not code |
254
329
  | --- | --- | --- |
255
- | **Sync transport a running app** | The `SyncClient`'s `push`/`onRemoteState` to one app's `useMutation` + `useSubscription`. | The two functions are app-specific glue; the drain loop, merge, retry, and persistence all ship and are tested. |
330
+ | **Two app-specific tags** | Which mutation writes the `crdtText()` column, and which reactive query streams the row, in [`useCrdtText`](#a-collaborative-text-field-usecrdttext). | Voltro generates no per-table CRUD surface, so there is nothing to derive them from. The client lifecycle, optimistic merge, offline queue, retry, persistence and edit encoding all ship. |
256
331
  | **Presence channel → a broker at scale** | The `PresenceChannel` to a provisioned Redis/NATS broker. | It's a network hop over an already-shipped broker; the awareness logic ships and is tested over the in-memory channel. |
257
332
  | **wa-sqlite / Turso adapter** *(optional)* | A SQL durable adapter for cross-tab queries, behind `PersistenceAdapter`. | IndexedDB is the durable default today; a SQL backing is a sibling factory, nothing above it changes. |
258
333