@voltro/cli 0.28.0 → 0.30.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 (354) hide show
  1. package/CHANGELOG.md +655 -0
  2. package/THIRD-PARTY-NOTICES.md +90 -4
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
  4. package/dist/apiBuild-CxvR1Dwt.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BMr-X48w.js +2427 -0
  7. package/dist/checkCommand-BOk9y264.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
  10. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
  11. package/dist/dbCommand-D8nhyppR.js +2 -0
  12. package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
  13. package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
  16. package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1 -1
  19. package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
  20. package/dist/inspect-mmBuRXmy.js +2 -0
  21. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
  22. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
  23. package/dist/manifestBuild-Dj8Jjoto.js +2 -0
  24. package/dist/sdkgen-CqpOq_fy.js +582 -0
  25. package/dist/serveCommand-Ciq_wIY8.js +1577 -0
  26. package/dist/serveEntry.js +2 -2
  27. package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
  28. package/dist/startEntry.js +2 -2
  29. package/package.json +17 -17
  30. package/templates/AGENTS.core.md +50 -0
  31. package/templates/AGENTS.md +54 -2
  32. package/templates/agent-docs/_index.md +4 -2
  33. package/templates/agent-docs/_manifest.json +26 -5
  34. package/templates/agent-docs/ai.md +191 -0
  35. package/templates/agent-docs/cli.md +143 -6
  36. package/templates/agent-docs/configuration.md +32 -0
  37. package/templates/agent-docs/data.md +282 -0
  38. package/templates/agent-docs/database/migrations.md +47 -0
  39. package/templates/agent-docs/database/misc.md +57 -0
  40. package/templates/agent-docs/database/schema.md +3 -1
  41. package/templates/agent-docs/internationalization.md +169 -8
  42. package/templates/agent-docs/local-first-mobile.md +426 -0
  43. package/templates/agent-docs/routing.md +94 -3
  44. package/templates/agent-docs/schema-driven-ui.md +12 -0
  45. package/templates/agent-docs/security.md +17 -0
  46. package/templates/agent-docs/templates/apibackends.md +210 -1
  47. package/templates/agent-docs/templates/appshells.md +144 -1
  48. package/templates/agent-docs/templates/mobile.md +60 -0
  49. package/templates/agent-docs/templates/overview.md +17 -7
  50. package/templates/agent-docs/whats-new.md +238 -158
  51. package/templates/agent-docs/workflows.md +376 -0
  52. package/templates/apps/api-ai/package.json +7 -7
  53. package/templates/apps/api-auth/package.json +8 -8
  54. package/templates/apps/api-backend/package.json +7 -7
  55. package/templates/apps/api-backend-deactivation/package.json +7 -7
  56. package/templates/apps/api-backend-mail/package.json +8 -8
  57. package/templates/apps/api-backend-mariadb/package.json +9 -9
  58. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  59. package/templates/apps/api-backend-sqlite/README.md +38 -0
  60. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  61. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  62. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  63. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  64. package/templates/apps/api-backend-sqlite/package.json +30 -0
  65. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  66. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  67. package/templates/apps/api-backend-sqlite/template.json +6 -0
  68. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  69. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  70. package/templates/apps/api-backend-storage/package.json +8 -8
  71. package/templates/apps/api-cms/README.md +104 -0
  72. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  73. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  74. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  75. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  76. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  77. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  78. package/templates/apps/api-cms/app.config.ts +61 -0
  79. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  80. package/templates/apps/api-cms/content/index.ts +18 -0
  81. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  82. package/templates/apps/api-cms/database/schema.ts +64 -0
  83. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  84. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  85. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  86. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  87. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  88. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  89. package/templates/apps/api-cms/package.json +32 -0
  90. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  91. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  92. package/templates/apps/api-cms/template.json +6 -0
  93. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  94. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  95. package/templates/apps/api-cms/tsconfig.json +5 -0
  96. package/templates/apps/api-collab/README.md +70 -0
  97. package/templates/apps/api-collab/app.config.ts +28 -0
  98. package/templates/apps/api-collab/database/schema.ts +63 -0
  99. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
  100. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
  101. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
  102. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
  103. package/templates/apps/api-collab/package.json +30 -0
  104. package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
  105. package/templates/apps/api-collab/queries/documents.query.ts +23 -0
  106. package/templates/apps/api-collab/template.json +6 -0
  107. package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
  108. package/templates/apps/api-collab/tsconfig.json +5 -0
  109. package/templates/apps/api-data-advanced/package.json +8 -8
  110. package/templates/apps/api-durable/package.json +8 -8
  111. package/templates/apps/api-feature-flags/package.json +9 -9
  112. package/templates/apps/api-governance/package.json +8 -8
  113. package/templates/apps/api-kv/package.json +8 -8
  114. package/templates/apps/api-moderation/package.json +8 -8
  115. package/templates/apps/api-observability/package.json +8 -8
  116. package/templates/apps/api-ratelimit/package.json +8 -8
  117. package/templates/apps/api-rbac/package.json +8 -8
  118. package/templates/apps/api-rest/package.json +7 -7
  119. package/templates/apps/api-saas/package.json +11 -11
  120. package/templates/apps/api-saas-starter/README.md +103 -0
  121. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  122. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  123. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  124. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  125. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  126. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  127. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  128. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  129. package/templates/apps/api-saas-starter/package.json +32 -0
  130. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  131. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  132. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  133. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  134. package/templates/apps/api-saas-starter/template.json +6 -0
  135. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  136. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  137. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  138. package/templates/apps/api-search/package.json +8 -8
  139. package/templates/apps/api-status/README.md +88 -0
  140. package/templates/apps/api-status/app.config.ts +36 -0
  141. package/templates/apps/api-status/authz.ts +33 -0
  142. package/templates/apps/api-status/database/schema.ts +70 -0
  143. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  144. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  145. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  146. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  147. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  148. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  149. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  150. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  151. package/templates/apps/api-status/package.json +30 -0
  152. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  153. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  154. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  155. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  156. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  157. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  158. package/templates/apps/api-status/template.json +6 -0
  159. package/templates/apps/api-status/tests/status.test.ts +70 -0
  160. package/templates/apps/api-status/tsconfig.json +5 -0
  161. package/templates/apps/api-versioning/package.json +8 -8
  162. package/templates/apps/api-webhooks/package.json +9 -9
  163. package/templates/apps/changelog/package.json +6 -6
  164. package/templates/apps/edge-functions/package.json +2 -2
  165. package/templates/apps/frontend-admin/app.config.ts +1 -1
  166. package/templates/apps/frontend-admin/package.json +8 -8
  167. package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
  168. package/templates/apps/frontend-app/app.config.ts +1 -1
  169. package/templates/apps/frontend-app/package.json +8 -8
  170. package/templates/apps/frontend-app/src/locales/index.ts +1 -1
  171. package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
  172. package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
  173. package/templates/apps/frontend-auth/README.md +78 -0
  174. package/templates/apps/frontend-auth/app.config.ts +34 -0
  175. package/templates/apps/frontend-auth/package.json +32 -0
  176. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  177. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  178. package/templates/apps/frontend-auth/src/config.ts +11 -0
  179. package/templates/apps/frontend-auth/src/globals.css +105 -0
  180. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  181. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  182. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  183. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  184. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  185. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  186. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  187. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  188. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  189. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  190. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  191. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  192. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  193. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  194. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  195. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  196. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  197. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  198. package/templates/apps/frontend-auth/template.json +6 -0
  199. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  200. package/templates/apps/frontend-blank/app.config.ts +1 -1
  201. package/templates/apps/frontend-blank/package.json +7 -7
  202. package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
  203. package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
  204. package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
  205. package/templates/apps/frontend-cms/README.md +47 -0
  206. package/templates/apps/frontend-cms/app.config.ts +37 -0
  207. package/templates/apps/frontend-cms/package.json +33 -0
  208. package/templates/apps/frontend-cms/src/config.ts +8 -0
  209. package/templates/apps/frontend-cms/src/globals.css +105 -0
  210. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  211. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  212. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  213. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  214. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  215. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  216. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  217. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  218. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  219. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  220. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  221. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  222. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  223. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  224. package/templates/apps/frontend-cms/template.json +6 -0
  225. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  226. package/templates/apps/frontend-collab/README.md +80 -0
  227. package/templates/apps/frontend-collab/app.config.ts +38 -0
  228. package/templates/apps/frontend-collab/package.json +33 -0
  229. package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
  230. package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
  231. package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
  232. package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
  233. package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
  234. package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
  235. package/templates/apps/frontend-collab/template.json +6 -0
  236. package/templates/apps/frontend-collab/tsconfig.json +11 -0
  237. package/templates/apps/frontend-contact/package.json +7 -7
  238. package/templates/apps/frontend-dashboard/app.config.ts +1 -1
  239. package/templates/apps/frontend-dashboard/package.json +7 -7
  240. package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
  241. package/templates/apps/frontend-docs/package.json +7 -7
  242. package/templates/apps/frontend-i18n/package.json +6 -6
  243. package/templates/apps/frontend-landing/package.json +7 -7
  244. package/templates/apps/frontend-portal/README.md +71 -0
  245. package/templates/apps/frontend-portal/app.config.ts +37 -0
  246. package/templates/apps/frontend-portal/package.json +32 -0
  247. package/templates/apps/frontend-portal/src/config.ts +8 -0
  248. package/templates/apps/frontend-portal/src/globals.css +93 -0
  249. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  250. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  251. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  252. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  253. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  254. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  255. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  256. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  257. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  258. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  259. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  260. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  261. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  262. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  263. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  264. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  265. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  266. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  267. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  268. package/templates/apps/frontend-portal/template.json +6 -0
  269. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  270. package/templates/apps/frontend-saas/README.md +73 -0
  271. package/templates/apps/frontend-saas/app.config.ts +46 -0
  272. package/templates/apps/frontend-saas/package.json +32 -0
  273. package/templates/apps/frontend-saas/src/config.ts +8 -0
  274. package/templates/apps/frontend-saas/src/globals.css +85 -0
  275. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  276. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  277. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  278. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  279. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  280. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  281. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  282. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  283. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  284. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  285. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  286. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  287. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  288. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  289. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  290. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  291. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  292. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  293. package/templates/apps/frontend-saas/template.json +6 -0
  294. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  295. package/templates/apps/frontend-spa/app.config.ts +1 -1
  296. package/templates/apps/frontend-spa/package.json +7 -7
  297. package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
  298. package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
  299. package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
  300. package/templates/apps/frontend-ssr/app.config.ts +1 -1
  301. package/templates/apps/frontend-ssr/package.json +7 -7
  302. package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
  303. package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
  304. package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
  305. package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
  306. package/templates/apps/frontend-ssr-api/package.json +8 -8
  307. package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
  308. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
  309. package/templates/apps/frontend-static-blog/package.json +6 -6
  310. package/templates/apps/frontend-status/README.md +51 -0
  311. package/templates/apps/frontend-status/app.config.ts +39 -0
  312. package/templates/apps/frontend-status/package.json +32 -0
  313. package/templates/apps/frontend-status/src/config.ts +7 -0
  314. package/templates/apps/frontend-status/src/globals.css +70 -0
  315. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  316. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  317. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  318. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  319. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  320. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  321. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  322. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  323. package/templates/apps/frontend-status/template.json +6 -0
  324. package/templates/apps/frontend-status/tsconfig.json +11 -0
  325. package/templates/apps/mobile-app/README.md +58 -0
  326. package/templates/apps/mobile-app/app.config.ts +33 -0
  327. package/templates/apps/mobile-app/babel.config.js +5 -0
  328. package/templates/apps/mobile-app/metro.config.js +20 -0
  329. package/templates/apps/mobile-app/package.json +33 -0
  330. package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
  331. package/templates/apps/mobile-app/src/app/index.tsx +62 -0
  332. package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
  333. package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
  334. package/templates/apps/mobile-app/src/client.ts +74 -0
  335. package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
  336. package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
  337. package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
  338. package/templates/apps/mobile-app/src/persistence.ts +38 -0
  339. package/templates/apps/mobile-app/template.json +6 -0
  340. package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
  341. package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
  342. package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
  343. package/templates/apps/mobile-app/tsconfig.json +10 -0
  344. package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
  345. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  346. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  347. package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
  348. package/dist/apiBuild-DgBS9ayv.js +0 -2
  349. package/dist/dbCommand-DvguqlzF.js +0 -2
  350. package/dist/fileConventions-3bffWssN.js +0 -30
  351. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  352. package/dist/inspect-gt8bq-Tz.js +0 -2
  353. package/dist/manifestBuild-ifczArzr.js +0 -2
  354. package/dist/serveCommand-ZTn-dPFa.js +0 -1425
@@ -19,18 +19,18 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
19
19
 
20
20
  | Group | Commands |
21
21
  |---|---|
22
- | [Scaffolding](/docs/cli/scaffolding) | `create-project`, `add-app`, `list-templates` |
22
+ | [Scaffolding](/docs/cli/scaffolding) | `create-project`, `add-app`, `list-templates`, `new` (scaffold one primitive — `query` / `mutation` / `action` / `workflow` / `page`) |
23
23
  | Packages | `package` (`create` / `publishable` / `private` / `status`), `create-package` |
24
- | [Dev](/docs/cli/dev) | `dev`, `codegen`, `agents-md`, [`env`](/docs/cli/env) (`check` / `sync` / `types` / `turbo`), [`generate`](/docs/cli/scaffolding) (AI app-builder), `dashboard` (serve the DevTools dashboard standalone; `--port`, `VOLTRO_DASHBOARD_APPS`) |
24
+ | [Dev](/docs/cli/dev) | `dev`, `codegen`, `typecheck` (`tsc --noEmit` with the app's own TypeScript), `agents-md`, [`env`](/docs/cli/env) (`check` / `sync` / `types` / `turbo`), [`generate`](/docs/cli/scaffolding) (AI app-builder), `dashboard` (serve the DevTools dashboard standalone; `--port`, `VOLTRO_DASHBOARD_APPS`) |
25
25
  | [Build & run](/docs/cli/build-and-start) | `build`, `start`, `serve` |
26
26
  | Deploy | `deploy` (`plan` — auto-detect the target tier per app + function), [`serverless`](/docs/deployment/serverless-functions) (`list` / `dev` / `serve` / `build` / `deploy`), [`static`](/docs/deployment/static-sites) (`hosts` / `deploy`), `dormancy` (single-node scale-to-zero: fronts the app, stops it when idle, wakes on the next request; `--idle-grace-ms` / `--tick-ms`) |
27
- | [Database](/docs/cli/migrate) | `migrate`, `db` (`plan` / `apply` / `plans` / `drift` / `squash` / `restore-snapshot` / `migrate` / `rollback` / `status` / `seed`) |
27
+ | [Database](/docs/cli/migrate) | `migrate`, `db` (`plan` / `apply` / `plans` / `drift` / `squash` / `restore-snapshot` / `migrate` / `rollback` / `status` / `seed`), [`evolve`](/docs/database/migrations/rename-and-drop) (schema-evolution copilot — propose a codemod + branch-verified backfill for a rename / retype / split / drop of an existing column or table) |
28
28
  | [Update](/docs/cli/update) | `update` (`--to` / `--dry-run` / `--force` / `--exact`) — bump every `@voltro/*`, install, run the codemods that adapt your source to the new version |
29
29
  | [Data transfer](/docs/cli/data) | `data` (`export` / `import` / `unpack` / `inspect` / `backup` / `restore`) — directory + single-file `.vbundle` bundles, streaming assets, masking, at-rest encryption |
30
30
  | Ops / infra | `cache` (`status` / `flush` / `invalidate`), `add` (`redis`), `baseline` (`list` / `status` / `set`), `schedule-manifest`, [`storage`](/docs/plugins/storage) (`doctor` / `cors`) |
31
- | AI / data | `embeddings backfill <table> --text <field> --vector <field>` — (re)embed rows the `vectorEmbedding()` mixin missed (pre-existing rows / a model change); `--dry-run` to preview |
31
+ | AI / data | `embeddings backfill <table> --text <field> --vector <field>` — (re)embed rows the `vectorEmbedding()` mixin missed (pre-existing rows / a model change); `--dry-run` to preview; [`eval`](/docs/ai/agents#evaluating-recorded-runs-voltro-eval) — replay recorded agent runs against golden cases + judge, exit 1 on regression (a deploy gate; `--json` / `--branch` / `--threshold`) |
32
32
  | [Inspect & debug](/docs/cli/inspect) | `inspect`, `logs`, `traces`, `workflows`, `cluster`, `check` |
33
- | [Health & surface](/docs/cli/build-and-start) | [`doctor`](/docs/cli/build-and-start) — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); [`capabilities`](/docs/cli/build-and-start) (`--json`) — the export surface read from your installed `@voltro/*`, so it can be verified instead of recalled |
33
+ | [Health & surface](/docs/cli/build-and-start) | [`doctor`](/docs/cli/build-and-start) — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); [`capabilities`](/docs/cli/build-and-start) (`--json`) — the export surface read from your installed `@voltro/*`, so it can be verified instead of recalled; `info` (`--json`) — CLI / node / package-manager / dialect + every installed `@voltro/*` version, flagging lockstep skew (exits 1 on skew) |
34
34
  | Harness | `test`, `e2e` |
35
35
  | Cloud | `cloud` (`login` / `whoami` / `projects` / `env` / `import`) |
36
36
  | Secrets | `secret` (`generate [purpose]` — the right var+format per secret; `generate` alone → a generic secret; `list`) |
@@ -69,7 +69,8 @@ The CLI reads:
69
69
  | `VOLTRO_LOG_FORMAT` | `pretty` / `json`. Force the logger's output format. |
70
70
  | `VOLTRO_LOG_LEVEL` | `trace` / `debug` / `info` / `warn` / `error` / `fatal`. |
71
71
  | `VOLTRO_INSPECT` | `off` to disable the `/_voltro/inspect/*` HTTP endpoints. |
72
- | `VOLTRO_INSPECT_TOKEN` | Bearer token for guarded inspect endpoints. |
72
+ | `VOLTRO_INSPECT_TOKEN` | Bearer for the inspect surface. **Fail-closed:** unset → every request is `401`. `voltro dev` mints one per project; `voltro serve` / `voltro start` mint nothing, so a public deploy is closed by default (set it explicitly to open the surface). |
73
+ | `VOLTRO_INSPECT_ALLOWED_HOSTS` | Extra `Host` names allowed to reach the **dev** inspect surface, past its DNS-rebinding guard (comma/space-separated). Loopback names + IP literals are always allowed; any other domain name is refused unless listed here — the api counterpart of vite's `allowedHosts`. |
73
74
  | `DB_URL` | Database connection string (falls back to `DB_PRIMARY_URL`; or the discrete `DB_*` / `PG_*` fields). |
74
75
  | `VOLTRO_SESSION_SECRET` | Session-cookie signing secret (`@voltro/plugin-auth`). Rotate with zero downtime: move the old value to `VOLTRO_SESSION_SECRET_PREVIOUS` for one session lifetime — cookies signed with either secret keep verifying, and previous-key cookies are re-issued under the new one. |
75
76
  | `VOLTRO_DATA_TRANSFER_SECRET` | Gates the prod data-transfer endpoints (`POST /_voltro/admin/{export,import}`); ≥16 chars or the routes don't mount. |
@@ -116,6 +117,47 @@ voltro build # vite build + SSG pre-render
116
117
  voltro start # production server
117
118
  ```
118
119
 
120
+ ### "Scaffold a new primitive the right way"
121
+
122
+ `voltro new <kind> <name>` writes the correct file convention(s) so you don't
123
+ learn the descriptor/executor split or the browser-safe boundary from a boot-time
124
+ error. It refuses to overwrite an existing file unless you pass `--force`.
125
+
126
+ ```bash
127
+ voltro new query notes.list # notes.list.query.ts + notes.list.query.server.ts
128
+ voltro new mutation notes.create # descriptor + .server executor pair
129
+ voltro new action notes.touch # descriptor + .server executor pair
130
+ voltro new workflow orders.fulfill # .workflow.tsx descriptor + .workflow.server.tsx executor
131
+ voltro new page about # src-pages page.tsx under the name path
132
+ voltro new query billing.summary --dir queries # write into a subdirectory
133
+ ```
134
+
135
+ The descriptor half imports only `@voltro/protocol` (or `@voltro/workflow/define`)
136
+ + `effect` — browser-safe by construction; the server graph lives in the paired
137
+ `.server` file. Fill in the `TODO`s, then `voltro dev` discovers it.
138
+
139
+ ### "Type-check before I commit"
140
+
141
+ ```bash
142
+ voltro typecheck # tsc --noEmit against ./tsconfig.json
143
+ voltro typecheck apps/api # a specific app
144
+ voltro typecheck --project tsconfig.build.json
145
+ ```
146
+
147
+ It runs the **app's own** TypeScript (a `tsc --noEmit`), so "green" means tests
148
+ AND types. Any flag it doesn't own passes straight through to `tsc`.
149
+
150
+ ### "What versions am I actually running?"
151
+
152
+ ```bash
153
+ voltro info # CLI, node, package manager, dialect + @voltro/* versions
154
+ voltro info --json # machine-readable; exits 1 on version skew
155
+ ```
156
+
157
+ `@voltro/*` ship in lockstep, so a mismatch (e.g. `@voltro/database` a minor
158
+ behind `@voltro/runtime`) means an untested graph. `voltro info` flags it and
159
+ `voltro update` realigns everything.
160
+
119
161
  ### "Something's wrong — inspect what's running"
120
162
 
121
163
  ```bash
@@ -1025,6 +1067,37 @@ For WebSocket connections to land on the same backend (required for in-process s
1025
1067
  - Reverse proxy: `lb_policy ip_hash` (Caddy) / `ip_hash` (nginx).
1026
1068
  - Or use `@voltro/plugin-cluster` to share subscription state across instances → any-load-balancer-works.
1027
1069
 
1070
+ ## `voltro build api --target <swift|kotlin>` — native SDK generation
1071
+
1072
+ Generate a fully native mobile client from the same API bindings the TypeScript client is generated from. No hand-written models, no drift: the SDK is derived from your app's **capability manifest** — the exact procedure descriptors + JSON Schemas the framework already assembles from source — so every type stays in lockstep with the server.
1073
+
1074
+ ```bash
1075
+ voltro build api --target swift apps/acme/api # → apps/acme/api/sdk/swift (Swift Package)
1076
+ voltro build api --target kotlin apps/acme/api # → apps/acme/api/sdk/kotlin (Kotlin Multiplatform)
1077
+ voltro build api --target swift --out ./ios/Sdk --name AcmeClient .
1078
+ ```
1079
+
1080
+ Flags:
1081
+
1082
+ - `--target swift | kotlin` — the language to emit. Required.
1083
+ - `--out <dir>` — output directory. Default: `<appDir>/sdk/<target>`.
1084
+ - `--name <PackageName>` — the Swift package / Kotlin module name (PascalCase). Default `VoltroClient`.
1085
+ - `--kotlin-package <dotted>` — Kotlin source package. Default `com.voltro.client`.
1086
+
1087
+ What each package contains:
1088
+
1089
+ | Piece | Swift | Kotlin |
1090
+ |---|---|---|
1091
+ | Type-safe models | `Codable` structs + `String` enums | `@Serializable` data classes + enum classes |
1092
+ | One-shot client (query / mutation / action) | `async throws` methods over `URLSession` | `suspend` methods over Ktor |
1093
+ | Subscription client (streams) | `AsyncThrowingStream` over `URLSessionWebSocketTask` | `Flow` over Ktor WebSockets |
1094
+ | Auth + tenant context | `AuthContext` (bearer + `x-tenant` headers) | `AuthContext` |
1095
+ | Push registration | `PushRegistration` stub | `PushRegistration` stub |
1096
+
1097
+ Type mapping is faithful: `string → String`, `integer → Int`, `number → Double/Double`, `boolean → Bool/Boolean`, arrays → `[T]` / `List<T>`, nested objects → their own named type, string-literal unions → an enum, and an **optional field** (one absent from the schema's `required` set, or a `NullOr`) becomes a Swift `Optional` / Kotlin nullable with a `= nil` / `= null` default.
1098
+
1099
+ **Scope — this is the SDK code generator, not a native runtime.** Deliberately out of scope (they need a native runtime or managed infra, not generated client code): native module bindings (camera, biometrics), the APNs/FCM push **sender** (per-tenant Apple/Firebase credentials, provisioned server-side), and the managed OTA / EAS build pipeline. The generated source is verified at the generator level (golden-string tests over the emitted Swift + Kotlin). Compiling it with `swiftc` / Gradle is the remaining step in your own mobile CI — the framework harness has no Swift/Kotlin toolchain.
1100
+
1028
1101
  ## `voltro doctor` — preflight a production serve
1029
1102
 
1030
1103
  Production `voltro serve` for an **API** app boots ONLY from the precompiled serve
@@ -1210,6 +1283,51 @@ since a rename or a deletion is decidable whatever the payload looks like.
1210
1283
  The required keys come from the live `payloadSchema`, the same source the runtime
1211
1284
  validation reads, so the two cannot disagree about what a payload needs.
1212
1285
 
1286
+ ### The junction-FK check
1287
+
1288
+ A link / junction table (`projectMembers`, `todoTagAssignments`) exists to connect two aggregates, so its columns are almost all foreign keys. Declared with `reference(() => projects)` the framework knows the edge — it enforces integrity, auto-indexes the FK, and can walk the reference graph. Declared as a bare `text()` id column the *same* edge is invisible: no FK, no auto-index, and nothing that walks references can follow it. Nothing type-checks the difference.
1289
+
1290
+ `voltro doctor` flags a junction table with an id-shaped column that is a plain scalar and not a `reference()`:
1291
+
1292
+ ```
1293
+ junction FKs: 2 junction tables with an id column that is a plain text() and not a reference()
1294
+ 'todoListMembers': 'todoListId', 'userId' (part of a composite primary key)
1295
+ 'todoTagAssignments': 'todoId', 'tagId' (this table is nothing but link columns)
1296
+ Declare each as reference(() => <table>): the FK is enforced, the column is auto-indexed,
1297
+ and the relationship becomes walkable (a plain text() id column is an invisible edge).
1298
+ ```
1299
+
1300
+ It will **not** fire on any `*Id` text column — a `tenantId`, a `traceId`, an external-system reference are all legitimate plain-scalar shapes. It fires only when the table's OWN structure independently says "link table", and it names which signal tripped it so the finding is auditable rather than a bare accusation:
1301
+
1302
+ | Signal | What it means |
1303
+ |---|---|
1304
+ | `part of a composite primary key` | the suspect column is a member of an explicit `primaryKey([...])` — the PK structure alone proves the row is a link |
1305
+ | `sits beside a wired reference() on this table` | a real `reference()` on a same-shaped sibling column, while this one is a bare scalar |
1306
+ | `this table is nothing but link columns` | the whole table is id-shaped columns + bookkeeping (a pure link table) |
1307
+
1308
+ The audit reads the tables' **real declared `ColumnType`s** — the same materialised column definitions the migrator emits DDL from — never source text. So a `reference` is told apart from a plain scalar by its declared type, not a name regex, and a column name that only appears in a comment cannot trip it.
1309
+
1310
+ ### Event delivery + scale
1311
+
1312
+ Two events with identical route / subscriber / buffer numbers can mean **opposite** things about a missing message — `each` counts a drop as a loss and tells the subscriber, `latest` supersedes the pending value and says nothing — and that mode is invisible once the app is running. So `voltro doctor` lists every declared event's delivery mode:
1313
+
1314
+ ```
1315
+ event delivery: 4 declared events — the mode decides what a MISSING message means
1316
+ 'games.started': each
1317
+ 'player.moved': latest
1318
+ each — every delivery matters; a slow subscriber loses the oldest and is TOLD how many (the default).
1319
+ latest — a newer delivery supersedes a pending one; a slow subscriber gets the current value, told nothing.
1320
+ ```
1321
+
1322
+ It also **warns** on two shapes that will not scale the way the declaration reads — advisory, never blocking:
1323
+
1324
+ | Warning | Why |
1325
+ |---|---|
1326
+ | **routing key has 3+ fields** | every key field is a routing address, and the count of distinct routes is the *product* of the fields' value spaces. Check each is an ADDRESS the delivery is decided by (`arenaId`), not a discriminator the handler reads (`gameType`) — the latter belongs in the payload, not the key. |
1327
+ | **`webhook:` on a per-frame event** | a webhook block on a name like `player.moved` / `cursor.moved` / `*.frameRendered` becomes N HTTP deliveries per second *per subscribed target*. The webhook rate limit **defers** the excess as pending rows rather than failing, so the symptom is a growing table. Publish a coarser event (a summary / state change) for the outside world. |
1328
+
1329
+ The field count comes from the same schema-property reader the runtime validation uses, so it cannot disagree with the key the event actually routes on. Both findings appear in `voltro doctor --json` under `eventDelivery`.
1330
+
1213
1331
  ### The hand-roll detector
1214
1332
 
1215
1333
  `voltro doctor` also scans your source for shapes the framework already has a
@@ -1735,16 +1853,35 @@ voltro workflows resume <runId>
1735
1853
  voltro workflows signal <runId> --name approval # inject a named signal
1736
1854
  voltro workflows update <runId> --name …
1737
1855
  voltro workflows children <parentExecutionId>
1856
+ voltro workflows flow # the admission queue + ledger
1857
+ voltro workflows pause|unpause <workflowName> # stop/restart admission fleet-wide
1858
+ voltro workflows cancel-many --reason "…" # DRY RUN until --commit
1859
+ voltro workflows replay-many --mode redrive # DRY RUN until --commit
1860
+ voltro workflows inferences # offloaded model calls in flight
1738
1861
  ```
1739
1862
 
1863
+ `inferences` shows what nothing else can: a run parked on an offloaded model call reads `suspended` in the run list with no step row yet, so during a slow provider — the moment you would look — the run list has nothing to say.
1864
+
1740
1865
  Underneath, workflow state lives in the `_voltro_workflow_runs` + `_voltro_workflow_run_steps` tables and is surfaced live by the dashboard's Workflows panel. The same data is reachable over HTTP:
1741
1866
 
1742
1867
  ```bash
1743
1868
  curl -s localhost:$PORT/_voltro/inspect/workflows/runs | jq # recent runs
1744
1869
  curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/steps" | jq # step-by-step
1745
1870
  curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/events" | jq # the run's event log
1871
+ curl -s "localhost:$PORT/_voltro/inspect/workflows/stats?hours=24" | jq # bucketed run activity (the dashboard chart)
1746
1872
  ```
1747
1873
 
1874
+ The runs endpoint filters **server-side**, so a triage query over a large run history costs one narrow page instead of the whole table:
1875
+
1876
+ ```bash
1877
+ # multi-status + tag search + source + id-prefix + time range — all composable
1878
+ curl -s "localhost:$PORT/_voltro/inspect/workflows/runs?statuses=failed,cancelled&q=orders&source=workflow-rpc&idPrefix=wfrun_&from=2026-08-01T00:00:00Z&to=2026-08-08T00:00:00Z" | jq
1879
+ ```
1880
+
1881
+ `statuses` is a comma list; `q` is a case-insensitive tag substring; `idPrefix` matches the run id **or** the execution id (you never have to know which kind your log line carried); `from`/`to` bound `startedAt`. The dashboard's filter bar sends exactly these params.
1882
+
1883
+ `/workflows/stats` returns ~48 buckets over a trailing window (`hours`, default 24, max 168; optional `tag`), each with `started` / `succeeded` / `failed` / `cancelled` counts, plus per-workflow totals. When the window held more runs than the scan cap, the response says `truncated: true` — the chart renders that as a warning, because a silently-truncated chart shows throughput dropping at exactly the moment it spiked.
1884
+
1748
1885
  Each run row carries ID, name, status (running / succeeded / failed / dead), step count, last completed step, and duration. The per-run action endpoint matches `…/workflows/runs/<runId>/<action>` for `cancel` / `retry` / `suspend` / `resume` / `signal` — the `voltro workflows` subcommands and the dashboard's run-detail buttons both POST to these:
1749
1886
 
1750
1887
  ```bash
@@ -265,6 +265,38 @@ const sync = resolveSecretSync('SESSION_SECRET') // env-only fast path (s
265
265
 
266
266
  The field-encryption key (for `.encrypted()` columns — see [plugin-governance](/docs/plugins/governance)) resolves through this same backend. `governancePlugin({ fieldEncryption: true })` reads the secret `VOLTRO_FIELD_ENCRYPTION_KEY` (override with `fieldEncryption: { secretKey }`); point `secrets` at your vault and the key never touches an env file.
267
267
 
268
+ ## Live rotation — swap a secret without a restart
269
+
270
+ The boot env gate resolves every secret once, at start-up. Rotating a leaked key normally means a redeploy. `@voltro/env/server` lets a running process cut over to a re-resolved value **and keep accepting the old one for a grace window** — so requests signed with the previous key still verify while callers catch up.
271
+
272
+ ```ts
273
+ import { rotateSecretLive, getSecretWithOverlap } from '@voltro/env/server'
274
+
275
+ // Re-resolve WEBHOOK_SIGNING_SECRET through the active backend and cut over,
276
+ // holding the OLD value valid for a 5-minute overlap (the default).
277
+ await rotateSecretLive('WEBHOOK_SIGNING_SECRET', { graceMs: 5 * 60_000 })
278
+
279
+ // A verifier accepts BOTH during the overlap — try current first, fall back:
280
+ const { current, previous } = getSecretWithOverlap('WEBHOOK_SIGNING_SECRET')
281
+ ```
282
+
283
+ `getSecretWithOverlap(key)` returns `{ current, previous }` — the same current/previous pattern session verification uses for `VOLTRO_SESSION_SECRET` + `_PREVIOUS`. `previous` is present only while a rotation's grace window is open, then `undefined` (revoked lazily, on read — no timer). For a value you already have in hand (e.g. fetched from your own KMS), the lower-level `refreshEnvValue` installs it directly:
284
+
285
+ ```ts
286
+ import { refreshEnvValue } from '@voltro/env'
287
+
288
+ refreshEnvValue('WEBHOOK_SIGNING_SECRET', nextValue, {
289
+ previous: oldValue, // held valid for the grace window
290
+ graceMs: 5 * 60_000,
291
+ })
292
+ ```
293
+
294
+ `refreshEnvValue` throws if called **before** the boot env gate ran — a live rotation is a post-boot operation that replaces a value the gate already resolved, not a way to set one that was missing.
295
+
296
+ ### What live rotation actually reaches — the honest bound
297
+
298
+ This updates what code that reads a secret **per use** sees: outbound API keys resolved on each call, webhook-signing verification, `.encrypted()` field encryption. It does **not** reconnect a live resource built once, at boot, from the old credential — a database connection pool created with the previous password keeps that connection. Rotating a DB password stays a reconnect concern (drain + rebuild the pool, or redeploy); rotating a signing or outbound key is what this is for.
299
+
268
300
  ## Testing
269
301
 
270
302
  `setSecretsBackend(backend)` installs a backend for a test; `resetSecretsBackend()` restores the env default. `resolveSecretsBackend(config)` is the pure resolver the boot path uses to turn the `secrets` config value into a backend.
@@ -1024,6 +1024,67 @@ const message = matchError(err, {
1024
1024
  }, () => 'Something went wrong')
1025
1025
  ```
1026
1026
 
1027
+ ## Cross-table business rules — `.rule()`
1028
+
1029
+ A [typed error](#typed-errors) is declared on ONE mutation. A **rule** is declared on a TABLE: an invariant the runtime enforces on EVERY mutation that writes that table, no matter which one did the write. Declare it in the schema with `.rule(name, predicate)`:
1030
+
1031
+ ```ts
1032
+ import { table, id, integer, eq } from '@voltro/database'
1033
+ import { database } from '../database/schema'
1034
+
1035
+ export const invoices = table('invoices', {
1036
+ id: id(),
1037
+ total: integer(),
1038
+ }).rule(
1039
+ 'totalMatchesLineItems',
1040
+ async (row, { store }) => {
1041
+ const items = await store.query(
1042
+ database.lineItems.where(eq('invoiceId', row.id)).descriptor,
1043
+ )
1044
+ const sum = items.reduce((acc, l) => acc + Number(l.amount), 0)
1045
+ return sum === row.total || { params: { computed: sum, declared: row.total } }
1046
+ },
1047
+ )
1048
+ ```
1049
+
1050
+ The predicate receives the **post-write row** and a `context` whose `store` is the SAME transactional store the mutation wrote through — so a cross-table read shares the write's MVCC snapshot and cannot race it. It runs INSIDE the mutation transaction, after the write and before commit, and it is dialect-neutral (the same predicate is correct on all four dialects — no per-dialect code). Return `true` (or nothing) when the invariant holds; return `false` or a `RuleViolationDetail` (`{ params?, field?, message? }`) to signal a violation for THIS row.
1051
+
1052
+ Unlike [`.check()`](/docs/database/columns#db-level-checks) — a single-row SQL `CHECK` the DATABASE enforces as DDL — a rule is a PREDICATE the runtime evaluates, so it can read OTHER tables. Use `.check()` when the database itself must guarantee a single-row constraint; reach for `.rule()` for a cross-table invariant (an invoice total matching its line items, a booking not exceeding a resource's capacity).
1053
+
1054
+ ### The violation is a typed error — automatically
1055
+
1056
+ A violated `error`-severity rule rolls the whole mutation back and fails with the typed, wire-preserved **`BusinessRuleViolation`**. You do **not** declare it on the mutation's `error:` — the runtime auto-merges it into every mutation's error union at the wire boundary, exactly like `ScopeError`. A rule declared on a SCHEMA table can fail ANY mutation that writes that table, so no single descriptor could know to declare it; the auto-merge is what keeps the violation a typed error the client decodes by `_tag` rather than an untyped defect.
1057
+
1058
+ Match it on the client the same way as any [typed error](#matching-typed-errors-on-the-client):
1059
+
1060
+ ```ts
1061
+ import { errorTag } from '@voltro/protocol'
1062
+
1063
+ try {
1064
+ await createInvoice(input)
1065
+ } catch (err) {
1066
+ if (errorTag(err) === 'BusinessRuleViolation') {
1067
+ // err.rule — the rule name; err.params — the offending values; err.field — the pointer
1068
+ }
1069
+ }
1070
+ ```
1071
+
1072
+ `BusinessRuleViolation` carries `rule` (the declared name), optional `params` (i18n params for the offending values), an optional `field` pointer, and `severity` (always `'error'` on the wire — a warning-severity rule never reaches the client as an error).
1073
+
1074
+ ### `severity: 'warning'` — log without blocking
1075
+
1076
+ ```ts
1077
+ export const invoices = table('invoices', { id: id(), total: integer() }).rule(
1078
+ 'totalMatchesLineItems',
1079
+ async (row, { store }) => true,
1080
+ { severity: 'warning' },
1081
+ )
1082
+ ```
1083
+
1084
+ A `warning`-severity rule LOGS and audits the violation but lets the write commit — useful while backfilling data that does not yet satisfy a newly-added invariant. The default is `'error'` (roll back).
1085
+
1086
+ Rules run on `voltro dev` and `voltro serve` through the same mutation runner, so the two boot paths cannot disagree about whether a rule fires. Inserts and updates on your own tables are re-validated; a delete is not (there is no post-write row to check).
1087
+
1027
1088
  ## `internal: true` — off the wire entirely
1028
1089
 
1029
1090
  Every discovered `*.mutation.ts` / `*.query.ts` / `*.action.ts` / `*.stream.ts` is
@@ -1472,6 +1533,88 @@ A subscription that was live and is then skipped goes idle — it does **not**
1472
1533
  keep serving the snapshot it still holds. Otherwise `skip: !open` would show
1473
1534
  last time's data the moment a dialog reopens.
1474
1535
 
1536
+ ## SSR preload — first paint with data
1537
+
1538
+ By default a `useSubscription` on an SSR page flashes its empty/loading state on
1539
+ mount, THEN opens the WebSocket and fetches the first snapshot — even though the
1540
+ server could have fetched that value during the render. **`usePreloadedSubscription`**
1541
+ closes that gap: it reads its first value from the SSR hydration payload, renders
1542
+ real data on the first paint, then upgrades to the live stream the instant its
1543
+ first snapshot lands.
1544
+
1545
+ Two things wire it up: the hook, and a `preload` export on the page.
1546
+
1547
+ ```tsx
1548
+ import { usePreloadedSubscription } from '@voltro/client'
1549
+
1550
+ export const preload = ['notes.list']
1551
+
1552
+ export default function NotesPage() {
1553
+ const { data } = usePreloadedSubscription<Note[]>('app', 'notes.list')
1554
+ // `data` is present on the first paint — no loading flash
1555
+ return <NotesTable notes={data} />
1556
+ }
1557
+ ```
1558
+
1559
+ `export const preload` lists the subscriptions the page's tree needs at first
1560
+ paint. During the SSR render (`voltro dev` and `voltro start`) the framework runs
1561
+ each one server-side — the SAME `ctx.query(tag, input)` a loader receives — and
1562
+ seeds the result into the hydration payload. The client's
1563
+ `usePreloadedSubscription` finds the seed by the SAME cache key `useSubscription`
1564
+ uses, so the server markup and the client hydration render read an identical
1565
+ value — there is no hydration mismatch. The live subscription still opens and
1566
+ takes over; the seed is only the first value, never the source of truth.
1567
+
1568
+ ### `preload` entries
1569
+
1570
+ An entry is either a bare tag or a tag plus an `input` derived from the route
1571
+ params:
1572
+
1573
+ ```tsx
1574
+ export const preload = [
1575
+ 'teams.list', // no input
1576
+ { tag: 'project.detail', input: (params) => ({ id: params.id }) },
1577
+ ]
1578
+ ```
1579
+
1580
+ The `input` you derive here MUST match the `input` you pass the hook — both
1581
+ address the same cache entry:
1582
+
1583
+ ```tsx
1584
+ export default function ProjectPage({ params }: { params: { id: string } }) {
1585
+ const { data } = usePreloadedSubscription('app', 'project.detail', { id: params.id })
1586
+ return <ProjectView project={data} />
1587
+ }
1588
+ ```
1589
+
1590
+ ### Falls back to `useSubscription`
1591
+
1592
+ When no seed exists for the key — a client-side SPA navigation the server never
1593
+ rendered, or the static prerender (which has no live api origin) —
1594
+ `usePreloadedSubscription` behaves EXACTLY like `useSubscription`: it loads until
1595
+ the stream answers. So it is always safe to reach for; the preload is a
1596
+ first-paint optimization, never a correctness dependency. A failed preload is
1597
+ likewise non-fatal — the live subscription still delivers the value on the
1598
+ client, the only loss is the first-paint seed.
1599
+
1600
+ ### Seeding by hand
1601
+
1602
+ `export const preload` is sugar over an explicit seed. When a loader ALREADY has
1603
+ the value — you fetched it for the `<title>`, a breadcrumb, or the row name — seed
1604
+ it directly with `seedPreloadedSubscription` (server-side only) instead of
1605
+ fetching it a second time:
1606
+
1607
+ ```ts
1608
+ import { seedPreloadedSubscription } from '@voltro/client'
1609
+
1610
+ // inside a loader / layout loader, server-side
1611
+ const notes = await ctx.query('notes.list', {})
1612
+ seedPreloadedSubscription('app', 'notes.list', {}, notes)
1613
+ ```
1614
+
1615
+ Calling it outside a server render throws — on the client the live subscription
1616
+ already provides the value, so a client-side seed would be meaningless.
1617
+
1475
1618
  ## Streams Are Different
1476
1619
 
1477
1620
  For non-database or transient element feeds, use [streams](/docs/data/streams), not subscriptions:
@@ -2697,6 +2840,145 @@ export default defineAggregate({
2697
2840
 
2698
2841
  When no analytics sink is configured the framework provides the no-op sink: the read methods fail with `AnalyticsCapabilityNotSupported({ provider: 'noop' })`, which surfaces as a refresh error (last-good rows are preserved). Provider-specific queries beyond the four-method contract (raw SQL, HyperLogLog) live outside the sink — there is no raw-client escape hatch; query the provider with your own client instance inside the build function where you need them.
2699
2842
 
2843
+ ## Standing IVM siblings — expectations, cost budgets, experiments
2844
+
2845
+ Three more primitives are built on the same engine as `defineAggregate({ incremental })` — they reduce a table to a value maintained incrementally from `store.onChange` CDC deltas (O(1) per write, no re-query). Unlike an aggregate you never `read()` a materialised table; you **observe a live signal** through a framework registry and a `useX(def)` handler-sugar. Each is discovered by its own file convention and wired into both `voltro dev` and `voltro serve`.
2846
+
2847
+ | Primitive | File | Reduces a table to | Observe with |
2848
+ |---|---|---|---|
2849
+ | `defineExpectation` | `*.expectation.ts` | a `holding`/`violated` data-quality signal | `useExpectation` / `ExpectationRegistry` |
2850
+ | `defineCostBudget` | `*.budget.ts` | per-tenant compute-cost attribution + `ok`/`warn`/`exceeded` budgets | `useCostBudget` / `CostRegistry` |
2851
+ | `defineExperiment` | `*.experiment.ts` | per-variant metric + lift (live A/B / holdout) | `useExperiment` / `ExperimentRegistry` |
2852
+
2853
+ They are **soft, observability-grade signals** — none of them ever blocks a write. Rejecting a write is a business rule's job (`table().validate(Schema)`); running an agent/workflow on a change is [`*.reaction.tsx`](/docs/data/reactions). These three only *watch* and *report*.
2854
+
2855
+ ### Data-quality expectations (`*.expectation.ts`)
2856
+
2857
+ A `defineExpectation` is a standing data-quality contract over a table. Where dbt tests / Great Expectations run in BATCH and catch bad data hours later, an expectation re-evaluates on every write and tips `holding`↔`violated` the instant an invariant breaks — tracing the violation to the write that caused it (the CDC event's `traceId` / `subjectId` / `procedure`).
2858
+
2859
+ ```ts
2860
+ // apps/api/expectations/orderFreshness.expectation.ts
2861
+ import { defineExpectation } from '@voltro/runtime'
2862
+
2863
+ export default defineExpectation({
2864
+ name: 'orders-fresh',
2865
+ on: { table: 'orders' }, // its CDC deltas drive re-evaluation
2866
+ invariant: { kind: 'freshness', column: 'createdAt', maxAgeMs: 10 * 60_000 },
2867
+ severity: 'critical', // 'info' | 'warn' (default) | 'critical' — alerting priority only
2868
+ })
2869
+ ```
2870
+
2871
+ The default export must be the return value of `defineExpectation({...})` — the cli discovers `*.expectation.ts` files by that branded shape.
2872
+
2873
+ Four invariant kinds, each reducing the table to one incrementally-maintained metric:
2874
+
2875
+ ```ts
2876
+ { kind: 'freshness', column: 'createdAt', maxAgeMs: 600_000 } // newest row no older than X (re-checked on a clock tick too)
2877
+ { kind: 'nullRate', column: 'email', maxRate: 0.01 } // ≤ 1% of rows null in `column`
2878
+ { kind: 'rowCount', min: 1, max: 100_000 } // COUNT(*) within [min, max]
2879
+ { kind: 'valueBounds', column: 'price', min: 0, max: 1000, maxViolationRate: 0.05 } // ≤ 5% of rows out of [min, max]
2880
+ ```
2881
+
2882
+ An optional `on.where` predicate narrows the population the invariant is asserted over — it runs server-side per row and never leaves the server, so it can be any predicate:
2883
+
2884
+ ```ts
2885
+ on: { table: 'orders', where: (row) => (row['status'] as string) === 'paid' }
2886
+ ```
2887
+
2888
+ Observe one expectation from a handler with `useExpectation`, or read the whole surface off `ExpectationRegistry`:
2889
+
2890
+ ```ts
2891
+ import { Effect } from 'effect'
2892
+ import { useExpectation } from '@voltro/runtime'
2893
+ import ordersFresh from '../expectations/orderFreshness.expectation'
2894
+
2895
+ export default (input, _ctx) =>
2896
+ Effect.gen(function* () {
2897
+ const state = yield* useExpectation(ordersFresh)
2898
+ // state: { status: 'holding' | 'violated' | 'unknown', metric, threshold,
2899
+ // since, lastEvaluatedAt, lastCause, ... } | null (null = not registered)
2900
+ return { degraded: state?.status === 'violated' }
2901
+ })
2902
+ ```
2903
+
2904
+ `ExpectationRegistry` exposes `snapshot()` (every expectation's state — the inspect feed), `get(name)`, `violations()` (the alerting view), and `subscribe(listener)` for `violated`/`recovered` transitions. A transition carries the causing write's provenance; a `freshness` SLA aging out with no write reports `cause: null` — the honest "no write caused this; the ABSENCE of writes did".
2905
+
2906
+ ### Cost budgets (`*.budget.ts`)
2907
+
2908
+ A `defineCostBudget` is a reactive-FinOps primitive: per-tenant / per-subscription compute-cost attribution plus a budget every tenant is held to independently. It is to reactive compute what AI's `requireAiBudget` is to USD spend — a standing per-tenant ceiling that crosses `ok`→`warn`→`exceeded` and recovers on a window rollover.
2909
+
2910
+ ```ts
2911
+ // apps/api/budgets/tenantCompute.budget.ts
2912
+ import { defineCostBudget } from '@voltro/runtime'
2913
+
2914
+ export default defineCostBudget({
2915
+ name: 'tenant-compute',
2916
+ limit: 100_000, // the per-tenant ceiling, in the budget's unit
2917
+ unit: 'recompute', // which cost unit to meter; omit ⇒ the tenant's TOTAL across every unit
2918
+ warnAt: 0.8, // fraction of `limit` at which it goes 'warn' (default 0.8; set 1 to disable)
2919
+ window: '24h', // tumbling window — the counter resets each boundary; omit ⇒ cumulative since boot
2920
+ severity: 'warn', // 'info' | 'warn' (default) | 'critical'
2921
+ })
2922
+ ```
2923
+
2924
+ Observe one budget for one tenant with `useCostBudget(def, tenantId)`:
2925
+
2926
+ ```ts
2927
+ import { Effect } from 'effect'
2928
+ import { useCostBudget } from '@voltro/runtime'
2929
+ import tenantCompute from '../budgets/tenantCompute.budget'
2930
+
2931
+ export default (input, ctx) =>
2932
+ Effect.gen(function* () {
2933
+ const state = yield* useCostBudget(tenantCompute, ctx.tenantId)
2934
+ // state: { status: 'ok' | 'warn' | 'exceeded', spent, limit, warnThreshold, ... } | null
2935
+ return { overBudget: state?.status === 'exceeded' }
2936
+ })
2937
+ ```
2938
+
2939
+ `CostRegistry` exposes `attribution()` (per-tenant chargeback/showback rows — `total` + `byUnit` + `bySubscription`), `tenant(tenantId)`, `budgets()`, `budget(name, tenantId)`, `breaches()` (the alerting view), and `subscribe(listener)` for threshold crossings.
2940
+
2941
+ > **Reactive recomputes now feed attribution automatically.** Declaring any `*.budget.ts` wires the dispatcher's `recordCost` tap in both boot paths: every time a source-row change re-runs an affected subscription and pushes it a delta, one `{ unit: 'recompute', amount: 1 }` cost event is attributed to that subscription's tenant (and traceId). So a tenant's `total`/`spent` populates from real reactive work — a `unit: 'recompute'` budget crosses `ok`→`warn`→`exceeded` as deliveries accrue, and recovers on a window rollover. The emission is **per delivered recompute** — one event per subscription the change fanned out to, on both the row-set and computed-query delivery paths. An app that declares NO cost budget wires no tap and allocates nothing on the reactive hot path.
2942
+
2943
+ ### Online experiments (`*.experiment.ts`)
2944
+
2945
+ A `defineExperiment` is a live A/B / holdout experiment expressed as IVM aggregates. Assignment is a deterministic salted hash (subject → variant, no stored assignment table, reproducible on the client); the success metric is maintained PER VARIANT from the watched table's CDC — so lift vs a baseline is real-time, with no batch pipeline. It differs from `plugin-flags` (which GATES a code path) — an experiment MEASURES the outcome.
2946
+
2947
+ ```ts
2948
+ // apps/api/experiments/checkoutColor.experiment.ts
2949
+ import { defineExperiment } from '@voltro/runtime'
2950
+
2951
+ export default defineExperiment({
2952
+ name: 'checkout-button-color',
2953
+ on: { table: 'orders' }, // its CDC deltas drive the live recompute
2954
+ subject: 'userId', // stable per-subject bucketing (a column name, or a (row) => key fn)
2955
+ variants: [{ name: 'control' }, { name: 'green', weight: 1 }], // ≥ 2 arms; `weight` skews the split (default 1)
2956
+ holdout: 0.1, // 10% held out entirely, for a clean untouched baseline
2957
+ metric: { kind: 'conversionRate', column: 'completed' }, // per-variant success metric
2958
+ baseline: 'control', // which variant lift is measured against (default: the first)
2959
+ })
2960
+ ```
2961
+
2962
+ Metric kinds: `{ kind: 'count' }`, `{ kind: 'sum', column }`, `{ kind: 'avg', column }`, and `{ kind: 'conversionRate', column, equals? }` (converted iff `row[column]` is truthy, or `=== equals` when given). `avg` / `conversionRate` are per-subject rates that a lift reads honestly; `count` / `sum` reflect exposure too, so their cross-variant comparison only means "more/less total".
2963
+
2964
+ Observe the live result with `useExperiment`, or off `ExperimentRegistry`:
2965
+
2966
+ ```ts
2967
+ import { Effect } from 'effect'
2968
+ import { useExperiment } from '@voltro/runtime'
2969
+ import checkoutColor from '../experiments/checkoutColor.experiment'
2970
+
2971
+ export default (input, _ctx) =>
2972
+ Effect.gen(function* () {
2973
+ const result = yield* useExperiment(checkoutColor)
2974
+ // result: { totalExposure, baseline, variants: [{ variant, isBaseline, isHoldout,
2975
+ // exposure, metric, lift, diff }, ...], lastUpdatedAt, ... } | null
2976
+ return { arms: result?.variants ?? [] }
2977
+ })
2978
+ ```
2979
+
2980
+ Each variant row carries its `exposure` (sample size), the maintained `metric`, and — for non-baseline arms — `lift` (`(metric − baseMetric) / baseMetric`) and `diff` (absolute). `ExperimentRegistry` exposes `snapshot()`, `get(name)`, and `subscribe(listener)` for the live recompute stream that a results view redraws from. The same-subject-same-variant assignment is a pure function (`assignVariant(def, subject)`) a client can reproduce.
2981
+
2700
2982
 
2701
2983
 
2702
2984
  ---
@@ -658,6 +658,53 @@ export const users = table('users', {
658
658
 
659
659
  The cleanup is a separate PR after the migration has rolled out. Don't mix the rollout PR with the cleanup PR — the markers ARE the migration's audit trail until it's applied everywhere.
660
660
 
661
+ ## `voltro evolve` — the schema-evolution copilot
662
+
663
+ Adding the `.renamedFrom()` marker by hand is easy for one column. The hard part of changing an EXISTING schema is the *rest*: which handlers read or write that column, whether a rename is safe or needs a backfill, and getting the annotation onto the entity AND every call site without missing one. `voltro evolve` does that reconnaissance and proposes a reviewable plan.
664
+
665
+ Given a change, it reads the **observed graph** (`app.graph.observed.generated.json`, recorded by `voltro dev` — see [`voltro check`](/docs/cli/inspect)) plus the declared manifest, enumerates the real blast radius, and prints three things: a proposed **codemod**, a branch-verified **backfill** plan, and a `voltro check` **verify** step.
666
+
667
+ ```bash
668
+ voltro evolve rename-column notes.title --to heading # dry-run: plan + codemod preview
669
+ voltro evolve rename-column notes.title --to heading --write # apply the codemod
670
+ voltro evolve retype-column orders.total --to numeric # reshape → manual codemod + steps
671
+ voltro evolve split-column users.name --into firstName,lastName
672
+ voltro evolve rename-table note --to notes
673
+ voltro evolve drop-column notes.legacy
674
+ voltro evolve rename-column notes.title --to heading --json # for CI / an agent loop
675
+ ```
676
+
677
+ It is **dry-run by default** (mirrors `voltro generate`); `--write` applies the codemod through the same `runCodemods` toolkit as `voltro update`. `--json` emits the whole plan for CI or an agent.
678
+
679
+ ### The blast radius is observed, not guessed
680
+
681
+ ```text
682
+ change: rename-column notes.title → heading
683
+
684
+ blast radius (observed + declared):
685
+ query(notes.list) — observed read
686
+ mutation(notes.update) — observed update
687
+ ⚠ 1 declared but NEVER exercised — column use UNKNOWN: notes.archive
688
+
689
+ codemod:
690
+ entity: rename 'notes.title' → 'heading' and add .renamedFrom('title') (catalog rename — data preserved)
691
+ annotate 2 handler site(s) that reference the old field
692
+
693
+ backfill (dry-runs on branch notes-pr-0):
694
+ [safe] catalog RENAME — no data movement
695
+
696
+ verify: voltro check
697
+ ```
698
+
699
+ A handler that a `voltro dev` session or a test actually ran is reported with what it did (`observed read` / `observed update`). A handler that is *declared* to touch the table but was **never exercised** is flagged `UNKNOWN` and listed separately — it is never folded into "safe", because no run proves what it does with the column. That honesty is the point: the tool tells you exactly where it cannot vouch for the change.
700
+
701
+ ### What the codemod does per kind
702
+
703
+ - **`rename-column`** gets a real `transform`: it renames the field in the `*.entity.ts` AND chains **`.renamedFrom('old')`** (so the differ plans a catalog RENAME, not the lossy drop+create described [above](#renamedfromoldname)), then annotates the handler sites the blast radius found.
704
+ - **`retype-column` / `split-column` / `drop-column` / `rename-table`** are reshaping changes with no single mechanical rewrite, so they get a **`manual`** codemod: a generated, numbered checklist of the edits + the annotation to add, printed for you to apply.
705
+
706
+ `voltro evolve` produces the plan; it does not apply the schema change. **`voltro check` is the gate on the result**, and `voltro db apply` lands it — after `--write`, review the annotated handlers, then run those two.
707
+
661
708
 
662
709
 
663
710
  ---