@voltro/cli 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +1801 -0
  2. package/bin/nodeEnvironment.d.mts +30 -0
  3. package/bin/nodeEnvironment.mjs +158 -0
  4. package/bin/voltro.mjs +69 -5
  5. package/dist/addCommand-BNeoeSxe.js +124 -0
  6. package/dist/addCommand-aXSQveak.js +2 -0
  7. package/dist/agentsMd-BTchIZku.js +2 -0
  8. package/dist/agentsMd-mhQMF1bx.js +254 -0
  9. package/dist/apiBuild-BICVGnEC.js +2 -0
  10. package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
  11. package/dist/appPort-B_HpJ_ck.js +48 -0
  12. package/dist/baselineCommand-C2ClWZN3.js +2 -0
  13. package/dist/baselineCommand-DIttzO8A.js +227 -0
  14. package/dist/bin.js +71 -28
  15. package/dist/build-CE7Ca9gm.js +711 -0
  16. package/dist/cacheCommand-DA4OH9xt.js +42 -0
  17. package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
  18. package/dist/checkCommand-Dv8r9tGO.js +231 -0
  19. package/dist/checkCommand-YZDmkAGd.js +2 -0
  20. package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
  21. package/dist/cliError-BmdYnghb.js +10 -0
  22. package/dist/cliOutput-D1tSBoRM.js +15 -0
  23. package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
  24. package/dist/cloudClient-DWL-Hw_T.js +67 -0
  25. package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
  26. package/dist/clusterCmd-CNXKlcvD.js +54 -0
  27. package/dist/codegen-CYM3Zqrf.js +605 -0
  28. package/dist/codegen-ChBi_hVa.js +2 -0
  29. package/dist/codegenCommand-DC6w2tNZ.js +30 -0
  30. package/dist/codemodRunner-DRRqXR74.js +5243 -0
  31. package/dist/commandRunner-BLAEFLjp.js +47 -0
  32. package/dist/commands-gutsz-Ac.js +808 -0
  33. package/dist/connectionConfig-UFlIEiys.js +66 -0
  34. package/dist/dashboardCommand-3YG8p-UA.js +25 -0
  35. package/dist/dataCommand-qL0r7fPO.js +535 -0
  36. package/dist/dataProfile-dW-PsfLB.js +15 -0
  37. package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
  38. package/dist/dbCommand-CpdKLeQq.js +2 -0
  39. package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
  40. package/dist/dev-CoG-ZPx8.js +3 -0
  41. package/dist/devActivity-Dx_3nnGv.js +100 -0
  42. package/dist/devActivity.js +1 -1
  43. package/dist/dialectDriver-CgXnDfec.js +39 -0
  44. package/dist/discover-C9XKJDco.js +25 -0
  45. package/dist/doctorCommand-BvqGBwNG.js +2 -0
  46. package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
  47. package/dist/dormancyCommand-Dszo57d6.js +69 -0
  48. package/dist/e2eCmd-BRabZww-.js +147 -0
  49. package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
  50. package/dist/envCommand-DPgdV1Bq.js +60 -0
  51. package/dist/evalCommand-6RUfPen4.js +118 -0
  52. package/dist/evolveCommand-DHpkgjgH.js +281 -0
  53. package/dist/fileTaxonomy-CJfgOllU.js +457 -0
  54. package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
  55. package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
  56. package/dist/generateCommand-oibemh97.js +147 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.js +4 -3
  59. package/dist/infoCommand-BJw9nLUR.js +60 -0
  60. package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
  61. package/dist/inspect-C_T_WGvl.js +2 -0
  62. package/dist/inspectCmd-Bppy-GGw.js +224 -0
  63. package/dist/inspectFetch-Cm8_wVvp.js +151 -0
  64. package/dist/inspectMetrics-CfdKLh6t.js +72 -0
  65. package/dist/loadEnv-D9nEOClM.js +44 -0
  66. package/dist/logFileSink-C_D2wRN1.js +105 -0
  67. package/dist/logsCmd-CCca3KRZ.js +260 -0
  68. package/dist/manifestBuild-ChsKAhmn.js +2 -0
  69. package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
  70. package/dist/metaCommands-7MJfZ5cf.js +196 -0
  71. package/dist/migrate-CBwOt_iV.js +83 -0
  72. package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
  73. package/dist/newCommand-COWOJ1_E.js +156 -0
  74. package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
  75. package/dist/packageCommand-Cug_3Ogl.js +271 -0
  76. package/dist/pageConvention-cEiRxdab.js +5 -0
  77. package/dist/privacyCommand-C-Df56U_.js +146 -0
  78. package/dist/projectScaffold-DmzEKHib.js +2 -0
  79. package/dist/projectScaffold-LMMtaavR.js +814 -0
  80. package/dist/renderModeScan-D7J1B7Kw.js +105 -0
  81. package/dist/renderProfile-1OWWAAtx.js +81 -0
  82. package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
  83. package/dist/runtimeTrace-CRxalXTs.js +91 -0
  84. package/dist/scheduleCmd--jksTrf6.js +69 -0
  85. package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
  86. package/dist/schemaIr-UJybUUZW.js +103 -0
  87. package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
  88. package/dist/seedRunner-TFHHiToI.js +329 -0
  89. package/dist/serveCommand-B_isw7q4.js +1647 -0
  90. package/dist/serveCommand-DOvbgRnQ.js +2 -0
  91. package/dist/serveEntry.js +5 -5
  92. package/dist/serverlessCommand-CfJZy6dS.js +482 -0
  93. package/dist/start-9LiUOfES.js +1087 -0
  94. package/dist/start-B-9Nsp-S.js +3 -0
  95. package/dist/startEntry.js +2 -2
  96. package/dist/staticCommand-Dr2M6tpU.js +304 -0
  97. package/dist/storageCommand-Co6NfLqN.js +42 -0
  98. package/dist/templates-De8IR5-c.js +102 -0
  99. package/dist/test-CI6iDsYc.js +115 -0
  100. package/dist/tracesCmd-CkEZQrtt.js +232 -0
  101. package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
  102. package/dist/tsxLoader-EuXmSJ1K.js +51 -0
  103. package/dist/typecheckCommand-BlsWiCNq.js +61 -0
  104. package/dist/updateCommand-Bkptutss.js +585 -0
  105. package/dist/updateCommand-us1_hdIC.js +2 -0
  106. package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
  107. package/dist/webDev-Cg-fFiyd2.js +2 -0
  108. package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
  109. package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
  110. package/dist/webhooksCommand-CID96Rga.js +267 -0
  111. package/dist/workflowsCmd-D1VTmLMY.js +608 -0
  112. package/package.json +179 -17
  113. package/templates/AGENTS.core.md +58 -3
  114. package/templates/AGENTS.md +64 -7
  115. package/templates/agent-docs/_index.md +6 -4
  116. package/templates/agent-docs/_manifest.json +22 -5
  117. package/templates/agent-docs/ai.md +370 -0
  118. package/templates/agent-docs/authentication.md +265 -31
  119. package/templates/agent-docs/caching.md +6 -0
  120. package/templates/agent-docs/cli.md +794 -50
  121. package/templates/agent-docs/data.md +550 -11
  122. package/templates/agent-docs/database/migrations.md +174 -25
  123. package/templates/agent-docs/database/misc.md +156 -40
  124. package/templates/agent-docs/database/querying.md +19 -1
  125. package/templates/agent-docs/database/scaling.md +60 -0
  126. package/templates/agent-docs/database/schema.md +1 -0
  127. package/templates/agent-docs/database/seedsdialects.md +208 -19
  128. package/templates/agent-docs/database/transactions.md +68 -0
  129. package/templates/agent-docs/deployment.md +69 -3
  130. package/templates/agent-docs/introduction.md +87 -16
  131. package/templates/agent-docs/local-first-mobile.md +79 -4
  132. package/templates/agent-docs/multi-tenancy.md +95 -20
  133. package/templates/agent-docs/observability.md +58 -3
  134. package/templates/agent-docs/plugins/ai-flows.md +161 -2
  135. package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
  136. package/templates/agent-docs/plugins/audit.md +37 -1
  137. package/templates/agent-docs/plugins/auth-social.md +143 -0
  138. package/templates/agent-docs/plugins/auth-workos.md +4 -2
  139. package/templates/agent-docs/plugins/auth.md +131 -6
  140. package/templates/agent-docs/plugins/billing.md +132 -15
  141. package/templates/agent-docs/plugins/cdc-out.md +46 -7
  142. package/templates/agent-docs/plugins/clickhouse.md +1 -1
  143. package/templates/agent-docs/plugins/duckdb.md +1 -1
  144. package/templates/agent-docs/plugins/flags.md +132 -0
  145. package/templates/agent-docs/plugins/governance.md +105 -7
  146. package/templates/agent-docs/plugins/multitenancy.md +9 -4
  147. package/templates/agent-docs/plugins/presence.md +13 -2
  148. package/templates/agent-docs/plugins/ratelimit.md +9 -0
  149. package/templates/agent-docs/plugins/search.md +157 -6
  150. package/templates/agent-docs/plugins/sso-saml.md +47 -8
  151. package/templates/agent-docs/plugins/webhooks.md +105 -0
  152. package/templates/agent-docs/plugins.md +150 -18
  153. package/templates/agent-docs/reference.md +60 -3
  154. package/templates/agent-docs/releases.md +1117 -0
  155. package/templates/agent-docs/routing.md +43 -25
  156. package/templates/agent-docs/scheduling.md +10 -0
  157. package/templates/agent-docs/schema-driven-ui.md +92 -12
  158. package/templates/agent-docs/security.md +426 -0
  159. package/templates/agent-docs/templates/apibackends.md +87 -18
  160. package/templates/agent-docs/templates/appshells.md +32 -14
  161. package/templates/agent-docs/templates/overview.md +13 -8
  162. package/templates/agent-docs/testing.md +211 -14
  163. package/templates/agent-docs/whats-new.md +1700 -104
  164. package/templates/agent-docs/workflows.md +130 -14
  165. package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
  166. package/templates/apps/api-ai/package.json +8 -7
  167. package/templates/apps/api-auth/actions/me.action.ts +13 -0
  168. package/templates/apps/api-auth/package.json +9 -8
  169. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
  170. package/templates/apps/api-backend/package.json +12 -8
  171. package/templates/apps/api-backend/queries/notes.query.ts +30 -8
  172. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
  173. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
  174. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
  175. package/templates/apps/api-backend-deactivation/package.json +8 -7
  176. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
  177. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
  178. package/templates/apps/api-backend-mail/package.json +9 -8
  179. package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
  180. package/templates/apps/api-backend-mariadb/.env.example +14 -0
  181. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
  182. package/templates/apps/api-backend-mariadb/package.json +10 -9
  183. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
  184. package/templates/apps/api-backend-sqlite/.env.example +14 -0
  185. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
  186. package/templates/apps/api-backend-sqlite/package.json +9 -8
  187. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
  188. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
  189. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
  190. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
  191. package/templates/apps/api-backend-storage/package.json +9 -8
  192. package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
  193. package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
  194. package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
  195. package/templates/apps/api-cms/actions/me.action.ts +13 -0
  196. package/templates/apps/api-cms/app.config.ts +19 -0
  197. package/templates/apps/api-cms/authz.ts +63 -0
  198. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
  199. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
  200. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
  201. package/templates/apps/api-cms/package.json +11 -10
  202. package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
  203. package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
  204. package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
  205. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
  206. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
  207. package/templates/apps/api-collab/package.json +9 -8
  208. package/templates/apps/api-collab/queries/documents.query.ts +19 -7
  209. package/templates/apps/api-data-advanced/package.json +9 -8
  210. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
  211. package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
  212. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
  213. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
  214. package/templates/apps/api-durable/package.json +9 -8
  215. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
  216. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
  217. package/templates/apps/api-feature-flags/package.json +10 -9
  218. package/templates/apps/api-governance/README.md +30 -8
  219. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
  220. package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
  221. package/templates/apps/api-governance/database/schema.ts +16 -4
  222. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
  223. package/templates/apps/api-governance/package.json +9 -8
  224. package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
  225. package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
  226. package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
  227. package/templates/apps/api-kv/package.json +9 -8
  228. package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
  229. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
  230. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
  231. package/templates/apps/api-moderation/package.json +9 -8
  232. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
  233. package/templates/apps/api-observability/package.json +9 -8
  234. package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
  235. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
  236. package/templates/apps/api-ratelimit/package.json +9 -8
  237. package/templates/apps/api-rbac/package.json +9 -8
  238. package/templates/apps/api-rest/package.json +8 -7
  239. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
  240. package/templates/apps/api-saas/package.json +12 -11
  241. package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
  242. package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
  243. package/templates/apps/api-saas-starter/app.config.ts +19 -0
  244. package/templates/apps/api-saas-starter/authz.ts +75 -0
  245. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
  246. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
  247. package/templates/apps/api-saas-starter/package.json +15 -11
  248. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
  249. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
  250. package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
  251. package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
  252. package/templates/apps/api-search/package.json +9 -8
  253. package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
  254. package/templates/apps/api-status/README.md +10 -3
  255. package/templates/apps/api-status/app.config.ts +8 -3
  256. package/templates/apps/api-status/authz.ts +5 -3
  257. package/templates/apps/api-status/package.json +9 -8
  258. package/templates/apps/api-status/queries/components.list.query.ts +14 -6
  259. package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
  260. package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
  261. package/templates/apps/api-status/tests/status.test.ts +9 -1
  262. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
  263. package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
  264. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
  265. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
  266. package/templates/apps/api-versioning/package.json +9 -8
  267. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
  268. package/templates/apps/api-webhooks/package.json +10 -9
  269. package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
  270. package/templates/apps/changelog/package.json +8 -6
  271. package/templates/apps/edge-functions/package.json +2 -2
  272. package/templates/apps/frontend-admin/package.json +10 -8
  273. package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
  274. package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
  275. package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
  276. package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
  277. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
  278. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
  279. package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
  280. package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
  281. package/templates/apps/frontend-app/app.config.ts +4 -3
  282. package/templates/apps/frontend-app/package.json +11 -8
  283. package/templates/apps/frontend-app/src/lib/api.ts +25 -0
  284. package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
  285. package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
  286. package/templates/apps/frontend-auth/package.json +10 -8
  287. package/templates/apps/frontend-blank/package.json +9 -7
  288. package/templates/apps/frontend-cms/package.json +11 -9
  289. package/templates/apps/frontend-collab/package.json +12 -9
  290. package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
  291. package/templates/apps/frontend-contact/package.json +9 -7
  292. package/templates/apps/frontend-dashboard/package.json +9 -7
  293. package/templates/apps/frontend-docs/package.json +9 -7
  294. package/templates/apps/frontend-i18n/package.json +8 -6
  295. package/templates/apps/frontend-landing/package.json +9 -7
  296. package/templates/apps/frontend-portal/package.json +10 -8
  297. package/templates/apps/frontend-saas/app.config.ts +10 -6
  298. package/templates/apps/frontend-saas/package.json +10 -8
  299. package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
  300. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
  301. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
  302. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
  303. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
  304. package/templates/apps/frontend-spa/package.json +9 -7
  305. package/templates/apps/frontend-ssr/package.json +9 -7
  306. package/templates/apps/frontend-ssr-api/package.json +10 -8
  307. package/templates/apps/frontend-static-blog/package.json +8 -6
  308. package/templates/apps/frontend-status/package.json +10 -8
  309. package/templates/apps/mobile-app/README.md +1 -0
  310. package/templates/apps/mobile-app/package.json +4 -2
  311. package/templates/apps/mobile-app/src/app/index.tsx +22 -12
  312. package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
  313. package/templates/apps/mobile-app/src/lib/api.ts +34 -0
  314. package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
  315. package/templates/baselines/bare/.env.example +14 -0
  316. package/templates/baselines/bare/baseline.json +4 -4
  317. package/templates/baselines/compose/.env.example +14 -0
  318. package/templates/baselines/compose/README.md +1 -1
  319. package/templates/baselines/compose/baseline.json +5 -5
  320. package/templates/baselines/compose-mariadb/.env.example +14 -0
  321. package/templates/baselines/compose-mariadb/README.md +1 -1
  322. package/templates/baselines/compose-mariadb/baseline.json +5 -5
  323. package/templates/baselines/helm/.env.example +14 -0
  324. package/templates/baselines/helm/baseline.json +4 -4
  325. package/dist/apiBuild-C-x9YacA.js +0 -2
  326. package/dist/checkCommand-DRovTKza.js +0 -2
  327. package/dist/commands-CJfepbm4.js +0 -11541
  328. package/dist/dbCommand-b1gum4td.js +0 -2
  329. package/dist/dev-iiMtlkfs.js +0 -3
  330. package/dist/devActivity-BhIu6ncs.js +0 -159
  331. package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
  332. package/dist/inspect-mmBuRXmy.js +0 -2
  333. package/dist/manifestBuild-Dj8Jjoto.js +0 -2
  334. package/dist/seedRunner-Bqxgp7HZ.js +0 -230
  335. package/dist/serveCommand-DdaM4Hup.js +0 -1608
  336. package/dist/start-C0koT0UO.js +0 -1084
  337. /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
@@ -19,7 +19,7 @@ The framework ships some plugins; you write your own; the contract is small enou
19
19
 
20
20
  - [The plugin contract](/docs/plugins/contract) — `definePlugin`, lifecycle hooks, rpc interceptors, framework-version compatibility
21
21
  - [plugin-audit](/docs/plugins/audit) — mutation audit log + `audit()` mixin
22
- - [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
22
+ - [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + email verification + tenant invitations + user impersonation + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
23
23
  - [plugin-multitenancy](/docs/plugins/multitenancy) — `tenant()` schema mixin + `assertOwnTenant` write-guard + `TenantMismatch`
24
24
  - [plugin-soft-delete](/docs/plugins/soft-delete) — `softDelete()` schema mixin (hide on delete, `hardDelete()` bypass)
25
25
  - [plugin-rbac](/docs/plugins/rbac) — roles + permissions + the `permission()` guard
@@ -46,7 +46,8 @@ The framework ships some plugins; you write your own; the contract is small enou
46
46
  - [plugin-governance](/docs/plugins/governance) — data governance: retention TTL sweep, GDPR export + erasure, consent ledger, field encryption
47
47
  - [plugin-openapi](/docs/plugins/openapi) — OpenAPI 3.1 spec + Swagger-UI docs generated from your `defineRestRoute` descriptors and (opt-in) rpc procedures
48
48
  - [plugin-versioning](/docs/plugins/versioning) — full row history + time-travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions`); what-changed-to-what on every write
49
- - [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, cross-instance
49
+ - [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, held in memory; cross-instance with [plugin-broadcast](/docs/plugins/broadcast)
50
+ - [plugin-auth-social](/docs/plugins/auth-social) — first-party Sign in with Google / GitHub / Apple: mandatory PKCE + state, JWKS-verified ID tokens, a deliberate account-linking policy, sessions issued through plugin-auth
50
51
  - [plugin-scim](/docs/plugins/scim) — SCIM 2.0 provisioning (Users + Groups at `/scim/v2`) so an enterprise IdP can create/deactivate users
51
52
  - [plugin-sso-saml](/docs/plugins/sso-saml) — enterprise SAML 2.0 SSO: SP-initiated login + Single Logout, ACS, metadata (+ IdP-metadata-URL auto cert rotation, encrypted assertions, SP request signing); mints a framework session
52
53
  - [API keys](/docs/configuration/api-keys) — **first-class** (not a plugin): `apiKeys: true` enables Bearer-key auth + admin-gated issue/list/revoke
@@ -60,7 +61,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
60
61
  | Plugin | Status | What it does |
61
62
  |---|---|---|
62
63
  | `@voltro/plugin-audit` | ✓ | Mutation audit log + `audit()` mixin |
63
- | `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
64
+ | `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, email verification (off/soft/strict policy), tenant invitations (addressed, single-use, role chosen by the inviter), user impersonation (marked, time-bounded, escalation-proof), passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
64
65
  | `@voltro/plugin-multitenancy` | ✓ | `tenant()` schema mixin (read-scope + write-fill) + `assertOwnTenant` guard + typed `TenantMismatch` |
65
66
  | `@voltro/plugin-soft-delete` | ✓ | `softDelete()` schema mixin — `deletedAt` / `deletedBy`; `delete` → UPDATE, `hardDelete()` bypass |
66
67
  | `@voltro/plugin-rbac` | ✓ | Roles compile to scopes + the `permission()` handler guard + typed `ScopeError` |
@@ -71,6 +72,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
71
72
  | `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
72
73
  | `@voltro/plugin-broadcast` | ✓ | Cross-replica reactivity — fans out app-mutation change events to every replica over a pub/sub bus (Redis / NATS). Closes the single-instance gap for every non-postgres dialect. [→ details](/docs/plugins/broadcast) |
73
74
  | `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `defineEvent` (durable delivery workflow, HMAC signing, retries, filters). [→ details](/docs/plugins/webhooks) |
75
+ | `@voltro/plugin-auth-social` | ✓ | First-party social login — Sign in with Google / GitHub / Apple with no identity vendor: authorize URL + code exchange + JWKS-verified ID tokens, mandatory PKCE (S256) and `state`, an explicit account-linking policy (`never` by default), Apple's signed-JWT client secret / one-time name / private-relay email all handled; sessions via `issueUserSession`. [→ details](/docs/plugins/auth-social) |
74
76
  | `@voltro/plugin-auth-{workos,kinde,clerk,auth0,supabase,oidc}` | ✓ | Six IdP adapters over the shared `jwtBearerStrategy` — JWKS verify + claims→tenant mapping; WorkOS additionally ships hosted-login OAuth primitives (`workosAuthorizationUrl` / `workosAuthenticateWithCode`) for a redirect-based SSO login flow. [→ details](/docs/authentication/external-idp) |
75
77
  | `@voltro/plugin-analytics-postgres` | ✓ | First-party lite — events on the main DataStore, cross-dialect (postgres / mysql / mariadb / mssql / sqlite / turso). [→ details](/docs/plugins/analytics#voltroplugin-analytics-postgres) |
76
78
  | `@voltro/plugin-duckdb` | ✓ | Embedded DuckDB sidecar — real OLAP performance, no external service. [→ details](/docs/plugins/analytics#voltroplugin-duckdb) |
@@ -91,7 +93,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
91
93
  | `@voltro/plugin-governance` | ✓ | Data governance — retention TTL sweep (delete / anonymise), GDPR subject export + erasure (admin-gated routes + `GovernanceService`), consent ledger, field encryption. [→ details](/docs/plugins/governance) |
92
94
  | `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
93
95
  | `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
94
- | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator, swept `_voltro_presence` table, cross-instance. [→ details](/docs/plugins/presence) |
96
+ | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator. Held **in memory**, owner-partitioned — no table is written; cross-instance requires [`@voltro/plugin-broadcast`](/docs/plugins/broadcast), and without a broker each replica sees only its own clients. [→ details](/docs/plugins/presence) |
95
97
  | `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
96
98
  | `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
97
99
 
@@ -126,7 +128,7 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
126
128
  | `extendSchema` | Contribute tables + custom SQL migrations (tracked in `_voltro_plugin_migrations`). |
127
129
  | `services` | Provide an Effect `Layer` whose Tags every handler can `yield*` (e.g. `MailService`, `StorageService`). |
128
130
  | `routes` | Register plugin-owned rpc queries / mutations / actions (alias-prefixed tags). |
129
- | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. |
131
+ | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation), and `remoteAddr`, the client address already resolved through `security.trustedProxies` (use it instead of `x-forwarded-for`). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. A state-changing route is [origin-checked](/docs/security/overview#cross-site-requests-are-refused) unless it declares `originGuard: 'exempt'`. |
130
132
  | `inspectEndpoints` | Mount tooling under `/_voltro/inspect/plugins/<alias>/…`. |
131
133
  | `onScheduleFire` / `onWorkflowStep` / `onHttpRequest` | Wrap every cron firing, every workflow `step()`, every pre-auth HTTP request. |
132
134
  | `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
@@ -207,10 +209,15 @@ To REPLACE one deliberately, declare it:
207
209
 
208
210
  ```ts
209
211
  // Adopt the plugin's namespace, add your own leaves beside it…
210
- defineQuery({ name: 'notifications.archive', … })
211
-
212
- // …and REPLACE just the one you need to behave differently.
213
- defineMutation({ name: 'notifications.markRead', overridesPlugin: true, })
212
+ defineQuery({ name: 'notifications.archive', guards: [{ scope: 'notifications:read' }], … })
213
+
214
+ // …and REPLACE just the one you need to behave differently. Your replacement is
215
+ // YOUR procedure, so it needs its own access decision — the plugin's does not
216
+ // carry over with the tag.
217
+ defineMutation({
218
+ name: 'notifications.markRead', overridesPlugin: true,
219
+ guards: [{ scope: 'notifications:write' }], …
220
+ })
214
221
  ```
215
222
 
216
223
  The plugin's route is dropped, not merely permitted alongside yours — permitting
@@ -345,6 +352,8 @@ onChangeEvent: (event: PluginChangeEvent) => Effect.Effect<void, unknown>
345
352
  // PluginChangeEvent = {
346
353
  // table, op: 'insert'|'update'|'delete', new: Row | null, old: Row | null,
347
354
  // origin?: 'inline' | 'injected', changeScope: 'local' | 'fleet',
355
+ // traceId?, subjectId?, procedure?,
356
+ // oversized?: 'rehydrated' | 'tombstone' | 'unrecovered',
348
357
  // }
349
358
  ```
350
359
 
@@ -359,6 +368,8 @@ onChangeEvent: (event) =>
359
368
  )
360
369
  ```
361
370
 
371
+ **Check `event.oversized` before you treat an image as a snapshot.** It is absent on an ordinary event, and set when the transport could not carry the row and the images were reconstructed — a row over postgres' 8000-byte NOTIFY cap. `'rehydrated'` means `new` is the row re-read from the database (correct to index or forward, but the row as it is NOW rather than the image at commit); `'tombstone'` means a delete whose `old` is the primary key and nothing else (enough to REMOVE the row, never a record of what it held); `'unrecovered'` means both images are null and the content is gone. A tap that stores history must not write a tombstone as a snapshot. Full guarantee: [postgres — oversized rows](/docs/database/dialects/postgres).
372
+
362
373
  Runs under BOTH `voltro dev` and `voltro serve` (the prod serve path fans out the same way). Requires the `'store:changes:read'` permission. It is NOT durable at the framework layer — a crash between commit and the fork loses the event; build durability INSIDE the Effect (insert into an outbox and retry against the typed error channel, the way `@voltro/plugin-cdc-out` does). Exactly-once / change-scope semantics are unchanged: read `event.origin` + `event.changeScope` inside the Effect to act once per change fleet-wide (skip `origin: 'injected'` on `'local'` scope; elect one worker on `'fleet'`). Used by `@voltro/plugin-search` to mirror rows into an external index. For in-transaction reactions use a mutation; for best-effort per-table reactions in app code prefer a `*.subscribe.ts` — `onChangeEvent` is the plugin-level equivalent.
363
374
 
364
375
  ### `onInstall` / `onUninstall` / `onActivate` / `onDeactivate` lifecycle
@@ -523,9 +534,8 @@ export const auditPlugin = (): VoltroPlugin =>
523
534
  })
524
535
  ```
525
536
 
526
- Tag derivation: `<plugin-alias>.<query.name>` unless `query.name` contains
527
- a dot (escape hatch). Plugin alias strips `@scope/` + the `plugin-` prefix
528
- and kebab→camelCase:
537
+ Tag derivation: `<plugin-alias>.<query.name>`. The plugin alias strips
538
+ `@scope/` + the `plugin-` prefix and kebab→camelCase:
529
539
 
530
540
  | Plugin name | Alias |
531
541
  |---|---|
@@ -535,6 +545,71 @@ and kebab→camelCase:
535
545
  | `plain-name` | `plainName` |
536
546
  | `@voltro/audit` | `audit` (no `plugin-` to strip) |
537
547
 
548
+ A `query.name` that already contains a dot is handled by whether it names the
549
+ plugin's OWN namespace:
550
+
551
+ - `'notifications.inbox'` on a plugin whose canonical name is
552
+ `@voltro/plugin-notifications` is re-namespaced — under
553
+ `alias: 'inbox'` it becomes `inbox.inbox`, not `notifications.inbox`.
554
+ A deeper path keeps its depth: `'audit.admin.events'` under `alias: 'trail'`
555
+ becomes `trail.admin.events`.
556
+ - `'acme.legacyBridge'` — a namespace that is not the plugin's own — passes
557
+ through untouched. That is the escape hatch, and it is the only case that
558
+ still bypasses the alias.
559
+
560
+ The re-namespacing needs the plugin to declare `baseName` (its canonical name,
561
+ before any app-supplied `alias`); a plugin that omits it keeps the older
562
+ behaviour where any dotted name passes through.
563
+
564
+ ### Naming a plugin: `alias` vs `name`
565
+
566
+ Two different app-side problems land on a plugin's name, so first-party plugins
567
+ that carry tables or routes accept two separate options:
568
+
569
+ | Option | Question it answers | Effect |
570
+ |---|---|---|
571
+ | `alias` | "your namespace collides with mine" | replaces the namespace — tags become `<alias>.<route>`, the inspect mount becomes `/_voltro/inspect/plugins/<alias>/…` |
572
+ | `name` | "I want two of these" | appends a `#suffix` discriminator so two installs never register the same tag |
573
+
574
+ ```ts
575
+ notificationsPlugin({ alias: 'alerts' }) // alerts.inbox
576
+ notificationsPlugin({ name: 'ops' }) // notifications#ops.inbox
577
+ notificationsPlugin({ alias: 'alerts', name: 'ops' }) // alerts#ops.inbox
578
+ ```
579
+
580
+ `alias` exists to escape a tag collision, which is fatal at codegen. Two costs
581
+ are worth knowing before you reach for it:
582
+
583
+ - the local and cloud dashboards fetch a plugin's inspect panel at its DEFAULT
584
+ slug, so an aliased plugin keeps serving its inspect endpoints while its
585
+ dashboard panel stops resolving;
586
+ - the plugin-migration ledger key is `<plugin-alias>__<migration.id>`, so
587
+ aliasing a plugin that ships `extendSchema.migrations` makes its already-applied
588
+ migrations look unapplied. Choose the alias before first boot, not after.
589
+
590
+ ### `tables: false` — keeping your own tables
591
+
592
+ Plugins whose tables carry no authorization or safety decision accept
593
+ `tables: false`, which stops them contributing DDL through `extendSchema` so an
594
+ app can keep equivalent tables it already has. Everything else — routes,
595
+ inspect, interceptors — is unchanged.
596
+
597
+ ```ts
598
+ notificationsPlugin({ tables: false }) // you declare the six notification tables
599
+ ```
600
+
601
+ It is offered on `@voltro/plugin-rbac`, `@voltro/plugin-audit`,
602
+ `@voltro/plugin-notifications` and `@voltro/plugin-ai-flows`. The plugin still
603
+ writes to those tables BY NAME, so you take over declaring each one with the
604
+ shape the package exports, and a missing or mis-shaped table fails at the first
605
+ write rather than at boot.
606
+
607
+ It is deliberately NOT offered on plugins whose tables carry a guarantee — the
608
+ SAML assertion replay cache, SCIM provisioning state, billing's usage counters,
609
+ cdc-out's delivery outbox, the governance consent ledger, search's tenant-scoped
610
+ index rows. A `tables: false` there would disable a security or correctness
611
+ decision with no signal to the app that it now owns it.
612
+
538
613
  Boot fails with a clear error on tag collisions (between two plugins, or
539
614
  with a user-authored tag).
540
615
 
@@ -770,7 +845,7 @@ markers. The plugin sees the api name + the list of discovered user-query
770
845
  rpc tags so it can emit per-rpc bindings. Returning `null` contributes
771
846
  nothing.
772
847
 
773
- ### `templates: PluginTemplate[]` — ship `voltro init` templates
848
+ ### `templates: PluginTemplate[]` — ship scaffolding templates
774
849
 
775
850
  ```ts
776
851
  definePlugin({
@@ -819,8 +894,14 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
819
894
  name: '@vendor/plugin-rate-limit',
820
895
  permissions: ['http:intercept'],
821
896
  onHttpRequest: async (next, ctx) => {
822
- const remoteAddr = ctx.headers['x-forwarded-for'] ?? ctx.remoteAddr ?? 'unknown'
823
- if (buckets.consume(remoteAddr, opts.perMinute) === 'exhausted') {
897
+ // `ctx.remoteAddr` is ALREADY resolved through the app's
898
+ // `security.trustedProxies` policy the same value the framework's own
899
+ // rate limiter, geo-block and audit rows use. Never read
900
+ // `ctx.headers['x-forwarded-for']`: it is a request header, so any
901
+ // caller can write it, and a limiter keyed on it is bypassed by one
902
+ // extra header. `undefined` only when the socket address is unavailable.
903
+ const clientAddr = ctx.remoteAddr ?? 'unknown'
904
+ if (buckets.consume(clientAddr, opts.perMinute) === 'exhausted') {
824
905
  return {
825
906
  status: 429,
826
907
  headers: { 'retry-after': '60', 'content-type': 'application/json' },
@@ -832,10 +913,28 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
832
913
  })
833
914
  ```
834
915
 
916
+ **`ctx.remoteAddr` is the client address — `x-forwarded-for` is not.**
917
+ The framework resolves `remoteAddr` through the app's
918
+ `security.trustedProxies` policy (`VOLTRO_TRUSTED_PROXIES`) before it hands you
919
+ the context: with no trusted proxy declared the header is ignored entirely and
920
+ the socket address wins, and with one declared only the hops that are actually
921
+ a configured proxy are believed. Reading `ctx.headers['x-forwarded-for']`
922
+ yourself throws that away and keys your limiter on a string the caller typed —
923
+ one extra header and every request looks like a new client. The same rule
924
+ applies to a plugin's raw HTTP routes, where the resolved value arrives as
925
+ `req.remoteAddr`; see [Trusted proxies](/docs/security/overview#the-same-address-reaches-your-plugin-routes).
926
+
835
927
  A per-plugin `plugin.<name>.http-intercept` metric is auto-emitted so
836
928
  the dashboard's Plugins panel surfaces HTTP-intercept latency next to
837
929
  RPC-intercept latency.
838
930
 
931
+ **Runs on both boot paths.** The chain is composed and installed identically by
932
+ `voltro dev` and `voltro serve` — this is a production capability, and for a
933
+ pre-auth shield production is the point. There is exactly ONE exemption, and it
934
+ is deliberate: `GET /internal/liveness` and `GET /internal/readiness` are
935
+ answered before the interceptor, so a rate-limit or geo-block plugin cannot 503
936
+ a Kubernetes probe and take the replica out of rotation.
937
+
839
938
  ### `extendSchema: { tables, migrations }` — contribute schema + migrations
840
939
 
841
940
  A plugin contributes BOTH declarative table descriptors AND custom SQL
@@ -848,6 +947,14 @@ The ledger key is `<plugin-alias>__<migration.id>` so two plugins can
848
947
  each ship `'001-init'` without collision. Failure aborts boot;
849
948
  re-runs are no-ops.
850
949
 
950
+ **Which commands run them:** `voltro dev`'s boot auto-migrate, `voltro db apply`
951
+ (bare and `--plan`) and `voltro migrate --create-only`. NOT `voltro serve` —
952
+ serve never applies a schema, so a plugin's steps land in the pre-deploy job
953
+ alongside the schema, which is where they belong. Until 0.34.0 only the `voltro
954
+ dev` boot ran them, so a plugin's SQL steps executed on every developer machine
955
+ and on no deployed database; if you ship migrations, verify against a deployed
956
+ database rather than a dev boot.
957
+
851
958
  ```ts
852
959
  import { Effect, Schema } from 'effect'
853
960
  import { definePlugin } from '@voltro/protocol'
@@ -1387,17 +1494,42 @@ analytics: duckdbAnalytics({
1387
1494
  }),
1388
1495
  ```
1389
1496
 
1390
- The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data }` — `id` is the source row's primary key, `data` is the full row as JSON. Analytical queries JOIN events against the mirror:
1497
+ The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data, version, is_deleted }` — `id` is the source row's primary key, `data` is the full row as JSON, `version` orders the writes (see below) and `is_deleted` marks a tombstone. Analytical queries JOIN events against the mirror and filter tombstones out:
1391
1498
 
1392
1499
  ```sql
1393
1500
  -- DuckDB: events per user tier
1394
1501
  SELECT json_extract_string(m.data, '$.tier'), COUNT(*)
1395
1502
  FROM voltro_events e
1396
- JOIN voltro_mirror_users m ON m.id = e.subject_id
1503
+ JOIN voltro_mirror_users m ON m.id = e.subject_id AND m.is_deleted = false
1397
1504
  GROUP BY 1
1398
1505
  ```
1399
1506
 
1400
- The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes remove by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite. Per-change failures are isolated into the log channel: the OLTP write that produced the change already committed, so a warehouse hiccup never surfaces to the request. On DuckDB the mirror table is a plain `(id, data)` table; on ClickHouse it's a `ReplacingMergeTree(version)` so re-inserts collapse to the latest version on merge (deletes write a `is_deleted = 1` tombstone — filter `is_deleted = 0` or use `FINAL`).
1507
+ The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes write a tombstone by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite.
1508
+
1509
+ ### Delivery guarantee
1510
+
1511
+ **At-least-once for the lifetime of the process, ordered per row.** Read that sentence literally — every clause is a promise the framework keeps, and the sentence stops where the implementation does:
1512
+
1513
+ - **Retried, not dropped.** A failing mirror write is retried with exponential backoff (`retryAttempts`, default 5). A write that outlives its retries is queued for **repair**: a timer re-reads the row's *current* state from your database and re-applies it, so the mirror converges on the truth rather than on a stale change that happened to be in flight.
1514
+ - **Ordered per row.** Writes for the same primary key are applied one at a time, in commit order, and every write carries a **version** stamped when the change left the store — never a clock read inside the warehouse client. A change that arrives late therefore *loses*: ClickHouse's `ReplacingMergeTree(version)` keeps the highest version, and the DuckDB / postgres mirrors apply the update only when the incoming version is newer. Different rows are still mirrored concurrently.
1515
+ - **A delete is a tombstone, not a row removal.** Filter `is_deleted = false` (`is_deleted = 0` / `FINAL` on ClickHouse). A physical delete would leave nothing for a late, stale insert of the same key to lose against — the row would silently come back.
1516
+ - **Never surfaces to the request.** The OLTP write that produced the change has already committed; a warehouse outage is isolated into the log channel and the metrics below.
1517
+ - **A graceful shutdown is not a crash.** On SIGINT / SIGTERM the mirror stops taking new changes and then **settles** what is already queued and in flight, before the sink itself is disposed. That drain is bounded (3 s of the teardown budget you set with `VOLTRO_SHUTDOWN_GRACE_MS`, default 10 s): a warehouse that has stopped answering cannot hold the process open until the orchestrator's SIGKILL, which would lose strictly more. The two outcomes log differently — `analytics mirror drained` at info, or a `warn` naming what was still pending when the deadline cut it, because that is the moment those counters can still be read.
1518
+ - **Not durable across a crash.** The repair queue lives in memory. A change still awaiting repair when the process dies — SIGKILL, an OOM, a host failure — is lost, as is one evicted after `repairQueueLimit`. Both are logged at error level and counted by `voltro_analytics_mirror_dropped_total` — if that counter is non-zero, the affected tables need a re-seed.
1519
+
1520
+ Metrics: `voltro_analytics_mirror_forwarded_total`, `..._retries_total`, `..._repair_queued_total`, `..._dropped_total`.
1521
+
1522
+ ### Tuning
1523
+
1524
+ Every number the mirror picks on your behalf has a default and an environment override:
1525
+
1526
+ | Env var | Default | Meaning |
1527
+ |---|---|---|
1528
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_ATTEMPTS` | `5` | Total attempts per mirror write (`1` = no retry). |
1529
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_BASE_MS` | `100` | First backoff delay; doubles per attempt. |
1530
+ | `VOLTRO_ANALYTICS_MIRROR_RETRY_MAX_MS` | `30000` | Ceiling for the doubling backoff. |
1531
+ | `VOLTRO_ANALYTICS_MIRROR_REPAIR_INTERVAL_MS` | `60000` | How often the repair loop re-drives exhausted changes (`0` disables it). |
1532
+ | `VOLTRO_ANALYTICS_MIRROR_REPAIR_QUEUE_LIMIT` | `10000` | Maximum keys held for repair before the oldest is dropped and counted. |
1401
1533
 
1402
1534
  ## Default — no sink configured
1403
1535
 
@@ -13,10 +13,13 @@ _The client-side hook surface, grouped by purpose._
13
13
 
14
14
  The web side of a Voltro app talks to API apps through React hooks. Data hooks are keyed by API name and RPC tag; routing hooks are provided by `@voltro/web`.
15
15
 
16
+ App code should reach for the **typed binding** rather than the tag-taking hooks directly: [`createHooks(apiName)`](/docs/reference/hooks-data) takes the api's generated `AppProcedures` map and returns `useSubscription` / `useMutation` / `useAction` whose tag is a literal union and whose input and output types are inferred — so a typo'd tag is a compile error and the result needs no annotation. Bind it once per api in `src/lib/api.ts`. The tag-taking forms documented below are the primitive underneath, for code that only learns the tag at runtime.
17
+
16
18
  ## Data Hooks
17
19
 
18
20
  | Hook | Purpose |
19
21
  |---|---|
22
+ | [`createHooks`](/docs/reference/hooks-data) | Bind the typed hook surface for one api — the recommended app-facing entry point. |
20
23
  | [`useSubscription`](/docs/reference/hooks-data) | Subscribe to a reactive query (`*.query.ts`). |
21
24
  | [`useMutation`](/docs/reference/hooks-data) | Run an atomic write (`*.mutation.ts`). |
22
25
  | [`useAction`](/docs/reference/hooks-data) | Run a unary non-transactional action (`*.action.ts`). |
@@ -112,6 +115,51 @@ _useSubscription, useMutation, useAction, useWorkflow, workflow status hooks, us
112
115
 
113
116
  Data hooks from `@voltro/client` are keyed by **api name** plus **RPC tag**. The tags come from descriptors discovered in the API app.
114
117
 
118
+ ## `createHooks(apiName)` — the typed hook binding
119
+
120
+ `createHooks` turns the api's generated procedure map into hooks whose **RPC tag is a literal union** and whose **input and output types are inferred**. It is the recommended way to call an api from app code.
121
+
122
+ Codegen emits `AppProcedures` into the api's `rpcGroup.generated.ts` — a type-level map of every tag (yours and every plugin's) to the descriptor behind it. Bind it once, at module scope, next to the rest of your api glue:
123
+
124
+ ```ts
125
+ // src/lib/api.ts — one file, one line, once per app
126
+ import { createHooks } from '@voltro/client'
127
+ import type { AppProcedures } from '@app/api/rpcGroup'
128
+
129
+ export const { useSubscription, useMutation, useAction } = createHooks<AppProcedures>('app')
130
+ ```
131
+
132
+ `'app'` is the key this web app gave the api in `app.config.ts` → `apis`. It is now spelled exactly once per app instead of once per call site:
133
+
134
+ ```tsx
135
+ import { useSubscription, useMutation } from '../lib/api'
136
+
137
+ const { data } = useSubscription('notes.list') // ReadonlyArray<Note>, inferred
138
+ const create = useMutation('notes.create') // input + output inferred
139
+ ```
140
+
141
+ What the compiler catches that it could not before:
142
+
143
+ | Mistake | Before | Now |
144
+ |---|---|---|
145
+ | Typo in the tag (`'notes.lst'`) | Runtime dev-console error | Compile error |
146
+ | Wrong hook for the kind (a mutation passed to `useSubscription`) | Runtime `console.error` | Compile error |
147
+ | Missing a required input field | Request sent with `undefined` | Compile error |
148
+ | Wrongly-shaped input | Server-side decode failure | Compile error |
149
+ | Result type annotation disagreeing with the server | Never detected | Impossible — there is no annotation |
150
+
151
+ The result keeps every narrowing rule of the untyped hook: `loading` still discriminates `data`, `fallback` / `initialSnapshot` still remove the branch, and only a **dynamic** `skip` adds the `idle` state.
152
+
153
+ Row types include the auto-optimistic marker the client adds, so `row.optimistic` type-checks on a live list without a hand-written row mirror.
154
+
155
+ **Destructure the result** — do not export the object and call `api.useSubscription(...)`. `react-hooks/rules-of-hooks` only recognises a member call as a hook when the object is PascalCase, so a lowercase namespace silently switches off rules-of-hooks and exhaustive-deps at every call site.
156
+
157
+ `import type` is erased at build time, so the binding adds nothing to the browser bundle.
158
+
159
+ **`rpcGroup.generated.ts` is written by codegen**, so a tree that has never booted does not have it yet and `tsc` reports `Cannot find module '@app/api/rpcGroup'`. `voltro dev` generates it on boot; run `voltro codegen` once for a fresh clone or a CI job that only typechecks. The scaffolded api templates do this in their own `typecheck` script, and `pnpm -r` runs the api before anything that depends on it.
160
+
161
+ The tag-taking hooks below are the primitive underneath. Reach for them when the tag is only known at runtime — plugin web bindings and libraries shipped against an unknown app — not in app code.
162
+
115
163
  ## `useSubscription(apiName, rpcTag, input?, options?)`
116
164
 
117
165
  Subscribes to a reactive `defineQuery` RPC.
@@ -1101,13 +1149,18 @@ If the work is "do X once and finish", it's a seed. If it's "keep X running unti
1101
1149
  <!-- source: en/reference/templates.md -->
1102
1150
  ## App templates
1103
1151
 
1104
- _The scaffolding catalogue — every api / web / serverless template, what it demonstrates, and when to pick it._
1152
+ _The scaffolding catalogue — the api / web / serverless / mobile templates, what each demonstrates, and when to pick it._
1105
1153
 
1106
1154
  Every template is a dogfooded, runnable reference. Scaffold one with
1107
1155
  `voltro create-project --api <id> --web <id>` (or `voltro add-app <name>
1108
- --template <id>`), and run `voltro list-templates` for the live list.
1156
+ --template <id>`).
1157
+
1158
+ **`voltro list-templates` is the authority, not this page.** It prints the
1159
+ templates your installed CLI actually ships — there are dozens — while the
1160
+ tables below cover the ones worth a paragraph of explanation. If an id appears
1161
+ in the command's output and not here, it exists and works.
1109
1162
 
1110
- Templates come in **three kinds**, matching the three things you deploy:
1163
+ Templates come in **four kinds**, matching the four things you deploy:
1111
1164
 
1112
1165
  - **`api`** — a long-running backend (`app.config` `type: 'api'`).
1113
1166
  - **`web`** — a frontend (`type: 'web'`); gets a dev-server port.
@@ -1115,6 +1168,10 @@ Templates come in **three kinds**, matching the three things you deploy:
1115
1168
  functions](/docs/deployment/serverless-functions) shipped on their own with
1116
1169
  `voltro serverless`. No long-running server, no port — add it to a project
1117
1170
  with `voltro add-app`.
1171
+ - **`mobile`** — an Expo (React Native) app (`mobile-app`), scaffolded with
1172
+ `--mobile`. Expo owns its own dev loop, so it is not part of `voltro dev`; the
1173
+ sibling api still is, and the app consumes it over the network with the same
1174
+ typed hooks the web app uses.
1118
1175
 
1119
1176
  ## API backends (`kind: api`)
1120
1177