@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
@@ -43,6 +43,8 @@ No actions, workflows, agents, or `AGENTS.md` ship — this template is delibera
43
43
 
44
44
  Every query / mutation / action is **two files paired by basename**: a browser-safe descriptor (`*.query.ts` / `*.mutation.ts`) holding `defineQuery` / `defineMutation`, and a server-only executor (`*.query.server.ts` / `*.mutation.server.ts`) with a default export. The framework pairs them at boot. The descriptor file must never import `node:*` / database / runtime modules — it's what the web client pulls through codegen.
45
45
 
46
+ **Every descriptor also declares an access decision** — `guards: [{ scope }]` or `openAccess: '<why>'`, or the app refuses to boot (`security.defaultDeny`). Both procedures here are `openAccess` with the reason in the file, and the reason is a real claim rather than the word "demo": `tenant()` confines every delivery to the request's tenant, but this template configures no auth strategy, so that tenant comes from the caller's own `x-tenant` header — it shapes the result, it does not authorize the caller. A `guards: [{ scope }]` would be *unsatisfiable* here: with no auth strategy and no rbac, every caller resolves to an anonymous Subject holding no scopes, so the guard would deny 100% of traffic — an outage, not security. Add an identity (see [`api-auth`](/docs/templates/api-auth) / [`api-rbac`](/docs/templates/api-rbac)), then swap the line for a `guards:` in the same change. **Your own new procedure files need one of these three from the first save.**
47
+
46
48
  ### Query — `queries/notes.query.ts` + `.server.ts`
47
49
 
48
50
  ```ts
@@ -52,6 +54,10 @@ import { Schema } from 'effect'
52
54
 
53
55
  export const listNotes = defineQuery({
54
56
  name: 'notes.list',
57
+ openAccess:
58
+ 'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
59
+ + 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
60
+ + 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
55
61
  input: Schema.Struct({}),
56
62
  output: Schema.Struct({
57
63
  id: Schema.String, title: Schema.String, body: Schema.String,
@@ -86,6 +92,10 @@ import { Schema } from 'effect'
86
92
 
87
93
  export const createNote = defineMutation({
88
94
  name: 'notes.create',
95
+ openAccess:
96
+ 'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
97
+ + '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
98
+ + 'there is no identity a scope guard could name.',
89
99
  target: { table: 'notes', op: 'insert',
90
100
  shape: (input: { tenantId: string; title: string; body: string }) => ({
91
101
  title: input.title, body: input.body, done: false,
@@ -465,8 +475,10 @@ S3_ACCESS_KEY_ID=minioadmin
465
475
  S3_SECRET_ACCESS_KEY=minioadmin
466
476
  S3_FORCE_PATH_STYLE=1
467
477
 
468
- # Auth — HMAC key for signed sessions (REQUIRED in production)
469
- VOLTRO_SESSION_SECRET=dev-only-change-me
478
+ # Auth — the HMAC key for signed sessions is NOT set here. `voltro dev` mints
479
+ # a unique per-project value into a gitignored `.env.local` on first boot
480
+ # (`envVar.secret({ generate: 'base64url' })`). In production the variable
481
+ # must come from your secret store; a missing one is a boot failure on purpose.
470
482
 
471
483
  # Cluster (K8s, >1 replica) — advertise a routable host so a workflow
472
484
  # can resume on another pod. Inject POD_IP via the downward API in prod.
@@ -606,6 +618,10 @@ import { Schema } from 'effect'
606
618
 
607
619
  export const placeOrder = defineMutation({
608
620
  name: 'orders.place',
621
+ openAccess:
622
+ 'inserts an order built only from caller-supplied fields into the caller\'s own tenant '
623
+ + '(`assertOwnTenant` rejects a mismatch); reads nothing and modifies no existing row. '
624
+ + 'No auth strategy ships here, so there is no identity a scope guard could name.',
609
625
  // Auto-optimistic: every query whose `source: 'orders'` matches gets a
610
626
  // placeholder row prepended; the server delta replaces it on commit.
611
627
  target: {
@@ -809,6 +825,11 @@ import { Schema } from 'effect'
809
825
 
810
826
  export const approveOrder = defineMutation({
811
827
  name: 'orders.approve',
828
+ openAccess:
829
+ 'injects the approval signal into the caller\'s own tenant\'s parked fulfilment run — the '
830
+ + 'executionId is derived from `{ orderId, tenantId }` and `assertOwnTenant` rejects a '
831
+ + 'mismatch, so no other tenant\'s run is addressable. It still decides whether an order '
832
+ + 'ships: the first procedure here to grow an `orders:approve` guard once an identity exists.',
812
833
  input: Schema.Struct({
813
834
  orderId: Schema.String,
814
835
  tenantId: Schema.String,
@@ -1059,7 +1080,7 @@ apps/acme/api/ # dir named by the app, not the template
1059
1080
  │ └── searchDocs.tool.tsx # retrieval tool — nearestNeighbours over docs
1060
1081
  ├── actions/
1061
1082
  │ ├── summarize.action.ts # descriptor — browser-safe
1062
- │ └── summarize.action.server.tsx # executor — generateObject structured output
1083
+ │ └── summarize.action.server.ts # executor — generateObject structured output
1063
1084
  └── seeds/
1064
1085
  └── docs.seed.ts # boot seed of sample docs (auto-embedded on insert)
1065
1086
  ```
@@ -1192,7 +1213,7 @@ export default async (
1192
1213
 
1193
1214
  Because `docs` carries `vectorEmbedding()`, the STRING overload of `nearestNeighbours(query, k)` is valid — the runtime embeds `query` before searching. Any failure (no AI key, embed error) is caught and returns `[]`, so a tool hiccup surfaces to the model as "no results" instead of aborting the whole agent turn.
1194
1215
 
1195
- ## The structured-output action — `actions/summarize.action.ts` + `.server.tsx`
1216
+ ## The structured-output action — `actions/summarize.action.ts` + `.server.ts`
1196
1217
 
1197
1218
  AI inference is external I/O, so it's an **action**, not a mutation. This one uses `generateObject` to take free-text and produce STRUCTURED output — a typed `{ title, summary, keyPoints, sentiment }` object instead of prose. The `output` Schema is exported from the descriptor so the executor can reuse the SAME schema to constrain the model: one source of truth for both the wire output and the shape the model must satisfy.
1198
1219
 
@@ -1214,6 +1235,9 @@ export type SummaryResult = Schema.Schema.Type<typeof SummaryResult>
1214
1235
 
1215
1236
  export const summarize = defineAction({
1216
1237
  name: 'support.summarize',
1238
+ openAccess:
1239
+ 'summarises caller-supplied text; reads no table and no other caller\'s data. '
1240
+ + 'Metered by the AI provider, so rate-limit and guard it before it is public.',
1217
1241
  input: Schema.Struct({
1218
1242
  // The raw text to summarise (a support thread, a doc, a transcript).
1219
1243
  text: Schema.String,
@@ -1223,7 +1247,7 @@ export const summarize = defineAction({
1223
1247
  ```
1224
1248
 
1225
1249
  ```ts
1226
- // summarize.action.server.tsx — EXECUTOR (server-only)
1250
+ // summarize.action.server.ts — EXECUTOR (server-only)
1227
1251
  import { Effect } from 'effect'
1228
1252
  import { generateObject } from '@voltro/ai'
1229
1253
  import { SummaryResult } from './summarize.action'
@@ -1538,6 +1562,10 @@ const Book = Schema.Struct({
1538
1562
  export const authorsWithBooks = defineQuery({
1539
1563
  name: 'authors.withBooks',
1540
1564
  source: ['authors', 'books'],
1565
+ openAccess:
1566
+ 'streams the seeded demo catalogue (authors + their books) for the request\'s tenant; the '
1567
+ + 'seed holds no personal data. `bio` is `.encrypted()` and still crosses the wire on '
1568
+ + 'purpose — encryption at rest is not an exposure marker; `.serverOnly()` is.',
1541
1569
  input: Schema.Struct({}),
1542
1570
  output: Schema.Struct({
1543
1571
  id: Schema.String, name: Schema.String,
@@ -1631,14 +1659,17 @@ export default {
1631
1659
 
1632
1660
  **Where the key actually comes from is the load-bearing detail.** `governancePlugin({ fieldEncryption: true })` resolves the AES-256-GCM key through the **Secrets-Resolver** — i.e. the process environment — NOT through a `defineEnv` `default`. A `defineEnv` default would satisfy the boot validation gate while the cipher still failed to resolve a key. That's why the env declaration above deliberately has no `default`, and the template instead ships a `.env` carrying a **DEV-ONLY** placeholder so `voltro dev` boots out of the box:
1633
1661
 
1634
- ```bash
1635
- # .env DEV-ONLY. `voltro dev` loads this into process.env at boot, BEFORE
1636
- # the env-validation gate and BEFORE plugins activate, which is how the
1637
- # governance plugin's cipher resolves its key from the Secrets-Resolver.
1638
- VOLTRO_FIELD_ENCRYPTION_KEY=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff
1662
+ ```ts
1663
+ // The key is OURS to invent, so declare it as minted. `voltro dev` writes a
1664
+ // unique per-project value into a gitignored `.env.local` before the env gate
1665
+ // runs so the cipher resolves a key that exists nowhere else, and no key
1666
+ // value is ever committed or shipped.
1667
+ VOLTRO_FIELD_ENCRYPTION_KEY: envVar.secret({ generate: 'hex' }),
1639
1668
  ```
1640
1669
 
1641
- > **Before production**, replace this placeholder with a real key (`openssl rand -hex 32`) and move it into your deployment's secret store — never ship the shipped value. **Lose the key → lose the data** (GCM fails closed, never silent corruption); **rotating it** makes every existing `.encrypted()` value unreadable.
1670
+ > **Minting is dev-only.** `serve` / `build` / `start` have no such step: in production a missing key must remain a boot failure, and the value belongs in your deployment's secret store. **Lose the key → lose the data** (GCM fails closed, never silent corruption); **rotating it** makes every existing `.encrypted()` value unreadable.
1671
+ >
1672
+ > Never write a key value into a shipped file — not even an obviously-fake one. A placeholder in a template is a signing key published to everyone who downloads it, and a 64-hex placeholder passes every length check and appears on no blocklist.
1642
1673
 
1643
1674
  See [Field encryption](/docs/plugins/governance) and the [columns reference](/docs/database/columns).
1644
1675
 
@@ -1654,6 +1685,10 @@ import { Schema } from 'effect'
1654
1685
  export const searchBooks = defineQuery({
1655
1686
  name: 'books.search',
1656
1687
  source: 'books',
1688
+ openAccess:
1689
+ 'full-text search over the seeded demo book catalogue, tenant-scoped; the seed holds no '
1690
+ + 'personal data. The result cache is `scope: \'subject\'`, so an open read still cannot '
1691
+ + 'serve one caller a snapshot built for another.',
1657
1692
  input: Schema.Struct({ q: Schema.String }),
1658
1693
  output: Schema.Struct({
1659
1694
  id: Schema.String, authorId: Schema.String, title: Schema.String,
@@ -2182,7 +2217,7 @@ expect(row.tenantId).toBe('acme') // tenant() stamped it — the REAL store, i
2182
2217
  voltro test
2183
2218
  ```
2184
2219
 
2185
- The package also ships `MockClock` / `MockEmail` / `mockAi` / `makeWorkflowRunner` and `runDialectParity` (the `@voltro/testing/dialect` subpath).
2220
+ The package also ships `MockClock` / `mockAi` / `makeTestApp` / `defineFactory` / `makeWorkflowRunner` and `runDialectParity` (the `@voltro/testing/dialect` subpath). Mail is asserted through the mail plugin's own memory provider, not a mock — see [Unit testing → plugin services](/docs/testing/unit-testing#plugin-services-mail-storage-your-own).
2186
2221
 
2187
2222
  ## Going to production
2188
2223
 
@@ -2403,6 +2438,8 @@ curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json
2403
2438
 
2404
2439
  The plugin synthesizes the `search.query` rpc — input `{ index, q, limit?, filters? }`, output the matching docs. Because the index spec set `tenantField: 'tenantId'`, the query auto-filters to the caller's tenant: a request resolved to tenant `acme` only ever sees `acme` docs, even though every tenant's docs share one index. Searching the same term as a different tenant returns nothing.
2405
2440
 
2441
+ **`search.query` is a SEPARATE surface with its own access decision, made by the plugin.** The boot gate that requires `guards:` / `openAccess:` / `internal: true` reads only the procedures *you* wrote — a plugin's are the plugin author's call and you cannot edit them. So the two files this template ships, `queries/articles.list.query.ts` and `mutations/articles.create.mutation.ts`, each carry their own `openAccess:` with the reason in the file (the rows are the template's own seeded demo articles; no auth strategy ships here, so a scope guard would deny every caller). Anything you add beside them needs one of the three from the first save, or the app will not boot. See [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).
2442
+
2406
2443
  ## Backfill existing rows
2407
2444
 
2408
2445
  The change tap covers writes AFTER boot. `startup/searchBackfill.startup.tsx` seeds the index from rows already in the table — the demo seed's articles, or every row after a restart or a switch to a new engine. It uses the same `searchBackend` instance, so the docs it writes are exactly what `search.query` reads:
@@ -2429,15 +2466,28 @@ const { results, run, pending } = useSearch('articles')
2429
2466
 
2430
2467
  The `/web` subpath imports nothing server-only — it's just a query over the `search.query` rpc, browser-safe.
2431
2468
 
2432
- ## Going to production — one line
2469
+ ## Going to production — one line, and it is not optional
2433
2470
 
2434
- `memoryBackend()` is in-process: great for dev, single-instance only. For real deployments swap it in `lib/search.ts` for a vendor engine — the indexing + query + backfill code is identical:
2471
+ `memoryBackend()` is in-process: great for dev, and **under
2472
+ `NODE_ENV=production` it refuses to boot** (`SearchBackendNotDurable`). This
2473
+ template ships it because it needs zero infrastructure to run locally — it is
2474
+ not a deployable default, and a deploy that keeps it does not start. Swap it in
2475
+ `lib/search.ts` for a vendor engine; the indexing + query + backfill code is
2476
+ identical:
2435
2477
 
2436
2478
  ```ts
2437
2479
  import { typesenseBackend } from '@voltro/plugin-search' // or meilisearch / algolia
2438
2480
  export const searchBackend = typesenseBackend({ url: process.env.TYPESENSE_URL!, apiKey: process.env.TYPESENSE_KEY! })
2439
2481
  ```
2440
2482
 
2483
+ **Single-instance is not the exception it sounds like.** The heap-resident index
2484
+ is empty after every restart and deploy, so a genuinely one-process app searches
2485
+ nothing until something re-seeds it. If that is your deployment and a
2486
+ `*.startup.tsx` really does call `backfillIndex` for every index, declare it —
2487
+ `searchPlugin({ singleProcessMemoryIndex: true, indexes })` — and the plugin
2488
+ will hold you to both halves, warning if a peer replica ever announces itself.
2489
+ Full reasoning: [the memory backend refuses to boot in production](/docs/plugins/search#the-memory-backend-refuses-to-boot-in-production).
2490
+
2441
2491
  ## When to use api-search vs. the other backends
2442
2492
 
2443
2493
  | You want… | Pick |
@@ -2981,7 +3031,7 @@ Compose the full lifecycle: `users.with(audit(), softDelete(), deactivation())`.
2981
3031
 
2982
3032
  _Data governance in one plugin with @voltro/plugin-governance — AES-256-GCM field encryption for .encrypted() columns (handler sees plaintext, ciphertext at rest), admin-gated GDPR governance.export/erase over declared subjectScopes, a consent ledger, and retention TTL sweeps. Memory store, zero infra._
2983
3033
 
2984
- Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **field encryption** (AES-256-GCM for `.encrypted()` columns — handlers see plaintext, the column holds `enc:v1:…` at rest), **GDPR** export/erase (admin-gated, walking declared subject scopes), a **consent ledger**, and **retention** sweeps (delete/anonymise rows past a TTL). The template boots zero-infra with a dev encryption key. Template id: **`api-governance`**.
3034
+ Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **field encryption** (AES-256-GCM for `.encrypted()` columns — handlers see plaintext, the column holds `enc:v1:…` at rest), **GDPR** export/erase (admin-gated, walking declared subject scopes), a **consent ledger**, and **retention** sweeps (delete/anonymise rows past a TTL). The template boots zero-infra on a key `voltro dev` mints for your project — no key ships with it. Template id: **`api-governance`**.
2985
3035
 
2986
3036
  ## Scaffold
2987
3037
 
@@ -2989,7 +3039,7 @@ Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **
2989
3039
  voltro create-project acme --api=api-governance
2990
3040
  ```
2991
3041
 
2992
- The shipped `.env` carries a DEV `VOLTRO_FIELD_ENCRYPTION_KEY`. Generate your own (`openssl rand -hex 32`) and keep it in a secret store for production.
3042
+ **No encryption key ships with this template** — a key committed to a template is a key published to everyone who downloads it. `VOLTRO_FIELD_ENCRYPTION_KEY` is declared with `generate: 'hex'`, so `voltro dev` mints a unique one into a gitignored `.env.local` on first boot. Your deployment mints its own: `voltro secret generate field-encryption`.
2993
3043
 
2994
3044
  ## Field encryption
2995
3045
 
@@ -2997,7 +3047,8 @@ The shipped `.env` carries a DEV `VOLTRO_FIELD_ENCRYPTION_KEY`. Generate your ow
2997
3047
  // database/schema.ts
2998
3048
  export const profiles = table('profiles', {
2999
3049
  id: id(), name: text(), email: text(),
3000
- ssn: text().encrypted(), // AES-256-GCM at rest
3050
+ // TWO markers, two different questions — say both when you mean both.
3051
+ ssn: text().encrypted().serverOnly(), // encrypted AT REST · and may never leave the server
3001
3052
  }).with(audit())
3002
3053
 
3003
3054
  // app.config.ts
@@ -3006,12 +3057,16 @@ governancePlugin({ fieldEncryption: true /* reads VOLTRO_FIELD_ENCRYPTION_KEY */
3006
3057
 
3007
3058
  You pass plaintext; the store middleware encrypts on write and decrypts on read — handlers never touch the ciphertext. Boot **fails loud** if an `.encrypted()` column exists but no cipher is registered.
3008
3059
 
3060
+ **`.encrypted()` is not an exposure marker**, and reading it as one is a tempting category error: the runtime decrypts for the handler, so an encrypted column reaches a client exactly like any other unless it is *also* `.serverOnly()`. That is why the column above carries both, and why `profiles.get` returns the last four digits it derived server-side — proof the cipher ran, without publishing the value.
3061
+
3009
3062
  ```bash
3010
3063
  ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
3011
3064
  -d '{"tag":"profiles.create","input":{"name":"Ada","email":"ada@acme.com","ssn":"123-45-6789"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
3012
3065
  curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
3013
3066
  -d "{\"tag\":\"profiles.get\",\"input\":{\"id\":\"$ID\"}}"
3014
- # → { ok:true, result:{ …, ssn:"123-45-6789" } } (plaintext — the column stored enc:v1:…)
3067
+ # → { ok:true, result:{ …, ssnLast4:"6789" } }
3068
+ # The handler read "123-45-6789" as plaintext (the cipher decrypted it) and
3069
+ # published four digits, because the column is also `.serverOnly()`.
3015
3070
  ```
3016
3071
 
3017
3072
  ## GDPR, consent, retention
@@ -3032,6 +3087,8 @@ governancePlugin({
3032
3087
 
3033
3088
  - **Don't encrypt what you filter on.** An `.encrypted()` column is ciphertext on disk — no `WHERE` / `ORDER BY` in SQL. Encrypt fields you read back WHOLE (PII, tokens, notes).
3034
3089
  - **The key is everything.** GCM fails closed on a bad key — you get an error, never silent corruption. Back the key up; rotating it means re-encrypting.
3090
+ - **Three orthogonal markers, three questions.** `.encrypted()` = encrypted at rest · `.serverOnly()` = may it leave the server at all · `.sensitive()`/`.safe()` = may it appear in a `voltro data export`. Using one to answer another's question is the mistake.
3091
+ - **Every procedure declares an access decision.** `guards: [{ scope }]` or `openAccess: '<why>'`, or the app refuses to boot (`security.defaultDeny`). Both procedures here are `openAccess` with the reason in the file: the template configures no auth strategy and no rbac, so every caller resolves to an anonymous Subject holding no scopes and a scope guard would deny 100% of traffic — an outage, not security. Add an identity (see [`api-auth`](/docs/templates/api-auth) / [`api-rbac`](/docs/templates/api-rbac)), then the guard.
3035
3092
  - **GDPR endpoints are admin-only by design.** Don't remove the `admin:full` gate — an unauthenticated export/erase is a data-exfiltration / griefing hole.
3036
3093
 
3037
3094
 
@@ -3132,6 +3189,10 @@ import { Schema } from 'effect'
3132
3189
 
3133
3190
  export const syncPull = defineAction({
3134
3191
  name: 'sync.pull',
3192
+ openAccess:
3193
+ 'advances the request tenant\'s own sync watermark; every KV key is namespaced by that '
3194
+ + 'tenant and the rows land in a `tenant()` table. The upstream is a local pure generator '
3195
+ + '— no network call, no credential.',
3135
3196
  input: Schema.Struct({
3136
3197
  limit: Schema.optional(Schema.Int.pipe(Schema.greaterThan(0))),
3137
3198
  }),
@@ -3378,6 +3439,10 @@ import { Schema } from 'effect'
3378
3439
  export const setDocumentBody = defineMutation({
3379
3440
  name: 'documents.setBody',
3380
3441
  target: { table: 'documents', op: 'update' },
3442
+ openAccess:
3443
+ 'merges a caller-supplied CRDT update into a document of the request\'s tenant — a merge, '
3444
+ + 'not an overwrite, so no concurrent editor\'s text is lost, and an id outside the tenant '
3445
+ + 'matches no row. Guard who may edit once the app has an identity.',
3381
3446
  input: Schema.Struct({
3382
3447
  id: Schema.NonEmptyString,
3383
3448
  update: Schema.Uint8ArrayFromBase64,
@@ -3424,6 +3489,10 @@ import { Schema } from 'effect'
3424
3489
 
3425
3490
  export const listDocuments = defineQuery({
3426
3491
  name: 'documents.list',
3492
+ openAccess:
3493
+ 'streams the documents of the request\'s tenant, CRDT body included (`tenant()` scopes '
3494
+ + 'every delivery). No auth strategy ships here, so the tenant comes from the caller\'s own '
3495
+ + '`x-tenant` header — add a strategy, then a `guards:`.',
3427
3496
  input: Schema.Struct({}),
3428
3497
  output: Schema.Struct({
3429
3498
  id: Schema.String,
@@ -1986,37 +1986,53 @@ import { useCapabilityManifest, deriveEntityAdmins } from '@voltro/client'
1986
1986
 
1987
1987
  const { manifest } = useCapabilityManifest('app') // one-shot fetch of the capability map
1988
1988
  const entities = manifest ? deriveEntityAdmins(manifest) : []
1989
- // each entity: { table, columns, listTag?, createTag?, updateTag?, deleteTag?, createScope, writeScope, deleteScope }
1989
+ // each entity: { table, columns, serverOnlyColumns, sensitiveColumns, reactive,
1990
+ // pkColumn?, editable, list, create, update, delete }
1991
+ // each of list/create/update/delete: { tag?, guards? }
1990
1992
  ```
1991
1993
 
1992
- `deriveEntityAdmins` joins each user table to the procedures that actually serve it — the query whose `source` is the table (→ the list `<DataTable>`), the mutations whose `target` is `{table, op}` (→ create/edit/delete). So the admin binds only to procedures that **exist**; it never guesses tags by naming convention.
1994
+ `deriveEntityAdmins` joins each user table to the procedures that actually serve it — the query whose `source` is the table (→ the list `<DataTable>`), the mutations whose `target` is `{table, op}` (→ create/edit/delete) — and carries each one's **declared access**. So the admin binds only to procedures that **exist**, and gates only on permissions the api really declares; it never guesses either by naming convention.
1993
1995
 
1994
1996
  ## Per-entity CRUD
1995
1997
 
1996
1998
  ```tsx
1997
1999
  // src/pages/admin/[entity]/page.tsx (abridged)
1998
- const canCreate = useCan(spec.createScope) // <table>:create
1999
- {spec.createTag && canCreate ? (
2000
- <AutoForm api="app" mutation={spec.createTag} submitLabel={`Add ${spec.table}`} />
2000
+ const create = useAccessDecision(spec.create.guards) // the procedure's OWN guards
2001
+ {spec.create.tag && create !== 'denied' ? (
2002
+ <AutoForm api="app" mutation={spec.create.tag} submitLabel={`Add ${spec.table}`} />
2001
2003
  ) : null}
2002
2004
 
2003
- {spec.listTag ? (
2004
- <DataTable api="app" query={spec.listTag} rowActions={rowActions} />
2005
+ {spec.list.tag ? (
2006
+ <DataTable api="app" query={spec.list.tag} rowActions={rowActions} />
2005
2007
  ) : null}
2006
2008
  ```
2007
2009
 
2008
- The list is a **live subscription** — a create from the form (or anyone, in another tab) appears without a refetch. Each row's actions include a delete (gated by `useCan(spec.deleteScope)`, run via `useMutation(spec.deleteTag)`) and a **provenance** drawer (`useProvenance`) answering "why is this row here?".
2010
+ The list is a **live subscription** — a create from the form (or anyone, in another tab) appears without a refetch. Each row's actions include a delete (keyed on `spec.pkColumn`, gated by `useAccessDecision(spec.delete.guards)`, run via `useMutation(spec.delete.tag)`) and a **provenance** drawer (`useProvenance`) answering "why is this row here?".
2009
2011
 
2010
- ## Permission gating
2012
+ ## Permission gating — from the api's own declaration
2011
2013
 
2012
- Write affordances are hidden via `useCan` on conventional per-entity scopes (`<table>:create`, `<table>:delete`). `<PermissionProvider>` in the admin layout feeds the current subject's scopes; the demo seeds `admin:full` (bypass) — **swap it for your session's real scopes**:
2014
+ Each action carries `guards`: the procedure's `guards:` / `openAccess:` declaration, straight off the capability map. `useAccessDecision` judges it against the scopes `<PermissionProvider>` supplies, so the UI gate and the server check read the **same data** and cannot drift. The demo seeds `admin:full` (bypass) — **swap it for your session's real scopes**:
2013
2015
 
2014
2016
  ```tsx
2015
2017
  const { data } = useSubscription<{ scopes: string[] }>('app', 'auth.session')
2016
2018
  <PermissionProvider scopes={data?.scopes ?? []}>…</PermissionProvider>
2017
2019
  ```
2018
2020
 
2019
- This is UX gating, not enforcement the api's own `permission()` guards remain the real authorization boundary. Row-level field visibility (vs. action-level) is a future step.
2021
+ **The decision is three-valued.** `allowed` and `denied` are what they sound like; `unknown` means only the server can answer — a guard carrying a `resource` extractor is checked per **row**, and a browser holding global scopes cannot pre-compute it. The admin **shows** those controls and lets the api reply with a typed `ScopeError`. Hiding them would empty the back-office for exactly the multi-tenant apps whose subjects are minted with no global scopes at all.
2022
+
2023
+ This is still UX gating, not enforcement — the api's own guards remain the real authorization boundary.
2024
+
2025
+ ## The three exposure axes
2026
+
2027
+ The columns come from your schema, and the admin honours each marker differently — they are orthogonal, and substituting one for another leaks or hides data:
2028
+
2029
+ | Marker | Admin behaviour |
2030
+ |---|---|
2031
+ | `.serverOnly()` | **Never rendered and never submitted.** It is absent from `spec.columns`; the page names it from `serverOnlyColumns` so the omission is visible rather than silent. The runtime refuses it as mutation input anyway (`assertNoServerOnlyInput`) — this is the same rule one layer earlier. |
2032
+ | `.encrypted()` | **Shown and editable.** At-rest encryption is not wire exposure; your procedures read it decrypted. |
2033
+ | `.sensitive(class)` | **Shown**, and listed in `sensitiveColumns` so a bulk export masks it. |
2034
+
2035
+ Rows are keyed on `spec.pkColumn`, not a hard-coded `id`. A table with no single primary key reports `editable: false` and renders list-only.
2020
2036
 
2021
2037
  ## Undo / redo
2022
2038
 
@@ -2057,9 +2073,11 @@ Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts
2057
2073
 
2058
2074
  ## Anti-patterns
2059
2075
 
2060
- - **Shipping the demo `admin:full` scopes to production.** That bypasses every `useCan` gate. Feed the subject's real scopes to `<PermissionProvider>`.
2061
- - **Treating `useCan` as authorization.** It hides buttons; the server's `permission()` guard is the real gate. A hidden action is still callable over rpc by a crafted client.
2062
- - **Assuming a naming convention.** The admin binds to discovered tags, not `<table>.create`-style guesses so it works even when your procedures are named differently.
2076
+ - **Shipping the demo `admin:full` scopes to production.** That bypasses every gate. Feed the subject's real scopes to `<PermissionProvider>`.
2077
+ - **Treating the UI decision as authorization.** It hides buttons; the server's guards are the real gate. A hidden action is still callable over rpc by a crafted client.
2078
+ - **Assuming a naming convention — for tags *or* for scopes.** The admin binds to discovered tags and gates on discovered guards. A UI that invents `<table>:create` hides every write from every app that named its scope anything else: a total outage of the affordance that looks like a working permission check, and that a demo seeding `admin:full` can never surface.
2079
+ - **Collapsing `unknown` into `denied`.** That is the same outage by another route — it is the answer for every per-resource guard, which is the whole multi-tenant case.
2080
+ - **Hiding a column because it is `.encrypted()`.** Encryption-at-rest is not wire exposure. `.serverOnly()` is the only one of the three axes that means "do not render this".
2063
2081
 
2064
2082
 
2065
2083
 
@@ -1,6 +1,6 @@
1
1
  # Templates
2
2
 
3
- > Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call.
3
+ > Dozens of dogfooded starter templates ship with the framework — backend shapes, frontend shapes, a serverless function library, and an Expo mobile app. voltro list-templates is the authority; scaffold any of them with one CLI call.
4
4
 
5
5
 
6
6
 
@@ -9,11 +9,16 @@
9
9
  <!-- source: en/templates/overview.md -->
10
10
  ## Overview
11
11
 
12
- _Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call._
12
+ _Dozens of dogfooded starter templates ship with the framework — backend shapes, frontend shapes, a serverless function library, and an Expo mobile app. voltro list-templates is the authority; scaffold any of them with one CLI call._
13
13
 
14
- Voltro ships **thirty-eight** starter templates across the **four kinds** you actually deploy: **twenty-two** backends (`api-*`), **fourteen** frontends (`frontend-*` plus `changelog`), **one** serverless library (`edge-functions`), and **one** Expo mobile app (`mobile-app`). Each is a small, opinionated, dogfooded reference you scaffold once and then own — no update channel, no lock-in.
14
+ Voltro ships **dozens** of starter templates across the **four kinds** you actually deploy: backends (`api-*`), frontends (`frontend-*` plus `changelog`), a serverless library (`edge-functions`), and an Expo mobile app (`mobile-app`). Each is a small, opinionated, dogfooded reference you scaffold once and then own — no update channel, no lock-in.
15
15
 
16
- The CLI loads templates by walking `voltro-templates/apps/<id>/`; the directory name IS the template id you pass to `--api` / `--web` / `--template`. Run `voltro list-templates` to see them live.
16
+ **`voltro list-templates` is the authority, not this page.** It prints the
17
+ templates your installed CLI actually ships; the tables below cover the ones
18
+ worth a paragraph of explanation, and several ids exist that have no entry here.
19
+ If an id appears in the command's output and not below, it exists and works.
20
+
21
+ The CLI loads templates by walking `voltro-templates/apps/<id>/`; the directory name IS the template id you pass to `--api` / `--web` / `--template`.
17
22
 
18
23
  ```bash
19
24
  # A project with a backend + a blank frontend:
@@ -27,7 +32,7 @@ Each app gets its own port from the project's `portRange`, wired through `pnpm d
27
32
 
28
33
  ## Backend templates (`--api=<id>`)
29
34
 
30
- The four `api-backend*` shapes share the same minimal `notes` domain and differ only in the plugin/store wired on top. The other three are **feature showcases** — a cohesive domain that exercises one whole slice of the framework end to end.
35
+ The `api-backend*` shapes share the same minimal `notes` domain and differ only in the plugin/store wired on top. The rest are **feature showcases** — each a cohesive domain that exercises one whole slice of the framework end to end.
31
36
 
32
37
  | Template | Best for |
33
38
  |---|---|
@@ -54,11 +59,11 @@ The four `api-backend*` shapes share the same minimal `notes` domain and differ
54
59
  | [`api-governance`](/docs/templates/api-governance) | **Field encryption + GDPR + consent + retention** — `@voltro/plugin-governance`: AES-256-GCM `.encrypted()` columns (plaintext to handlers, ciphertext at rest), admin-gated `governance.export`/`erase`, a consent ledger, retention sweeps. |
55
60
  | [`api-collab`](/docs/templates/api-collab) | **Local-first / CRDT collaborative editing** — a `documents` table with a `crdtText()` `body`; concurrent edits from many clients CONVERGE via an authoritative server-side merge on the write path (no last-write-wins loser), then broadcast over the reactive engine. Zero-infra; pairs with `frontend-collab`. |
56
61
 
57
- All twenty-two are `kind: 'api'`.
62
+ Every one of these is `kind: 'api'` — and `voltro list-templates` lists more `api-*` ids than the table above covers.
58
63
 
59
64
  ## Frontend templates (`--web=<id>`)
60
65
 
61
- Fourteen web shapes spanning every render mode — `static`, `spa`, `ssr`, `isr`, selective `islands` hydration — plus the reactive fullstack loop (and its local-first / CRDT variant).
66
+ Web shapes spanning every render mode — `static`, `spa`, `ssr`, `isr`, selective `islands` hydration — plus the reactive fullstack loop (and its local-first / CRDT variant).
62
67
 
63
68
  **Every frontend template is bilingual (en/de) out of the box**, with the i18n strategy matched to its render mode: the hydrated/SSR shells use cookie i18n (`<LocaleSwitcher>`), the static/islands sites use URL-prefix i18n (`/de/…`, one pre-rendered HTML file per locale). `frontend-i18n` remains the dedicated URL-prefix reference. See [URL strategies](/docs/i18n/url-strategies) for why render mode decides.
64
69
 
@@ -79,7 +84,7 @@ Fourteen web shapes spanning every render mode — `static`, `spa`, `ssr`, `isr`
79
84
  | [`frontend-ssr-api`](/docs/templates/ssr-api) | `ssr` · fed by api | **SSR from your backend** — an `ssr` loader calls `query('notes.list', {})` over the sibling api's POST /rpc, so real rows are in the first paint + `<title>`; `useSubscription` upgrades to live. Pairs with `api-backend`. |
80
85
  | [`frontend-contact`](/docs/templates/contact) | `static` + serverless | **Static page + a serverless email form** — the headline "static frontend, serverless backend" combo. The page ships to a CDN; the function scales to zero. |
81
86
 
82
- All fourteen are `kind: 'web'`. The CLI rejects an api template passed to `--web` (and vice-versa) on a kind mismatch.
87
+ Every one of these is `kind: 'web'`, and again there are more `web` ids than rows here. The CLI rejects an api template passed to `--web` (and vice-versa) on a kind mismatch.
83
88
 
84
89
  ## Serverless template (`--template=<id>` via `add-app`)
85
90