@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
@@ -414,6 +414,29 @@ const choice = yield* step({
414
414
  })
415
415
  ```
416
416
 
417
+ ## Parallel steps
418
+
419
+ Steps with no data dependency run concurrently with plain `Effect.all` — no special API:
420
+
421
+ ```ts
422
+ const [jira, github] = yield* Effect.all(
423
+ [
424
+ step({ name: 'fetch-jira', success: JiraIssues, execute: fetchJira(projectKey) }),
425
+ step({ name: 'fetch-github', success: GithubPrs, execute: fetchGithub(repo) }),
426
+ ],
427
+ { concurrency: 'unbounded' },
428
+ )
429
+ ```
430
+
431
+ Both steps journal independently, and the durable guarantees hold across the join:
432
+
433
+ - The steps genuinely **overlap** — one is not secretly serialized behind the other.
434
+ - On a retry or an operator redrive, a parallel step that already **completed replays** from its journal; only the sibling that failed re-executes.
435
+
436
+ Both properties are pinned by a contract test against the real engine (`workflowParallelSteps.integration.test.ts`), so an engine upgrade that broke either would go red rather than quietly serializing your fan-out.
437
+
438
+ Name each parallel step distinctly — the name is the journal key, and two concurrent steps sharing one name would share one checkpoint. Deterministic-replay rules apply unchanged: the *set* of steps started must be stable for the same payload.
439
+
417
440
  ## Starting from the client
418
441
 
419
442
  Codegen synthesises an RPC for each workflow. To start it from React, use `useWorkflow(...)`; the call returns a run handle immediately and the durable work continues in the workflow engine:
@@ -680,9 +703,20 @@ The three recovery tools, and when each applies:
680
703
  | `retry` | The input was wrong, or the job is short + idempotent | Fresh execution, empty journal — every step runs again |
681
704
  | `resume` | The run is `suspended` (you set `suspendOnFailure: true`, or it awaits a signal) | Continues from the failure/suspend point — completed steps replay |
682
705
  | `redrive` | The run is `failed` (dead-letter) and re-running earlier steps is expensive/unsafe | Continues from the failure point — completed steps replay, failed steps re-run |
706
+ | `resume-from-step` | The run is `failed`, but the failure point is **not** the right recovery point — an earlier step ran on stale/wrong state | Rewinds to a chosen step — steps before it replay, that step + everything after re-run |
683
707
 
684
708
  `redrive` refuses a run that isn't a not-yet-discarded failure (use `resume` for a suspended run, `retry` for a fresh execution), and it needs a durable journal — on the memory store there is nothing to re-drive, so it declines cleanly (`redriven: false` with a reason) rather than pretending. It works under `voltro serve` as well as `voltro dev`, because dead-letter recovery happens in production. A re-drive records a `run-redriven` event on the run's timeline.
685
709
 
710
+ ### Rewind further back — `resume-from-step`
711
+
712
+ `redrive` continues from the step that **failed**. Sometimes that isn't the right place to restart: step 5 failed, but the real problem is that step 3 completed against stale external state, and re-running from the failure point would carry that bad result forward. `resume-from-step` rewinds to a step **you** choose:
713
+
714
+ ```sh
715
+ voltro workflows resume-from-step wf_01H... charge-card # rewind to 'charge-card' and re-run from there
716
+ ```
717
+
718
+ It resets the chosen step **and every step after it** — including steps that succeeded — so they re-execute, while the steps *before* your target replay from the journal unchanged. `redrive` is the special case where your chosen step is exactly the one that failed. Like `redrive`, it works under both `voltro serve` and `voltro dev`, refuses a run that isn't a not-yet-discarded failure, refuses an unknown step name, and declines cleanly on a run with no durable journal.
719
+
686
720
  ## Compensation
687
721
 
688
722
  For saga-style workflows, model compensation explicitly with `Effect.catchAll` around the step that can fail after an earlier side effect:
@@ -1105,6 +1139,8 @@ Use flow control when the workflow is durable, but the resource it touches is li
1105
1139
 
1106
1140
  Voltro exposes the workflow engine's durable queue and rate limiter through `@voltro/workflow`:
1107
1141
 
1142
+ > **This page is about primitives you call INSIDE a running workflow.** For declaring `debounce` / `singleton` / `concurrency` / `throttle` / `rateLimit` / `batch` on the workflow itself — enforced before a run exists — see [Declarative flow control](/docs/workflows/declarative-flow-control).
1143
+
1108
1144
  ```ts
1109
1145
  import {
1110
1146
  durableQueue,
@@ -1192,6 +1228,343 @@ Queue workers and rate-limited steps still record normal workflow steps, timers,
1192
1228
 
1193
1229
 
1194
1230
 
1231
+ ---
1232
+
1233
+ <!-- source: en/workflows/declarative-flow-control.md -->
1234
+ ## Declarative flow control
1235
+
1236
+ _debounce, singleton, concurrency, throttle, rateLimit, batch, priority, timeouts and onFailure — declared on the workflow, enforced before the run exists._
1237
+
1238
+ Everything on this page is declared on `workflow({...})` and enforced at the **admission boundary** — the moment `ctx.workflows.start(...)` is called, before a durable run exists.
1239
+
1240
+ That timing is the whole point. Once a run is enqueued, the only tools left are cancel and sleep, and neither of them un-spends the durable entity. So "run this at most once per row per fifteen minutes" cannot be a primitive you call inside the body; it has to be a property of the declaration.
1241
+
1242
+ > **Not the same as [Flow control](/docs/workflows/flow-control).** That page covers `durableQueue` / `processQueue` / `rateLimit` — primitives you call **inside** a running workflow to bound the work it fans out. This page is about whether the run **starts at all**. They compose: a workflow can declare `concurrency` here and still use a durable queue in its body.
1243
+
1244
+ ## The shape
1245
+
1246
+ ```ts
1247
+ import { Schema } from 'effect'
1248
+ import { workflow } from '@voltro/workflow/define'
1249
+
1250
+ export const tourNarration = workflow({
1251
+ name: 'tourNarration',
1252
+ payload: Schema.Struct({
1253
+ rowId: Schema.String,
1254
+ tenantId: Schema.String,
1255
+ editedAt: Schema.Number,
1256
+ }),
1257
+ success: Schema.Void,
1258
+ idempotencyKey: ({ rowId, editedAt }) => `tour:${rowId}:${editedAt}`,
1259
+
1260
+ debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes' },
1261
+ concurrency: { limit: 5, key: (p) => p.tenantId },
1262
+ timeouts: { start: '1 hour', finish: '10 minutes' },
1263
+ onFailure: 'narrationFailed',
1264
+ })
1265
+ ```
1266
+
1267
+ Every `key` callback receives the workflow's own **decoded payload type**. A misspelled field is a compile error, not a key that quietly becomes the string `"undefined"` and collapses every row in your deployment into one bucket.
1268
+
1269
+ ## `idempotencyKey` is the execution's identity — read this first
1270
+
1271
+ This is the single most expensive misunderstanding in the workflow API, and getting it wrong produces a design that looks right and silently stops working.
1272
+
1273
+ `idempotencyKey` is **not** a dedupe window. It is the execution's identity, permanently:
1274
+
1275
+ ```ts
1276
+ const a = yield* wf.execute({ id: 'same' })
1277
+ const b = yield* wf.execute({ id: 'same' }) // does NOT run — replays a's result
1278
+ ```
1279
+
1280
+ After the run completes the key is **spent**. A later, genuinely new invocation under that key is a silent no-op that returns the old output. Nothing errors and nothing logs, because from the engine's point of view you asked for a run it already has.
1281
+
1282
+ So a key must be unique **per unit of work you want to happen**:
1283
+
1284
+ | | |
1285
+ |---|---|
1286
+ | `` `tour:${rowId}` `` | wrong if the tour can ever be re-narrated |
1287
+ | `` `tour:${rowId}:${editedAt}` `` | right — every edit is a new unit of work |
1288
+
1289
+ ### And a flow-control key is a different thing
1290
+
1291
+ Conflating the two is what makes "I need to re-arm a key" feel like a missing feature. It is not missing; it is two fields:
1292
+
1293
+ - **`idempotencyKey`** — the execution's identity. **Varies** per unit of work.
1294
+ - **`debounce.key` / `singleton.key` / `concurrency.key`** — the **resource** runs compete for. **Stable**.
1295
+
1296
+ "One job, fifteen minutes after the last edit, latest state wins" is then the example at the top of this page: twenty edits mint twenty identities, and exactly one is ever admitted. There is no `restart: true` in this API because separating the two keys *is* the mechanism it would have been.
1297
+
1298
+ ## debounce — collapse a burst
1299
+
1300
+ ```ts
1301
+ debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes', timeout: '1 hour' }
1302
+ ```
1303
+
1304
+ Starts sharing a key collapse into **one** pending row. The timer resets on every arrival, and the **latest payload wins** — which is what "narrate what settled" means.
1305
+
1306
+ `timeout` is a hard cap measured from the **first** start in the burst. It is optional and uncapped when unset, which is a real trade: an unbroken stream of starts arriving faster than `period` defers the run forever. We do not invent a default cap and we do not warn — instead the starvation is a number you can see:
1307
+
1308
+ ```
1309
+ voltro workflows flow
1310
+ ```
1311
+
1312
+ A `waiting=` climbing past a few multiples of your `period` is the signal. Set `timeout` when you see it, or from the start if the burst is user-driven.
1313
+
1314
+ ## singleton — one run per resource
1315
+
1316
+ ```ts
1317
+ singleton: { key: (p) => p.tenantId, mode: 'skip' } // newcomer stands down
1318
+ singleton: { key: (p) => p.tenantId, mode: 'cancel' } // newcomer evicts the incumbent
1319
+ ```
1320
+
1321
+ There is no default `mode`: `'skip'` discards the incoming request and `'cancel'` discards the running one, and choosing for you would silently throw away work either way.
1322
+
1323
+ Under `'skip'`, `start()` returns the **incumbent's** handle — a real, pollable run. Under `'cancel'`, the incumbent is cancelled **when the replacement actually starts**, not when it is queued. That matters when you also declare `debounce`: evicting at queue time would leave the whole quiet period with the old run dead and the new one not yet begun.
1324
+
1325
+ ## concurrency — bound what is in flight
1326
+
1327
+ ```ts
1328
+ concurrency: { limit: 5, key: (p) => p.tenantId }
1329
+ ```
1330
+
1331
+ At most `limit` runs in flight per key, **across every replica** — the count rides the shared admissions ledger, so three replicas with `limit: 5` are five runs, not fifteen. Excess starts queue and are admitted as slots free.
1332
+
1333
+ ### `pool` — share one budget across workflows
1334
+
1335
+ ```ts
1336
+ // embeddings.workflow.tsx
1337
+ concurrency: { limit: 10, pool: 'openai' }
1338
+ // summarize.workflow.tsx
1339
+ concurrency: { limit: 10, pool: 'openai' }
1340
+ ```
1341
+
1342
+ Without `pool`, the limit bounds *this workflow's* runs. With it, every workflow declaring the same pool name competes for **one** budget — the shape a rate-limited provider forces: five workflows that each call OpenAI must share ten slots, not hold ten each.
1343
+
1344
+ `key` still partitions *within* the pool: give each member `key: (p) => p.tenantId` and the shared budget applies per tenant.
1345
+
1346
+ Every member of a pool must declare the **same `limit`** — the boot fails otherwise. Two limits for one budget is a contradiction, and silently picking either would enforce a number somebody did not write.
1347
+
1348
+ ## throttle vs rateLimit — late, or gone
1349
+
1350
+ They are mutually exclusive, and declaring both is a boot error.
1351
+
1352
+ ```ts
1353
+ throttle: { limit: 100, period: '1 minute', key: (p) => p.tenantId } // QUEUES the excess
1354
+ rateLimit: { limit: 100, period: '1 minute', key: (p) => p.tenantId } // DROPS the excess
1355
+ ```
1356
+
1357
+ Reach for `throttle` when every start must eventually run. Reach for `rateLimit` when the excess is genuinely surplus and running it late is worse than not running it.
1358
+
1359
+ A dropped start is never silent: `start()` resolves to a handle with `status: 'dropped'` and a `retryAfterMs`, and the drop is a row in the admissions ledger with its key and reason.
1360
+
1361
+ `throttle` has no `burst` knob. The window is sliding, so its maximum instantaneous burst is already exactly `limit`; a separate knob could only duplicate it.
1362
+
1363
+ ## batch — many starts, one run
1364
+
1365
+ ```ts
1366
+ export const refreshIssues = workflow({
1367
+ name: 'refreshIssues',
1368
+ payload: Schema.Struct({ items: Schema.Array(IssueRef) }),
1369
+ success: Schema.Void,
1370
+ idempotencyKey: ({ items }) => `refresh:${items.length}:${items[0]?.issueKey ?? ''}`,
1371
+ batch: { item: IssueRef, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' },
1372
+ })
1373
+
1374
+ await ctx.workflows.start('refreshIssues', { tenantId, issueKey: 'ABC-1' })
1375
+ ```
1376
+
1377
+ Callers start it with a **single item**; the workflow's own `payload` is the **batch** shape. That mismatch is checked at declaration time — a decode failure on the batching replica is a failure nobody is watching.
1378
+
1379
+ The timeout is a **deadline**, not a quiet period: it does not reset per item, or a steady trickle would never flush. `batch` and `debounce` cannot both be declared for exactly that reason.
1380
+
1381
+ ## priority
1382
+
1383
+ ```ts
1384
+ priority: (p) => (p.urgent ? 100 : 0)
1385
+ ```
1386
+
1387
+ Higher runs first out of the pending queue. Ties break by arrival, so an all-default deployment is FIFO rather than dialect-dependent.
1388
+
1389
+ ## timeouts
1390
+
1391
+ ```ts
1392
+ timeouts: { start: '1 hour', finish: '10 minutes' }
1393
+ ```
1394
+
1395
+ - **`start`** bounds how long a start may sit in the admission queue, measured from the **first** arrival in its group. A debounced run that never gets a quiet moment is a job that silently did not happen.
1396
+ - **`finish`** bounds the run itself once admitted, and also tightens the crash backstop on its concurrency slot.
1397
+
1398
+ Both expire into the same path as an exhausted retry: the run is recorded failed and `onFailure` fires. Declaring `timeouts.start` on a workflow that cannot defer is a boot error — it would never fire.
1399
+
1400
+ ## onFailure — the signal that replaces the sweep
1401
+
1402
+ ```ts
1403
+ onFailure: 'narrationFailed'
1404
+ ```
1405
+
1406
+ A **workflow name**, not a function. A closure cannot be journaled: the failure may be noticed by a different replica, minutes later, after the process that held it is gone.
1407
+
1408
+ It fires for every way a run fails to deliver — not only an exhausted retry:
1409
+
1410
+ - the body failed and retries are spent
1411
+ - `timeouts.finish` cancelled an overrunning run
1412
+ - `timeouts.start` expired a start that never got a slot
1413
+ - the workflow was renamed away while starts were queued
1414
+
1415
+ The last two produce **no run row at all**, which is exactly why polling `listRuns({ status: 'failed' })` could never see them.
1416
+
1417
+ The named workflow's payload is `WorkflowFailureReport`:
1418
+
1419
+ ```ts
1420
+ export const narrationFailed = workflow({
1421
+ name: 'narrationFailed',
1422
+ payload: Schema.Struct({
1423
+ workflow: Schema.String,
1424
+ payload: Schema.Unknown,
1425
+ errorTag: Schema.NullOr(Schema.String),
1426
+ errorMessage: Schema.NullOr(Schema.String),
1427
+ runId: Schema.NullOr(Schema.String),
1428
+ executionId: Schema.NullOr(Schema.String),
1429
+ reason: Schema.String,
1430
+ failedAt: Schema.Number,
1431
+ }),
1432
+ success: Schema.Void,
1433
+ idempotencyKey: ({ runId, failedAt }) => `failed:${runId ?? 'none'}:${failedAt}`,
1434
+ })
1435
+ ```
1436
+
1437
+ There is no `onFailure` for an `onFailure` — a handler that fails is logged and not re-notified, because the alternative is one run per failure per level with no floor.
1438
+
1439
+ ## encryptSteps
1440
+
1441
+ ```ts
1442
+ encryptSteps: true
1443
+ ```
1444
+
1445
+ `step({ input })` is journaled and shown in the dashboard, which is a feature and the reason people pass rich input. For a step carrying personal data it is also a **second copy** outside the `.encrypted()` boundary the governance plugin establishes for tables.
1446
+
1447
+ `encryptSteps` closes it, reusing the **same** cipher — one key, one rotation story:
1448
+
1449
+ ```ts
1450
+ governancePlugin({ fieldEncryption: { secret: 'VOLTRO_FIELD_ENCRYPTION_KEY' } })
1451
+ ```
1452
+
1453
+ Declaring it without that plugin configured is a **boot refusal**, not a warning. A plaintext fallback would leave the declaration reading as protection while every step input sat readable.
1454
+
1455
+ ## cancelOn — stop live work when a correlated event arrives
1456
+
1457
+ ```ts
1458
+ workflow({
1459
+ name: 'tourNarration',
1460
+ payload: { rowId: Schema.String, issueKey: Schema.String },
1461
+ idempotencyKey: (p) => `tour:${p.rowId}`,
1462
+ cancelOn: [{
1463
+ event: 'jira.issue.deleted',
1464
+ schema: JiraIssueDeleted,
1465
+ match: (event, payload) => event.issueKey === payload.issueKey,
1466
+ }],
1467
+ })
1468
+ ```
1469
+
1470
+ Both sides are typed: `event` from the entry's own `schema`, `payload` from the workflow's.
1471
+
1472
+ **Why it is a declaration and not a race inside the body.** You can express "stop when the issue is deleted" with `awaitEvent` and an interrupt. That works while the body is *running*. It does not work while the run is sleeping for six hours, suspended on a signal, or still sitting in the admission queue — which is the case you wanted cancellation for. The event has to reach a run whose fiber is not executing anything, and only something outside the body can do that.
1473
+
1474
+ So it is swept: a coordinated tick reads events published since a durable watermark, resolves each declaring workflow's live runs, and cancels the ones that correlate.
1475
+
1476
+ **It also discards queued starts.** Cancelling only the running one leaves a debounced or concurrency-queued duplicate to start seconds later, against the row that was just deleted — the exact outcome the declaration was meant to prevent, arriving late enough that nobody connects the two.
1477
+
1478
+ | Field | Meaning |
1479
+ |---|---|
1480
+ | `event` | The name, exactly as `ctx.events.publish` writes it |
1481
+ | `schema` | Decoded before `match` runs. An event whose shape does not decode is **reported and never matched** — cancelling on an event you could not read is cancelling blind |
1482
+ | `match` | Required. Write `match: () => true` if you really mean "every live run" |
1483
+ | `within` | Only cancel runs started within this window before the event |
1484
+ | `reason` | Recorded on the run's `run-cancelled` event; defaults to `cancelOn:<event>` |
1485
+
1486
+ `match` has **no default** for the same reason `singleton.mode` has none: the omitted case is "cancel every live run of this workflow", which is a legitimate thing to want and a catastrophic thing to acquire by forgetting a line.
1487
+
1488
+ **A run that started *after* the event is never cancelled.** Without that rule, a sweep catching up after a deployment gap reads an hour of history and kills runs that started in the meantime — and the symptom (fresh work cancelled for no visible reason) looks nothing like its cause (a restart).
1489
+
1490
+ ## Bulk cancel and bulk replay
1491
+
1492
+ A bad deploy leaves four thousand runs that must all stop, or four thousand that must all be re-driven once the downstream is fixed.
1493
+
1494
+ ```
1495
+ voltro workflows cancel-many --workflow tourNarration --reason "bad deploy"
1496
+ voltro workflows cancel-many --workflow tourNarration --reason "bad deploy" --commit
1497
+ voltro workflows replay-many --status failed --mode redrive --limit 200 --commit
1498
+ ```
1499
+
1500
+ Three things are deliberately stricter than the obvious design:
1501
+
1502
+ - **`--limit` is required and there is no "all".** The cap *is* the blast radius. `truncated` in the result says whether more matched, so "did I get all of them" stays answerable without an unbounded verb ever existing.
1503
+ - **It is a dry run unless you pass `--commit`.** The default for a verb that can stop a thousand runs is the one that stops none. The dashboard panel enforces the same order: the apply button does not exist until a preview has returned a number.
1504
+ - **`--reason` is required for a cancel.** It lands on every affected run's `run-cancelled` event, so "why did four thousand runs stop on the 8th" has an answer in the table an operator is already reading.
1505
+
1506
+ The result is per-run, not a count: `succeeded`, `failed` (with the reason for each) and `skipped` (with what made each ineligible) are three different outcomes. A bulk op that reports "4000 cancelled" while forty failed is how people learn not to trust bulk ops.
1507
+
1508
+ Eligibility is fixed by the verb: a cancel acts on `running` and `suspended` runs; `replay --mode redrive` on `failed` only (redrive resumes from the step that died, which only exists for a failure); `replay --mode retry` on `failed` and `cancelled`. `--mode` has no default because the two cost very different amounts.
1509
+
1510
+ ## What a deferred start returns
1511
+
1512
+ `start()` no longer always returns a running handle:
1513
+
1514
+ ```ts
1515
+ const handle = await ctx.workflows.start('tourNarration', payload)
1516
+
1517
+ if (handle.status === 'queued') { /* handle.deferral.dueAt tells you when */ }
1518
+ if (handle.status === 'dropped') { /* over a rateLimit cap; it will NOT run */ }
1519
+ if (handle.status === 'skipped') { /* handle.executionId is the incumbent */ }
1520
+ ```
1521
+
1522
+ `executionId` is `null` for `queued` and `dropped`, because there is no execution and there may never be one. Inventing an id there would produce a handle that polls `status: 'unknown'` forever.
1523
+
1524
+ ### Blocking callers
1525
+
1526
+ `ctx.workflows.run(...)` and `start({ wait: true })` block for the run's **result**, and a start that was collapsed into a future run has none. A workflow declaring `debounce` / `batch` / `throttle` / `concurrency` therefore **refuses** those callers with an error naming both halves. Controls with a synchronous answer — `singleton`, `rateLimit` — keep working on every path (`rateLimit` throws `WorkflowRateLimitedError`, `singleton: 'skip'` throws `WorkflowSingletonHeldError` carrying the incumbent's id).
1527
+
1528
+ ## Pausing a workflow
1529
+
1530
+ ```
1531
+ voltro workflows pause tourNarration --reason "deploying a fix"
1532
+ voltro workflows unpause tourNarration
1533
+ ```
1534
+
1535
+ A pause makes starts **collect**, never discard — so you come back to a backlog rather than a hole in the data. `unpause` drains it.
1536
+
1537
+ The pause is a row, so it applies fleet-wide; each replica picks it up on its next drain tick (~1 s).
1538
+
1539
+ ## Seeing what happened
1540
+
1541
+ ```
1542
+ voltro workflows flow
1543
+ voltro workflows flow --workflow tourNarration --format json
1544
+ ```
1545
+
1546
+ or `GET /_voltro/inspect/workflows/flow-control`.
1547
+
1548
+ This is not optional colour. A debounce that collapses nineteen starts into one is indisputably correct behaviour **and** indistinguishable from nineteen starts vanishing — unless something writes down that it happened. So every decision is a row in `_voltro_workflow_admissions`, with its key, its reason, how many starts folded into it, and how long it waited.
1549
+
1550
+ Three questions it answers:
1551
+
1552
+ | Question | Where |
1553
+ |---|---|
1554
+ | "Twenty edits, one run — did that work, or did I lose nineteen?" | `collapsed` |
1555
+ | "Nothing has run for an hour. Stuck, or quiet?" | `waiting=` on the queued row |
1556
+ | "Why did *my* run not start?" | the ledger's `outcome` + `reason` |
1557
+
1558
+ ## Cost
1559
+
1560
+ A workflow that declares **no** control takes exactly the code path it took before this feature existed — no query, no branch beyond one map lookup.
1561
+
1562
+ A workflow that declares one pays only for that one: an undeclared control costs zero round trips. The rate/throttle window reads at most `limit` rows, which is why `limit` is a throughput knob and not somewhere to put 10⁶.
1563
+
1564
+ The drainer runs on **one replica per tick** through the same claim arbiter the cron scheduler uses. N replicas draining at once would each see a free slot and each take it.
1565
+
1566
+
1567
+
1195
1568
  ---
1196
1569
 
1197
1570
  <!-- source: en/workflows/versioning.md -->
@@ -1303,6 +1676,9 @@ queued/running/waiting work by workflow lane and start source.
1303
1676
  voltro workflows list --status running --tail 50
1304
1677
  voltro workflows list --tag notes.summarise --format json
1305
1678
  voltro workflows list --dead-letter # failed runs not yet discarded
1679
+ voltro workflows list --statuses failed,cancelled --q orders --since 2026-08-01T00:00:00Z
1680
+ voltro workflows list --id-prefix wfrun_01K # matches the run id OR the execution id
1681
+ voltro workflows stats --hours 24 # bucketed activity sparkline + per-workflow totals
1306
1682
  voltro workflows start notes.summarise --payload '{"noteId":"note_123"}'
1307
1683
  voltro workflows show wfrun_01H...
1308
1684
  voltro workflows retry wfrun_01H...
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.97.0",
14
14
  "@effect/rpc": "^0.76.0",
15
- "@voltro/ai": "0.28.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/protocol": "0.28.0",
20
- "@voltro/runtime": "0.28.0",
15
+ "@voltro/ai": "0.30.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/protocol": "0.30.0",
20
+ "@voltro/runtime": "0.30.0",
21
21
  "effect": "^3.22.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.28.0",
24
+ "@voltro/testing": "0.30.0",
25
25
  "typescript": "^6.0.3",
26
26
  "vitest": "^4.1.10"
27
27
  }
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-auth": "0.28.0",
20
- "@voltro/protocol": "0.28.0",
21
- "@voltro/runtime": "0.28.0",
22
- "@voltro/sql-postgres": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-auth": "0.30.0",
20
+ "@voltro/protocol": "0.30.0",
21
+ "@voltro/runtime": "0.30.0",
22
+ "@voltro/sql-postgres": "0.30.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.28.0",
26
+ "@voltro/testing": "0.30.0",
27
27
  "typescript": "^6.0.3",
28
28
  "vitest": "^4.1.10"
29
29
  }
@@ -13,16 +13,16 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-multitenancy": "0.28.0",
20
- "@voltro/protocol": "0.28.0",
21
- "@voltro/runtime": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-multitenancy": "0.30.0",
20
+ "@voltro/protocol": "0.30.0",
21
+ "@voltro/runtime": "0.30.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.28.0",
25
+ "@voltro/testing": "0.30.0",
26
26
  "typescript": "^6.0.3",
27
27
  "vitest": "^4.1.10"
28
28
  }
@@ -13,16 +13,16 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-deactivation": "0.28.0",
20
- "@voltro/protocol": "0.28.0",
21
- "@voltro/runtime": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-deactivation": "0.30.0",
20
+ "@voltro/protocol": "0.30.0",
21
+ "@voltro/runtime": "0.30.0",
22
22
  "effect": "^3.22.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.28.0",
25
+ "@voltro/testing": "0.30.0",
26
26
  "typescript": "^6.0.3",
27
27
  "vitest": "^4.1.10"
28
28
  }
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@react-email/components": "^1.0.12",
15
15
  "@react-email/render": "^1.4.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-mail": "0.28.0",
20
- "@voltro/plugin-multitenancy": "0.28.0",
21
- "@voltro/protocol": "0.28.0",
22
- "@voltro/runtime": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-mail": "0.30.0",
20
+ "@voltro/plugin-multitenancy": "0.30.0",
21
+ "@voltro/protocol": "0.30.0",
22
+ "@voltro/runtime": "0.30.0",
23
23
  "effect": "^3.22.0",
24
24
  "react": "^19.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.28.0",
27
+ "@voltro/testing": "0.30.0",
28
28
  "typescript": "^6.0.3",
29
29
  "vitest": "^4.1.10"
30
30
  }
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/env": "0.28.0",
19
- "@voltro/plugin-multitenancy": "0.28.0",
20
- "@voltro/plugin-storage": "0.28.0",
21
- "@voltro/protocol": "0.28.0",
22
- "@voltro/runtime": "0.28.0",
23
- "@voltro/sql-mysql": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-multitenancy": "0.30.0",
20
+ "@voltro/plugin-storage": "0.30.0",
21
+ "@voltro/protocol": "0.30.0",
22
+ "@voltro/runtime": "0.30.0",
23
+ "@voltro/sql-mysql": "0.30.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.28.0",
27
+ "@voltro/testing": "0.30.0",
28
28
  "typescript": "^6.0.3",
29
29
  "vitest": "^4.1.10"
30
30
  }
@@ -0,0 +1,19 @@
1
+ # {{capProjectName}} — SQLite backend env. Copy to `.env` and adjust.
2
+
3
+ # ── Database (embedded SQLite) ─────────────────────────────────────────
4
+ DB_DIALECT=sqlite
5
+ # A file path keeps data across restarts (the `.data/` dir is gitignored):
6
+ DB_URL=file:./.data/{{projectNameSnake}}.sqlite
7
+ # Or an ephemeral, in-process DB that vanishes on exit (great for tests):
8
+ # DB_URL=:memory:
9
+
10
+ # Session signing secret — NO VALUE SHIPS HERE ON PURPOSE.
11
+ #
12
+ # `voltro dev` mints a unique one for this project into a gitignored
13
+ # .env.local on first boot, so local development needs nothing from you.
14
+ # A shipped placeholder would be a signing key published to everyone who
15
+ # downloads this template, making every session in your deployment forgeable.
16
+ #
17
+ # Your DEPLOYMENT needs its own — `voltro serve` refuses to start without it:
18
+ # voltro secret generate session
19
+ VOLTRO_SESSION_SECRET=
@@ -0,0 +1,38 @@
1
+ # {{projectName}} / {{appName}}
2
+
3
+ Voltro backend scaffold (template: **api-backend-sqlite**) — an embedded,
4
+ single-process **SQLite** store. No database server to run; one native
5
+ dependency (`better-sqlite3`, via `@voltro/sql-sqlite`).
6
+
7
+ ## Boot
8
+
9
+ ```bash
10
+ pnpm install # at the repo root
11
+ pnpm --filter @{{projectName}}/{{appName}} dev
12
+ # → http://localhost:4000
13
+ # → ws://localhost:4000/ws
14
+ ```
15
+
16
+ The database file is created on first boot at the `DB_URL` path
17
+ (`.env.example` defaults to `file:./.data/{{projectNameSnake}}.sqlite`, which is
18
+ gitignored). Use `DB_URL=:memory:` for an ephemeral, in-process database.
19
+
20
+ ## What's in here
21
+
22
+ | File | Role |
23
+ |---|---|
24
+ | `app.config.ts` | App declaration (`store: 'sqlite'`); `voltro dev` reads this first. |
25
+ | `.env.example` | `DB_DIALECT` + `DB_URL` — copy to `.env`. |
26
+ | `database/schema.ts` | One example table (`notes`) with the `tenant()` mixin. |
27
+ | `queries/notes.query.ts` | Streaming subscription example. |
28
+ | `mutations/notes.create.mutation.ts` | Tenant-guarded mutation example. |
29
+
30
+ Drop more `*.query.ts`, `*.mutation.ts`, `*.action.ts`, or `*.workflow.tsx`
31
+ files anywhere in this tree — discovery is by file convention.
32
+
33
+ ## When to reach for something else
34
+
35
+ SQLite is single-writer and single-process, so real-time subscriptions do not
36
+ fan out across replicas (there is no CDC bus — within the one process they work
37
+ as normal). Scale to `store: 'postgres'` / `'mysql'` / `'mariadb'` when you run
38
+ more than one replica.