@voltro/cli 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/CHANGELOG.md +1968 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-B8aoJvuw.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
  11. package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
  12. package/dist/appGraph-zuMGKVYX.js +2 -0
  13. package/dist/appPort-B_HpJ_ck.js +48 -0
  14. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  15. package/dist/baselineCommand-DIttzO8A.js +227 -0
  16. package/dist/bin.js +71 -28
  17. package/dist/build-CD8K4XOr.js +711 -0
  18. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  19. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  20. package/dist/checkCommand-Ct9xkTrS.js +232 -0
  21. package/dist/checkCommand-DKpDLlqu.js +2 -0
  22. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  23. package/dist/cliError-BmdYnghb.js +10 -0
  24. package/dist/cliOutput-D1tSBoRM.js +15 -0
  25. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  26. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  27. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  28. package/dist/clusterCmd-D5wsCmA_.js +54 -0
  29. package/dist/codegen-CYM3Zqrf.js +605 -0
  30. package/dist/codegen-ChBi_hVa.js +2 -0
  31. package/dist/codegenCommand-C4YoQIc2.js +30 -0
  32. package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
  33. package/dist/commandRunner-BLAEFLjp.js +47 -0
  34. package/dist/commands-BE8E7zF3.js +816 -0
  35. package/dist/connectionConfig-UFlIEiys.js +66 -0
  36. package/dist/dashboardCommand-D7SgZGaN.js +25 -0
  37. package/dist/dataCommand-BhYwDgg-.js +537 -0
  38. package/dist/dataProfile-dW-PsfLB.js +15 -0
  39. package/dist/dbCommand-DS4b97Is.js +2 -0
  40. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
  41. package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
  42. package/dist/dev-D2BikO7a.js +3 -0
  43. package/dist/devActivity-Dx_3nnGv.js +100 -0
  44. package/dist/devActivity.js +1 -1
  45. package/dist/dialectDriver-CgXnDfec.js +39 -0
  46. package/dist/discover-C9XKJDco.js +25 -0
  47. package/dist/doctorCommand-CM4Ch9C7.js +2 -0
  48. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
  49. package/dist/dormancyCommand-QewYug_s.js +69 -0
  50. package/dist/e2eCmd-BRabZww-.js +147 -0
  51. package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
  52. package/dist/envCommand-CCGPRQY1.js +60 -0
  53. package/dist/evalCommand-6RUfPen4.js +118 -0
  54. package/dist/evolveCommand-CHsLCtDf.js +281 -0
  55. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  56. package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
  57. package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
  58. package/dist/generateCommand-DbgcUpGw.js +147 -0
  59. package/dist/index.d.ts +45 -0
  60. package/dist/index.js +4 -3
  61. package/dist/infoCommand-DwOgK1t6.js +60 -0
  62. package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
  63. package/dist/inspect-P4pxoMaV.js +2 -0
  64. package/dist/inspectCmd-EHFZ9yYu.js +224 -0
  65. package/dist/inspectFetch-EMuhTG_9.js +151 -0
  66. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  67. package/dist/loadEnv-D9nEOClM.js +44 -0
  68. package/dist/logFileSink-C_D2wRN1.js +105 -0
  69. package/dist/logsCmd-D36xK7Zu.js +260 -0
  70. package/dist/manifestBuild-COkJoyAr.js +2 -0
  71. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
  72. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  73. package/dist/migrate-BV7I-ZHZ.js +83 -0
  74. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  75. package/dist/newCommand-COWOJ1_E.js +156 -0
  76. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  77. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  78. package/dist/pageConvention-cEiRxdab.js +5 -0
  79. package/dist/privacyCommand-C-Df56U_.js +146 -0
  80. package/dist/probeCommand-CZfaaUOZ.js +122 -0
  81. package/dist/projectScaffold-DmzEKHib.js +2 -0
  82. package/dist/projectScaffold-LMMtaavR.js +814 -0
  83. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  84. package/dist/renderProfile-1OWWAAtx.js +81 -0
  85. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  86. package/dist/runtimeTrace-CH3eUiMw.js +91 -0
  87. package/dist/scheduleCmd-DQRu6BZC.js +149 -0
  88. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  89. package/dist/schemaIr-UJybUUZW.js +103 -0
  90. package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
  91. package/dist/seedRunner-ZmLSqNe2.js +333 -0
  92. package/dist/serveCommand-CbDHU6l-.js +2 -0
  93. package/dist/serveCommand-iwlUBNS1.js +1766 -0
  94. package/dist/serveEntry.js +5 -5
  95. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  96. package/dist/start-BgN62boB.js +3 -0
  97. package/dist/start-T4VesWiM.js +1087 -0
  98. package/dist/startEntry.js +2 -2
  99. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  100. package/dist/storageCommand-Co6NfLqN.js +42 -0
  101. package/dist/templates-De8IR5-c.js +102 -0
  102. package/dist/test-CI6iDsYc.js +115 -0
  103. package/dist/tracesCmd-DStmCJPi.js +232 -0
  104. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  105. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  106. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  107. package/dist/updateCommand-BlMZhWgO.js +2 -0
  108. package/dist/updateCommand-x0pI_x-B.js +585 -0
  109. package/dist/webDev-BcykISYQ2.js +2 -0
  110. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
  111. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  112. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  113. package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
  114. package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
  115. package/package.json +209 -17
  116. package/templates/AGENTS.core.md +58 -3
  117. package/templates/AGENTS.md +64 -7
  118. package/templates/agent-docs/_index.md +6 -4
  119. package/templates/agent-docs/_manifest.json +23 -6
  120. package/templates/agent-docs/ai.md +370 -0
  121. package/templates/agent-docs/authentication.md +313 -31
  122. package/templates/agent-docs/caching.md +6 -0
  123. package/templates/agent-docs/cli.md +853 -50
  124. package/templates/agent-docs/data.md +608 -12
  125. package/templates/agent-docs/database/migrations.md +223 -25
  126. package/templates/agent-docs/database/misc.md +156 -40
  127. package/templates/agent-docs/database/querying.md +19 -1
  128. package/templates/agent-docs/database/scaling.md +60 -0
  129. package/templates/agent-docs/database/schema.md +1 -0
  130. package/templates/agent-docs/database/seedsdialects.md +208 -19
  131. package/templates/agent-docs/database/transactions.md +68 -0
  132. package/templates/agent-docs/deployment.md +284 -25
  133. package/templates/agent-docs/introduction.md +88 -17
  134. package/templates/agent-docs/local-first-mobile.md +79 -4
  135. package/templates/agent-docs/multi-tenancy.md +188 -42
  136. package/templates/agent-docs/observability.md +58 -3
  137. package/templates/agent-docs/plugins/ai-flows.md +247 -2
  138. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  139. package/templates/agent-docs/plugins/audit.md +37 -1
  140. package/templates/agent-docs/plugins/auth-social.md +143 -0
  141. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  142. package/templates/agent-docs/plugins/auth.md +131 -6
  143. package/templates/agent-docs/plugins/billing.md +132 -15
  144. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  145. package/templates/agent-docs/plugins/clickhouse.md +32 -1
  146. package/templates/agent-docs/plugins/duckdb.md +1 -1
  147. package/templates/agent-docs/plugins/flags.md +132 -0
  148. package/templates/agent-docs/plugins/governance.md +105 -7
  149. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  150. package/templates/agent-docs/plugins/presence.md +13 -2
  151. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  152. package/templates/agent-docs/plugins/search.md +162 -8
  153. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  154. package/templates/agent-docs/plugins/storage.md +11 -0
  155. package/templates/agent-docs/plugins/webhooks.md +105 -0
  156. package/templates/agent-docs/plugins.md +152 -18
  157. package/templates/agent-docs/reference.md +60 -3
  158. package/templates/agent-docs/releases.md +1117 -0
  159. package/templates/agent-docs/routing.md +43 -25
  160. package/templates/agent-docs/scheduling.md +27 -0
  161. package/templates/agent-docs/schema-driven-ui.md +92 -12
  162. package/templates/agent-docs/security.md +426 -0
  163. package/templates/agent-docs/templates/apibackends.md +87 -18
  164. package/templates/agent-docs/templates/appshells.md +32 -14
  165. package/templates/agent-docs/templates/overview.md +13 -8
  166. package/templates/agent-docs/testing.md +211 -14
  167. package/templates/agent-docs/whats-new.md +98 -136
  168. package/templates/agent-docs/workflows.md +231 -14
  169. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  170. package/templates/apps/api-ai/package.json +8 -7
  171. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  172. package/templates/apps/api-auth/package.json +9 -8
  173. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  174. package/templates/apps/api-backend/package.json +12 -8
  175. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  176. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  177. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  178. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  179. package/templates/apps/api-backend-deactivation/package.json +8 -7
  180. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  181. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mail/package.json +9 -8
  183. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  185. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-mariadb/package.json +10 -9
  187. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  189. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  190. package/templates/apps/api-backend-sqlite/package.json +9 -8
  191. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  192. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  193. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  194. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  195. package/templates/apps/api-backend-storage/package.json +9 -8
  196. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  197. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  198. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  199. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  200. package/templates/apps/api-cms/app.config.ts +19 -0
  201. package/templates/apps/api-cms/authz.ts +63 -0
  202. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  203. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  204. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  205. package/templates/apps/api-cms/package.json +11 -10
  206. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  207. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  208. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  209. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  210. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  211. package/templates/apps/api-collab/package.json +9 -8
  212. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  213. package/templates/apps/api-data-advanced/package.json +9 -8
  214. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  215. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  216. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  217. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  218. package/templates/apps/api-durable/package.json +9 -8
  219. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  220. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  221. package/templates/apps/api-feature-flags/package.json +10 -9
  222. package/templates/apps/api-governance/README.md +30 -8
  223. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  224. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  225. package/templates/apps/api-governance/database/schema.ts +16 -4
  226. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  227. package/templates/apps/api-governance/package.json +9 -8
  228. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  229. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  230. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  231. package/templates/apps/api-kv/package.json +9 -8
  232. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  233. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  234. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  235. package/templates/apps/api-moderation/package.json +9 -8
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  237. package/templates/apps/api-observability/package.json +9 -8
  238. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  239. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  240. package/templates/apps/api-ratelimit/package.json +9 -8
  241. package/templates/apps/api-rbac/package.json +9 -8
  242. package/templates/apps/api-rest/package.json +8 -7
  243. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  244. package/templates/apps/api-saas/package.json +12 -11
  245. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  246. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  247. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  248. package/templates/apps/api-saas-starter/authz.ts +75 -0
  249. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  250. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  251. package/templates/apps/api-saas-starter/package.json +15 -11
  252. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  253. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  254. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  255. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  256. package/templates/apps/api-search/package.json +9 -8
  257. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  258. package/templates/apps/api-status/README.md +10 -3
  259. package/templates/apps/api-status/app.config.ts +8 -3
  260. package/templates/apps/api-status/authz.ts +5 -3
  261. package/templates/apps/api-status/package.json +9 -8
  262. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  263. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  264. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  265. package/templates/apps/api-status/tests/status.test.ts +9 -1
  266. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  267. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  268. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  269. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  270. package/templates/apps/api-versioning/package.json +9 -8
  271. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  272. package/templates/apps/api-webhooks/package.json +10 -9
  273. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  274. package/templates/apps/changelog/package.json +8 -6
  275. package/templates/apps/edge-functions/package.json +2 -2
  276. package/templates/apps/frontend-admin/package.json +10 -8
  277. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  278. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  279. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  280. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  281. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  282. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  283. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  284. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  285. package/templates/apps/frontend-app/app.config.ts +4 -3
  286. package/templates/apps/frontend-app/package.json +11 -8
  287. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  288. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  289. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  290. package/templates/apps/frontend-auth/package.json +10 -8
  291. package/templates/apps/frontend-blank/package.json +9 -7
  292. package/templates/apps/frontend-cms/package.json +11 -9
  293. package/templates/apps/frontend-collab/package.json +12 -9
  294. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  295. package/templates/apps/frontend-contact/package.json +9 -7
  296. package/templates/apps/frontend-dashboard/package.json +9 -7
  297. package/templates/apps/frontend-docs/package.json +9 -7
  298. package/templates/apps/frontend-i18n/package.json +8 -6
  299. package/templates/apps/frontend-landing/package.json +9 -7
  300. package/templates/apps/frontend-portal/package.json +10 -8
  301. package/templates/apps/frontend-saas/app.config.ts +10 -6
  302. package/templates/apps/frontend-saas/package.json +10 -8
  303. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  304. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  305. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  306. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  307. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  308. package/templates/apps/frontend-spa/package.json +9 -7
  309. package/templates/apps/frontend-ssr/package.json +9 -7
  310. package/templates/apps/frontend-ssr-api/package.json +10 -8
  311. package/templates/apps/frontend-static-blog/package.json +8 -6
  312. package/templates/apps/frontend-status/package.json +10 -8
  313. package/templates/apps/mobile-app/README.md +1 -0
  314. package/templates/apps/mobile-app/package.json +4 -2
  315. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  316. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  317. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  318. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  319. package/templates/baselines/bare/.env.example +14 -0
  320. package/templates/baselines/bare/baseline.json +4 -4
  321. package/templates/baselines/compose/.env.example +14 -0
  322. package/templates/baselines/compose/README.md +1 -1
  323. package/templates/baselines/compose/baseline.json +5 -5
  324. package/templates/baselines/compose-mariadb/.env.example +14 -0
  325. package/templates/baselines/compose-mariadb/README.md +1 -1
  326. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  327. package/templates/baselines/helm/.env.example +14 -0
  328. package/templates/baselines/helm/baseline.json +4 -4
  329. package/dist/apiBuild-C-x9YacA.js +0 -2
  330. package/dist/appGraph-CvQCte0z.js +0 -2
  331. package/dist/checkCommand-DRovTKza.js +0 -2
  332. package/dist/commands-CJfepbm4.js +0 -11541
  333. package/dist/dbCommand-b1gum4td.js +0 -2
  334. package/dist/dev-iiMtlkfs.js +0 -3
  335. package/dist/devActivity-BhIu6ncs.js +0 -159
  336. package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
  337. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  338. package/dist/inspect-mmBuRXmy.js +0 -2
  339. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  340. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  341. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  342. package/dist/start-C0koT0UO.js +0 -1084
  343. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -11,7 +11,7 @@
11
11
 
12
12
  _Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps._
13
13
 
14
- Voltro Cloud is the managed tier of the same open-core runtime you run locally. It's not a fork — `voltro start` is what powers your production instances, just with custom domains, multi-region routing, provisioned Postgres / Redis / object storage, and a control plane that handles deploys.
14
+ Voltro Cloud is the managed tier of the same runtime you run yourself. It's not a fork — `voltro start` is what powers your production instances, just with custom domains, multi-region routing, provisioned Postgres / Redis / object storage, and a control plane that handles deploys.
15
15
 
16
16
  **Status:** managed cloud **deploy + provisioning is coming soon** and is not yet available. What's live today is the **Free control plane**: sign up and use it to **register and observe your self-hosted apps** (hosted observability, quotas, governance, teams). The managed-tier features described below are the planned shape of Voltro Cloud — see the per-section notes.
17
17
 
@@ -117,7 +117,7 @@ voltro cloud import # scaffold voltro.cloud.toml from the app's app.config.ts
117
117
 
118
118
  _Run Voltro on your own infra — Docker compose, env vars, reverse proxy, scaling._
119
119
 
120
- Voltro is open-core. The runtime that powers Voltro Cloud is the same runtime you run locally with `voltro start`. Self-hosting is fully supported — no separate "lite" runtime, no feature gates around the core API.
120
+ The runtime that powers Voltro Cloud is the same runtime you run yourself with `voltro start` — one codebase, one license. Self-hosting is fully supported — no separate "lite" runtime, no feature gates around the core API.
121
121
 
122
122
  ## The shape of a self-hosted deploy
123
123
 
@@ -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.
@@ -1073,17 +1129,29 @@ Durable in-DB traces are **off in production by default** — traces belong in y
1073
1129
 
1074
1130
  ## 7. Graceful shutdown
1075
1131
 
1076
- On `SIGTERM` / `SIGINT`, `voltro serve` shuts down cleanly (exit 0): it stops
1077
- schedulers, detaches subscribers / reactions / aggregates, drains the analytics
1078
- sink, stops accepting new connections while **finishing already-accepted
1079
- requests**, and closes the SQL connection pool **last** (waiting for in-flight
1080
- transactions). Verified under concurrent load accepted requests complete.
1081
-
1082
- **But the app cannot drain a rolling update by itself.** The runtime
1083
- (`NodeRuntime`) owns the `SIGTERM` signal and closes the HTTP listener promptly
1084
- so a request that *arrives* during shutdown is refused. Failing readiness from
1085
- inside the process doesn't help: the listener is already closing. Draining is a
1086
- **k8s-layer** job, done with a **preStop hook**:
1132
+ On `SIGTERM` / `SIGINT`, `voltro serve` shuts down cleanly (exit 0), in this
1133
+ order: it stops accepting new connections and lets **in-flight requests finish
1134
+ against a fully-alive app** (bounded the request drain gets 60% of the
1135
+ shutdown grace, so the teardown behind it always fits inside the deadline),
1136
+ then stops schedulers, detaches subscribers / reactions / aggregates, drains
1137
+ the analytics sink and mirror, ends any remaining WebSocket, and closes the
1138
+ SQL connection pool **last** (waiting for in-flight transactions). Verified
1139
+ against a real serve under signal: a request in flight when `SIGTERM` lands
1140
+ completes with a full response before the process exits no preStop hook, no
1141
+ orchestrator. A bare `voltro serve` under docker compose drains itself.
1142
+
1143
+ The **transactional-outbox worker** is part of that sequence: its poll timer and
1144
+ its change subscription are released, and a delivery already in flight is
1145
+ awaited, before the pool closes. An outbox row that was still pending is not
1146
+ lost — it is durable, and the next process's first pass picks it up, which is
1147
+ one of the three reasons that poll exists.
1148
+
1149
+ **What an orchestrator still adds: routing.** The app finishes every request
1150
+ it has *accepted* — but a request that *arrives* after `SIGTERM` is refused
1151
+ (the listener closes immediately, on purpose), and only the layer that routes
1152
+ traffic can stop sending it. Failing readiness from inside the process doesn't
1153
+ help: the listener is already closed. Under k8s, close that window with a
1154
+ **preStop hook**:
1087
1155
 
1088
1156
  ```yaml
1089
1157
  spec:
@@ -1101,10 +1169,15 @@ spec:
1101
1169
  command: ["sh", "-c", "sleep 5"]
1102
1170
  ```
1103
1171
 
1104
- Without the preStop hook, a rolling update drops the small window of requests
1105
- that route to a terminating pod before k8s finishes removing it from the Service
1106
- endpoints. With it, that window is drained. `terminationGracePeriodSeconds` must
1107
- be larger than the sleep plus the app's own teardown, or k8s SIGKILLs mid-drain.
1172
+ Without the preStop hook, a rolling update refuses the small window of requests
1173
+ that still route to a terminating pod before k8s finishes removing it from the
1174
+ Service endpoints refused with a connection error, not truncated mid-response
1175
+ (everything already accepted completes either way). With it, that window is
1176
+ served too. `terminationGracePeriodSeconds` must be larger than the sleep plus
1177
+ the app's own teardown, or k8s SIGKILLs mid-drain. Environments with no
1178
+ endpoint removal at all — docker compose above all — need nothing: there is no
1179
+ routing layer to lag behind the shutdown, so the built-in drain is the whole
1180
+ story.
1108
1181
 
1109
1182
  **Bound the app's own teardown with `VOLTRO_SHUTDOWN_GRACE_MS`.** After
1110
1183
  `SIGTERM`, the runtime runs its finalizers (pool close, plugin `onDeactivate`,
@@ -1128,10 +1201,10 @@ spec:
1128
1201
  value: "22000"
1129
1202
  ```
1130
1203
 
1131
- The close is clean on the client side too: on shutdown each live WebSocket is
1132
- closed with a proper close frame (not an abrupt socket drop), and the web
1133
- client's supervisor reconnects on any close so an open dashboard re-attaches
1134
- to a healthy replica across a rolling deploy without a page reload.
1204
+ Live WebSockets are ENDED promptly at shutdown the drain never lets an open
1205
+ socket hold the process to the deadline and the web client's supervisor
1206
+ treats any close as a reconnect signal, so an open dashboard re-attaches to a
1207
+ healthy replica across a rolling deploy without a page reload.
1135
1208
 
1136
1209
  ## 8. Multiple replicas
1137
1210
 
@@ -1305,7 +1378,7 @@ VOLTRO_WORKFLOW_FAILOVER_HEARTBEAT=5 # lease-refresh cadence (default 10; kee
1305
1378
 
1306
1379
  Lower the lease for **faster failover**, at the cost of **false-positive reclaims**: if a *healthy* replica is paused longer than the lease by a GC pause or a DB-latency spike, another replica may briefly also claim its shards. Keep the heartbeat around a third of the lease so one slow refresh doesn't trip a reclaim. For crash detection that doesn't depend on the timeout at all, pair it with a K8s **liveness probe** so a dead pod is removed promptly.
1307
1380
 
1308
- (A separate concern is *new*-message pickup: a workflow triggered on the replica that owns its shard starts immediately, but one owned by ANOTHER replica is otherwise picked up on that replica's next storage poll — up to 10s. **If you run a broadcast broker (Redis/NATS — which a multi-replica deployment already does for cross-replica reactivity), this is automatic and near-instant**: a trigger pushes a "wake" over the bus and the shard owner re-polls at once, on any SQL dialect. Without a broker, tune `VOLTRO_WORKFLOW_POLL_INTERVAL=2` instead. Unrelated to the failover path above.)
1381
+ (A separate concern is *new*-message pickup: a workflow triggered on the replica that owns its shard starts immediately, but one owned by ANOTHER replica is otherwise picked up on that replica's next storage poll — up to 10s. **If you run a broadcast broker (Redis/NATS — which a multi-replica deployment already does for cross-replica reactivity), this is automatic and near-instant**: a trigger pushes a "wake" over the bus and the shard owner re-polls at once, on any SQL dialect. **Without a broker, the change stream does the same job wherever remote changes reach the spine** — a Postgres-only multi-replica fleet (LISTEN/NOTIFY CDC) is the common case: a remote replica's signal, start context, or run transition arrives as a change event and triggers an immediate, coalesced re-poll, so signal/step latency stops being poll-bounded there too. Only with *neither* a broker *nor* CDC does the poll interval remain the bound — tune `VOLTRO_WORKFLOW_POLL_INTERVAL=2` then. Unrelated to the failover path above.)
1309
1382
 
1310
1383
  ## Checklist
1311
1384
 
@@ -1315,6 +1388,10 @@ Lower the lease for **faster failover**, at the cost of **false-positive reclaim
1315
1388
  - [ ] Serving pods run `voltro serve` (not `voltro dev`), with `VOLTRO_AUTO_MIGRATE=0`
1316
1389
  - [ ] Schema applied by a pre-deploy Job / initContainer (`voltro db apply`), not in the serving pod
1317
1390
  - [ ] `VOLTRO_MAX_RPC_BODY_BYTES` sane; ingress caps body size + per-IP rate
1391
+ - [ ] `VOLTRO_TRUSTED_PROXIES` set if you run behind an ingress AND rate-limit per IP
1392
+ - [ ] `VOLTRO_ALLOWED_ORIGINS` set if the web app is on a different origin than the api
1393
+ - [ ] Security headers reviewed (`VOLTRO_SECURITY_HEADERS`, `VOLTRO_CSP`); HSTS reaching the browser over https
1394
+ - [ ] Every `*.webhook.tsx` declares its verification, and each signature-verified one has its `VOLTRO_WEBHOOK_SECRET_<ID>`
1318
1395
  - [ ] `auth.anonymousTenantRequired: true` (unless the app serves anonymous public data)
1319
1396
  - [ ] `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME` pointed at your collector
1320
1397
  - [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
@@ -1495,3 +1572,185 @@ Air-gapped and enterprise deployments attest seats contractually instead; no tel
1495
1572
 
1496
1573
  - [Voltro Cloud](./voltro-cloud.md) — the control plane these features live in
1497
1574
  - [Self-hosting](./self-hosting.md) — running Voltro yourself
1575
+
1576
+
1577
+
1578
+ ---
1579
+
1580
+ <!-- source: en/deployment/platforms.md -->
1581
+ ## Platform recipes
1582
+
1583
+ _Deploy the same container to Fly.io, Railway, Render, or a Hetzner VM — one image, four wrappers._
1584
+
1585
+ Every recipe on this page deploys the **same artifact**: the production image
1586
+ from the compose baseline's `docker/api.Dockerfile` (`voltro baseline set
1587
+ compose` writes it into your project). The platforms differ only in the wrapper
1588
+ — how they build it, which env vars they inject, and how they health-check it.
1589
+
1590
+ **What is verified, stated exactly.** The container itself is the tested part:
1591
+ it builds from a clean context, boots `voltro serve` from the precompiled serve
1592
+ bundle, survives the `pnpm deploy` relocation, and answers
1593
+ `/internal/readiness` with 200 — that loop runs in this repo's own validation.
1594
+ The platform wrapper files below are written against each platform's current
1595
+ config format and have **not** been executed against a live account of that
1596
+ platform; if one drifts from what the platform ships today, the container is
1597
+ still right and the fix is in the wrapper.
1598
+
1599
+ Three properties of the image every platform relies on:
1600
+
1601
+ - **`PORT` wins.** The port precedence is `PORT` > `--port` > `app.config.ts` —
1602
+ deliberately, because platforms assign through `PORT`. You never configure a
1603
+ port in the wrapper beyond telling the platform which one the app answers on.
1604
+ - **A missing secret refuses to boot.** `VOLTRO_SESSION_SECRET` unset is a
1605
+ clean, named boot refusal — not a server that signs with a default. Set
1606
+ secrets in the platform's secret store before the first deploy, or read the
1607
+ refusal message; both are correct outcomes.
1608
+ - **`/internal/readiness` flips to 200 only after the whole boot.** Use it as
1609
+ the health check everywhere; routing traffic on process-up instead of
1610
+ readiness is how a deploy serves 502s for the first seconds.
1611
+
1612
+ ## Fly.io
1613
+
1614
+ ```toml
1615
+ # fly.toml
1616
+ app = "my-voltro-api"
1617
+ primary_region = "fra"
1618
+
1619
+ [build]
1620
+ dockerfile = "docker/api.Dockerfile"
1621
+ build-args = { APP_PATH = "apps/my-app/api" }
1622
+
1623
+ [env]
1624
+ DB_DIALECT = "postgres"
1625
+
1626
+ [http_service]
1627
+ internal_port = 4000
1628
+ force_https = true
1629
+ auto_stop_machines = "stop"
1630
+ auto_start_machines = true
1631
+ min_machines_running = 0
1632
+
1633
+ [[http_service.checks]]
1634
+ path = "/internal/readiness"
1635
+ interval = "10s"
1636
+ timeout = "2s"
1637
+ ```
1638
+
1639
+ ```sh
1640
+ fly secrets set VOLTRO_SESSION_SECRET=$(openssl rand -base64 32) DB_URL=<from fly postgres attach>
1641
+ fly deploy
1642
+ ```
1643
+
1644
+ The one Fly-specific decision: `auto_stop_machines` gives you scale-to-zero,
1645
+ and a cold start pays the container boot. The serve bundle exists for exactly
1646
+ this — the framework-boot slice of a cold start is ~180–210 ms instead of ~1 s.
1647
+ Read [Scale to zero](/docs/deployment/scale-to-zero) before choosing
1648
+ `min_machines_running = 0` for an api that owns schedules: a machine that is
1649
+ never awake fires no cron.
1650
+
1651
+ ## Railway
1652
+
1653
+ Railway detects the Dockerfile; point it at the right one and set the build
1654
+ context to the repo root (the Dockerfile copies the whole workspace for
1655
+ `pnpm install`).
1656
+
1657
+ ```json
1658
+ // railway.json
1659
+ {
1660
+ "build": {
1661
+ "builder": "DOCKERFILE",
1662
+ "dockerfilePath": "docker/api.Dockerfile"
1663
+ },
1664
+ "deploy": {
1665
+ "healthcheckPath": "/internal/readiness",
1666
+ "restartPolicyType": "ON_FAILURE"
1667
+ }
1668
+ }
1669
+ ```
1670
+
1671
+ Set `VOLTRO_SESSION_SECRET` and `DB_URL` as service variables; Railway injects
1672
+ `PORT` and the image binds to it — no port config anywhere. The
1673
+ `APP_PATH` build arg goes into the service's build settings.
1674
+
1675
+ ## Render
1676
+
1677
+ ```yaml
1678
+ # render.yaml
1679
+ services:
1680
+ - type: web
1681
+ name: my-voltro-api
1682
+ runtime: docker
1683
+ dockerfilePath: ./docker/api.Dockerfile
1684
+ dockerContext: .
1685
+ healthCheckPath: /internal/readiness
1686
+ envVars:
1687
+ - key: DB_DIALECT
1688
+ value: postgres
1689
+ - key: VOLTRO_SESSION_SECRET
1690
+ sync: false
1691
+ - key: DB_URL
1692
+ fromDatabase:
1693
+ name: my-voltro-db
1694
+ property: connectionString
1695
+
1696
+ databases:
1697
+ - name: my-voltro-db
1698
+ plan: basic-1gb
1699
+ ```
1700
+
1701
+ `sync: false` makes the secret a dashboard-entered value that never lands in
1702
+ the blueprint file — the same "we ship no secret values" rule the framework
1703
+ enforces on its own templates applies to yours.
1704
+
1705
+ ## Hetzner (or any bare VM)
1706
+
1707
+ A VM is the compose baseline with a process manager on top — this is the one
1708
+ recipe whose whole stack is the already-validated path from
1709
+ [Self-hosting](/docs/deployment/self-hosting).
1710
+
1711
+ ```sh
1712
+ # once, on the VM
1713
+ apt-get install -y docker.io docker-compose-plugin
1714
+ git clone <your-repo> /srv/app && cd /srv/app
1715
+ cp .env.example .env # then fill in real values — nothing boots without them
1716
+ docker compose up -d --build
1717
+ ```
1718
+
1719
+ ```ini
1720
+ # /etc/systemd/system/voltro.service — survive reboots
1721
+ [Unit]
1722
+ Description=voltro stack
1723
+ Requires=docker.service
1724
+ After=docker.service
1725
+
1726
+ [Service]
1727
+ Type=oneshot
1728
+ RemainAfterExit=true
1729
+ WorkingDirectory=/srv/app
1730
+ ExecStart=/usr/bin/docker compose up -d
1731
+ ExecStop=/usr/bin/docker compose down
1732
+
1733
+ [Install]
1734
+ WantedBy=multi-user.target
1735
+ ```
1736
+
1737
+ Caddy (in the baseline compose) terminates TLS with an automatic Let's Encrypt
1738
+ certificate — point the domain's A record at the VM and the certificate is
1739
+ provisioned on first request. What a VM does NOT give you: rolling deploys
1740
+ (compose restarts in place — expect seconds of downtime per deploy, or put two
1741
+ VMs behind a load balancer), and managed Postgres backups —
1742
+ [`voltro data backup`](/docs/cli/data) plus the restore drill is your baseline,
1743
+ and the drill is the part people skip.
1744
+
1745
+ ## Which one
1746
+
1747
+ | | scale-to-zero | managed DB | rolling deploys | cost floor |
1748
+ |---|---|---|---|---|
1749
+ | Fly.io | yes (`auto_stop`) | Fly Postgres | yes | ~0 idle |
1750
+ | Railway | usage-based sleep | built-in | yes | ~0 idle |
1751
+ | Render | paid plans only | built-in | yes | fixed/instance |
1752
+ | Hetzner VM | no | bring your own | no (single VM) | fixed, cheapest at steady load |
1753
+
1754
+ An api that owns cron schedules should not scale to zero. An api with bursty
1755
+ traffic and no schedules is exactly what scale-to-zero is for. When in doubt,
1756
+ the boring answer — one always-on instance — is also the cheapest to operate.
@@ -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`)
@@ -102,7 +127,7 @@ Voltro is deliberately opinionated about boring things (HTTP, state, transport,
102
127
 
103
128
  ## What we won't do
104
129
 
105
- - **Lock you in.** Self-hosting is fully supported. Cloud is the premium tier of the same open-core runtime, not a fork.
130
+ - **Lock you in.** Self-hosting is fully supported. Cloud is the premium tier of the same runtime, not a fork.
106
131
  - **Multi-runtime grab-bag.** Effect-TS end-to-end — no Restate, no Temporal, no Inngest, not even opt-in.
107
132
  - **Magic.** Every file convention is documented; every generated file lives in `.framework/` and you can read it.
108
133
  - **Codegen you have to remember to run.** Schema flows from your tables to your React components automatically via Vite's module graph.
@@ -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`