@voltro/cli 0.1.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 (626) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +57 -0
  3. package/README.md +26 -0
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +20236 -0
  6. package/bin/voltro.mjs +43 -0
  7. package/dist/bin.d.ts +1 -0
  8. package/dist/bin.js +9 -0
  9. package/dist/commands-CfPH2Wf4.js +18061 -0
  10. package/dist/frameworkInspectState-CX2250XB.js +86 -0
  11. package/dist/index.d.ts +48 -0
  12. package/dist/index.js +5 -0
  13. package/dist/inspectState.d.ts +116 -0
  14. package/dist/inspectState.js +2 -0
  15. package/dist/startup.d.ts +26 -0
  16. package/dist/startup.js +2 -0
  17. package/dist/startupRunner-CRhuUl91.js +71 -0
  18. package/package.json +88 -0
  19. package/templates/AGENTS.core.md +258 -0
  20. package/templates/AGENTS.md +351 -0
  21. package/templates/agent-docs/_index.md +93 -0
  22. package/templates/agent-docs/_manifest.json +655 -0
  23. package/templates/agent-docs/ai.md +1845 -0
  24. package/templates/agent-docs/authentication.md +1788 -0
  25. package/templates/agent-docs/caching.md +624 -0
  26. package/templates/agent-docs/cli.md +1650 -0
  27. package/templates/agent-docs/configuration.md +295 -0
  28. package/templates/agent-docs/data.md +2172 -0
  29. package/templates/agent-docs/database/advancedqueries.md +1583 -0
  30. package/templates/agent-docs/database/columntypes.md +1200 -0
  31. package/templates/agent-docs/database/hosting.md +881 -0
  32. package/templates/agent-docs/database/migrations.md +2938 -0
  33. package/templates/agent-docs/database/misc.md +270 -0
  34. package/templates/agent-docs/database/overview.md +108 -0
  35. package/templates/agent-docs/database/querying.md +1622 -0
  36. package/templates/agent-docs/database/scaling.md +331 -0
  37. package/templates/agent-docs/database/schema.md +1458 -0
  38. package/templates/agent-docs/database/seedsdialects.md +1235 -0
  39. package/templates/agent-docs/database/transactions.md +285 -0
  40. package/templates/agent-docs/deployment.md +999 -0
  41. package/templates/agent-docs/internationalization.md +359 -0
  42. package/templates/agent-docs/introduction.md +438 -0
  43. package/templates/agent-docs/multi-tenancy.md +610 -0
  44. package/templates/agent-docs/observability.md +350 -0
  45. package/templates/agent-docs/plugins.md +1273 -0
  46. package/templates/agent-docs/reference.md +978 -0
  47. package/templates/agent-docs/routing.md +1553 -0
  48. package/templates/agent-docs/scheduling.md +666 -0
  49. package/templates/agent-docs/schema-driven-ui.md +607 -0
  50. package/templates/agent-docs/security.md +42 -0
  51. package/templates/agent-docs/templates/apibackends.md +3214 -0
  52. package/templates/agent-docs/templates/appshells.md +2062 -0
  53. package/templates/agent-docs/templates/custom.md +128 -0
  54. package/templates/agent-docs/templates/overview.md +122 -0
  55. package/templates/agent-docs/templates/serverless.md +315 -0
  56. package/templates/agent-docs/testing.md +376 -0
  57. package/templates/agent-docs/workflows.md +1351 -0
  58. package/templates/apps/api-ai/README.md +105 -0
  59. package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
  60. package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
  61. package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
  62. package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
  63. package/templates/apps/api-ai/app.config.ts +57 -0
  64. package/templates/apps/api-ai/database/schema.ts +67 -0
  65. package/templates/apps/api-ai/package.json +27 -0
  66. package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
  67. package/templates/apps/api-ai/template.json +6 -0
  68. package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
  69. package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
  70. package/templates/apps/api-ai/tsconfig.json +5 -0
  71. package/templates/apps/api-auth/.env +17 -0
  72. package/templates/apps/api-auth/README.md +109 -0
  73. package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
  74. package/templates/apps/api-auth/actions/me.action.ts +19 -0
  75. package/templates/apps/api-auth/app.config.ts +59 -0
  76. package/templates/apps/api-auth/database/schema.ts +26 -0
  77. package/templates/apps/api-auth/package.json +28 -0
  78. package/templates/apps/api-auth/template.json +6 -0
  79. package/templates/apps/api-auth/tests/me.test.ts +37 -0
  80. package/templates/apps/api-auth/tsconfig.json +5 -0
  81. package/templates/apps/api-backend/README.md +25 -0
  82. package/templates/apps/api-backend/app.config.ts +39 -0
  83. package/templates/apps/api-backend/database/schema.ts +57 -0
  84. package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
  85. package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
  86. package/templates/apps/api-backend/package.json +28 -0
  87. package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
  88. package/templates/apps/api-backend/queries/notes.query.ts +20 -0
  89. package/templates/apps/api-backend/template.json +6 -0
  90. package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
  91. package/templates/apps/api-backend/tsconfig.json +5 -0
  92. package/templates/apps/api-backend-deactivation/README.md +49 -0
  93. package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
  94. package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
  95. package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
  96. package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
  97. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
  98. package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
  99. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
  100. package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
  101. package/templates/apps/api-backend-deactivation/package.json +28 -0
  102. package/templates/apps/api-backend-deactivation/template.json +6 -0
  103. package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
  104. package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
  105. package/templates/apps/api-backend-mail/README.md +39 -0
  106. package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
  107. package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
  108. package/templates/apps/api-backend-mail/app.config.ts +34 -0
  109. package/templates/apps/api-backend-mail/database/schema.ts +57 -0
  110. package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
  111. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
  112. package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
  113. package/templates/apps/api-backend-mail/package.json +30 -0
  114. package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
  115. package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
  116. package/templates/apps/api-backend-mail/template.json +6 -0
  117. package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
  118. package/templates/apps/api-backend-mail/tsconfig.json +5 -0
  119. package/templates/apps/api-backend-mariadb/.env.example +35 -0
  120. package/templates/apps/api-backend-mariadb/README.md +31 -0
  121. package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
  122. package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
  123. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
  124. package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
  125. package/templates/apps/api-backend-mariadb/package.json +30 -0
  126. package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
  127. package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
  128. package/templates/apps/api-backend-mariadb/template.json +6 -0
  129. package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
  130. package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
  131. package/templates/apps/api-backend-storage/README.md +86 -0
  132. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
  133. package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
  134. package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
  135. package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
  136. package/templates/apps/api-backend-storage/app.config.ts +38 -0
  137. package/templates/apps/api-backend-storage/database/schema.ts +57 -0
  138. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
  139. package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
  140. package/templates/apps/api-backend-storage/package.json +27 -0
  141. package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
  142. package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
  143. package/templates/apps/api-backend-storage/template.json +6 -0
  144. package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
  145. package/templates/apps/api-backend-storage/tsconfig.json +5 -0
  146. package/templates/apps/api-data-advanced/.env +17 -0
  147. package/templates/apps/api-data-advanced/README.md +88 -0
  148. package/templates/apps/api-data-advanced/app.config.ts +56 -0
  149. package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
  150. package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
  151. package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
  152. package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
  153. package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
  154. package/templates/apps/api-data-advanced/database/index.ts +23 -0
  155. package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
  156. package/templates/apps/api-data-advanced/package.json +28 -0
  157. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
  158. package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
  159. package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
  160. package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
  161. package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
  162. package/templates/apps/api-data-advanced/template.json +6 -0
  163. package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
  164. package/templates/apps/api-data-advanced/tsconfig.json +5 -0
  165. package/templates/apps/api-durable/README.md +81 -0
  166. package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
  167. package/templates/apps/api-durable/app.config.ts +41 -0
  168. package/templates/apps/api-durable/database/schema.ts +65 -0
  169. package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
  170. package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
  171. package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
  172. package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
  173. package/templates/apps/api-durable/package.json +28 -0
  174. package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
  175. package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
  176. package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
  177. package/templates/apps/api-durable/template.json +6 -0
  178. package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
  179. package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
  180. package/templates/apps/api-durable/tsconfig.json +5 -0
  181. package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
  182. package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
  183. package/templates/apps/api-feature-flags/README.md +63 -0
  184. package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
  185. package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
  186. package/templates/apps/api-feature-flags/app.config.ts +41 -0
  187. package/templates/apps/api-feature-flags/database/schema.ts +37 -0
  188. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
  189. package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
  190. package/templates/apps/api-feature-flags/package.json +29 -0
  191. package/templates/apps/api-feature-flags/template.json +6 -0
  192. package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
  193. package/templates/apps/api-feature-flags/tsconfig.json +5 -0
  194. package/templates/apps/api-governance/.env +4 -0
  195. package/templates/apps/api-governance/README.md +61 -0
  196. package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
  197. package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
  198. package/templates/apps/api-governance/app.config.ts +35 -0
  199. package/templates/apps/api-governance/database/schema.ts +41 -0
  200. package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
  201. package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
  202. package/templates/apps/api-governance/package.json +29 -0
  203. package/templates/apps/api-governance/template.json +6 -0
  204. package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
  205. package/templates/apps/api-governance/tsconfig.json +5 -0
  206. package/templates/apps/api-kv/README.md +100 -0
  207. package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
  208. package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
  209. package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
  210. package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
  211. package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
  212. package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
  213. package/templates/apps/api-kv/app.config.ts +48 -0
  214. package/templates/apps/api-kv/database/schema.ts +62 -0
  215. package/templates/apps/api-kv/package.json +28 -0
  216. package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
  217. package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
  218. package/templates/apps/api-kv/template.json +6 -0
  219. package/templates/apps/api-kv/tests/sync.test.ts +103 -0
  220. package/templates/apps/api-kv/tsconfig.json +5 -0
  221. package/templates/apps/api-moderation/README.md +46 -0
  222. package/templates/apps/api-moderation/app.config.ts +33 -0
  223. package/templates/apps/api-moderation/database/schema.ts +45 -0
  224. package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
  225. package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
  226. package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
  227. package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
  228. package/templates/apps/api-moderation/package.json +29 -0
  229. package/templates/apps/api-moderation/template.json +6 -0
  230. package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
  231. package/templates/apps/api-moderation/tsconfig.json +5 -0
  232. package/templates/apps/api-observability/README.md +106 -0
  233. package/templates/apps/api-observability/app.config.ts +32 -0
  234. package/templates/apps/api-observability/database/schema.ts +38 -0
  235. package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
  236. package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
  237. package/templates/apps/api-observability/package.json +29 -0
  238. package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
  239. package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
  240. package/templates/apps/api-observability/template.json +6 -0
  241. package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
  242. package/templates/apps/api-observability/tsconfig.json +5 -0
  243. package/templates/apps/api-ratelimit/README.md +49 -0
  244. package/templates/apps/api-ratelimit/app.config.ts +42 -0
  245. package/templates/apps/api-ratelimit/database/schema.ts +37 -0
  246. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
  247. package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
  248. package/templates/apps/api-ratelimit/package.json +29 -0
  249. package/templates/apps/api-ratelimit/template.json +6 -0
  250. package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
  251. package/templates/apps/api-ratelimit/tsconfig.json +5 -0
  252. package/templates/apps/api-rbac/README.md +59 -0
  253. package/templates/apps/api-rbac/app.config.ts +48 -0
  254. package/templates/apps/api-rbac/database/schema.ts +37 -0
  255. package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
  256. package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
  257. package/templates/apps/api-rbac/package.json +29 -0
  258. package/templates/apps/api-rbac/template.json +6 -0
  259. package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
  260. package/templates/apps/api-rbac/tsconfig.json +5 -0
  261. package/templates/apps/api-rest/README.md +85 -0
  262. package/templates/apps/api-rest/app.config.ts +70 -0
  263. package/templates/apps/api-rest/database/schema.ts +52 -0
  264. package/templates/apps/api-rest/lib/product.ts +30 -0
  265. package/templates/apps/api-rest/package.json +27 -0
  266. package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
  267. package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
  268. package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
  269. package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
  270. package/templates/apps/api-rest/template.json +6 -0
  271. package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
  272. package/templates/apps/api-rest/tsconfig.json +5 -0
  273. package/templates/apps/api-saas/README.md +106 -0
  274. package/templates/apps/api-saas/app.config.ts +51 -0
  275. package/templates/apps/api-saas/database/schema.ts +50 -0
  276. package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
  277. package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
  278. package/templates/apps/api-saas/package.json +31 -0
  279. package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
  280. package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
  281. package/templates/apps/api-saas/template.json +6 -0
  282. package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
  283. package/templates/apps/api-saas/tsconfig.json +5 -0
  284. package/templates/apps/api-search/README.md +68 -0
  285. package/templates/apps/api-search/app.config.ts +31 -0
  286. package/templates/apps/api-search/database/schema.ts +47 -0
  287. package/templates/apps/api-search/lib/search.ts +27 -0
  288. package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
  289. package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
  290. package/templates/apps/api-search/package.json +29 -0
  291. package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
  292. package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
  293. package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
  294. package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
  295. package/templates/apps/api-search/template.json +6 -0
  296. package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
  297. package/templates/apps/api-search/tsconfig.json +5 -0
  298. package/templates/apps/api-versioning/README.md +51 -0
  299. package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
  300. package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
  301. package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
  302. package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
  303. package/templates/apps/api-versioning/app.config.ts +24 -0
  304. package/templates/apps/api-versioning/database/schema.ts +38 -0
  305. package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
  306. package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
  307. package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
  308. package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
  309. package/templates/apps/api-versioning/package.json +29 -0
  310. package/templates/apps/api-versioning/template.json +6 -0
  311. package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
  312. package/templates/apps/api-versioning/tsconfig.json +5 -0
  313. package/templates/apps/api-webhooks/.env +6 -0
  314. package/templates/apps/api-webhooks/README.md +106 -0
  315. package/templates/apps/api-webhooks/app.config.ts +16 -0
  316. package/templates/apps/api-webhooks/database/schema.ts +49 -0
  317. package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
  318. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
  319. package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
  320. package/templates/apps/api-webhooks/package.json +28 -0
  321. package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
  322. package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
  323. package/templates/apps/api-webhooks/template.json +6 -0
  324. package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
  325. package/templates/apps/api-webhooks/tsconfig.json +5 -0
  326. package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
  327. package/templates/apps/changelog/README.md +77 -0
  328. package/templates/apps/changelog/app.config.ts +27 -0
  329. package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
  330. package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
  331. package/templates/apps/changelog/package.json +30 -0
  332. package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
  333. package/templates/apps/changelog/src/globals.css +66 -0
  334. package/templates/apps/changelog/src/globals.d.ts +16 -0
  335. package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
  336. package/templates/apps/changelog/src/lib/locale.ts +55 -0
  337. package/templates/apps/changelog/src/lib/releases.ts +21 -0
  338. package/templates/apps/changelog/src/locales/de.ts +22 -0
  339. package/templates/apps/changelog/src/locales/en.ts +30 -0
  340. package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
  341. package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
  342. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
  343. package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
  344. package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
  345. package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
  346. package/templates/apps/changelog/src/pages/index.tsx +55 -0
  347. package/templates/apps/changelog/src/pages/layout.tsx +84 -0
  348. package/templates/apps/changelog/template.json +6 -0
  349. package/templates/apps/changelog/tsconfig.json +11 -0
  350. package/templates/apps/edge-functions/README.md +55 -0
  351. package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
  352. package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
  353. package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
  354. package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
  355. package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
  356. package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
  357. package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
  358. package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
  359. package/templates/apps/edge-functions/package.json +21 -0
  360. package/templates/apps/edge-functions/template.json +6 -0
  361. package/templates/apps/edge-functions/tsconfig.json +10 -0
  362. package/templates/apps/frontend-admin/README.md +40 -0
  363. package/templates/apps/frontend-admin/app.config.ts +37 -0
  364. package/templates/apps/frontend-admin/package.json +30 -0
  365. package/templates/apps/frontend-admin/src/config.ts +8 -0
  366. package/templates/apps/frontend-admin/src/globals.css +76 -0
  367. package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
  368. package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
  369. package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
  370. package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
  371. package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
  372. package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
  373. package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
  374. package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
  375. package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
  376. package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
  377. package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
  378. package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
  379. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
  380. package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
  381. package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
  382. package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
  383. package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
  384. package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
  385. package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
  386. package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
  387. package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
  388. package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
  389. package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
  390. package/templates/apps/frontend-admin/template.json +6 -0
  391. package/templates/apps/frontend-admin/tsconfig.json +11 -0
  392. package/templates/apps/frontend-app/README.md +98 -0
  393. package/templates/apps/frontend-app/app.config.ts +43 -0
  394. package/templates/apps/frontend-app/package.json +30 -0
  395. package/templates/apps/frontend-app/src/locales/de.ts +36 -0
  396. package/templates/apps/frontend-app/src/locales/en.ts +43 -0
  397. package/templates/apps/frontend-app/src/locales/index.ts +15 -0
  398. package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
  399. package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
  400. package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
  401. package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
  402. package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
  403. package/templates/apps/frontend-app/template.json +6 -0
  404. package/templates/apps/frontend-app/tsconfig.json +11 -0
  405. package/templates/apps/frontend-blank/README.md +18 -0
  406. package/templates/apps/frontend-blank/app.config.ts +29 -0
  407. package/templates/apps/frontend-blank/package.json +29 -0
  408. package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
  409. package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
  410. package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
  411. package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
  412. package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
  413. package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
  414. package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
  415. package/templates/apps/frontend-blank/template.json +6 -0
  416. package/templates/apps/frontend-blank/tsconfig.json +11 -0
  417. package/templates/apps/frontend-contact/README.md +65 -0
  418. package/templates/apps/frontend-contact/app.config.ts +25 -0
  419. package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
  420. package/templates/apps/frontend-contact/package.json +33 -0
  421. package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
  422. package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
  423. package/templates/apps/frontend-contact/src/config.ts +12 -0
  424. package/templates/apps/frontend-contact/src/globals.css +84 -0
  425. package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
  426. package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
  427. package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
  428. package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
  429. package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
  430. package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
  431. package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
  432. package/templates/apps/frontend-contact/template.json +6 -0
  433. package/templates/apps/frontend-contact/tsconfig.json +11 -0
  434. package/templates/apps/frontend-dashboard/README.md +54 -0
  435. package/templates/apps/frontend-dashboard/app.config.ts +39 -0
  436. package/templates/apps/frontend-dashboard/package.json +29 -0
  437. package/templates/apps/frontend-dashboard/src/config.ts +8 -0
  438. package/templates/apps/frontend-dashboard/src/globals.css +72 -0
  439. package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
  440. package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
  441. package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
  442. package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
  443. package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
  444. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
  445. package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
  446. package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
  447. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
  448. package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
  449. package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
  450. package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
  451. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
  452. package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
  453. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
  454. package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
  455. package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
  456. package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
  457. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
  458. package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
  459. package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
  460. package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
  461. package/templates/apps/frontend-dashboard/template.json +6 -0
  462. package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
  463. package/templates/apps/frontend-docs/README.md +19 -0
  464. package/templates/apps/frontend-docs/app.config.ts +25 -0
  465. package/templates/apps/frontend-docs/package.json +29 -0
  466. package/templates/apps/frontend-docs/src/globals.css +32 -0
  467. package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
  468. package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
  469. package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
  470. package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
  471. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
  472. package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
  473. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
  474. package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
  475. package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
  476. package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
  477. package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
  478. package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
  479. package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
  480. package/templates/apps/frontend-docs/template.json +6 -0
  481. package/templates/apps/frontend-docs/tsconfig.json +11 -0
  482. package/templates/apps/frontend-i18n/README.md +61 -0
  483. package/templates/apps/frontend-i18n/app.config.ts +33 -0
  484. package/templates/apps/frontend-i18n/package.json +28 -0
  485. package/templates/apps/frontend-i18n/src/globals.css +46 -0
  486. package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
  487. package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
  488. package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
  489. package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
  490. package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
  491. package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
  492. package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
  493. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
  494. package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
  495. package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
  496. package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
  497. package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
  498. package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
  499. package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
  500. package/templates/apps/frontend-i18n/template.json +6 -0
  501. package/templates/apps/frontend-i18n/tsconfig.json +11 -0
  502. package/templates/apps/frontend-landing/README.md +17 -0
  503. package/templates/apps/frontend-landing/app.config.ts +25 -0
  504. package/templates/apps/frontend-landing/package.json +29 -0
  505. package/templates/apps/frontend-landing/src/globals.css +23 -0
  506. package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
  507. package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
  508. package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
  509. package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
  510. package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
  511. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
  512. package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
  513. package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
  514. package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
  515. package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
  516. package/templates/apps/frontend-landing/template.json +6 -0
  517. package/templates/apps/frontend-landing/tsconfig.json +11 -0
  518. package/templates/apps/frontend-spa/README.md +45 -0
  519. package/templates/apps/frontend-spa/app.config.ts +27 -0
  520. package/templates/apps/frontend-spa/package.json +29 -0
  521. package/templates/apps/frontend-spa/src/globals.css +84 -0
  522. package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
  523. package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
  524. package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
  525. package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
  526. package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
  527. package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
  528. package/templates/apps/frontend-spa/template.json +6 -0
  529. package/templates/apps/frontend-spa/tsconfig.json +11 -0
  530. package/templates/apps/frontend-ssr/README.md +68 -0
  531. package/templates/apps/frontend-ssr/app.config.ts +32 -0
  532. package/templates/apps/frontend-ssr/package.json +29 -0
  533. package/templates/apps/frontend-ssr/src/globals.css +67 -0
  534. package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
  535. package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
  536. package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
  537. package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
  538. package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
  539. package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
  540. package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
  541. package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
  542. package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
  543. package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
  544. package/templates/apps/frontend-ssr/template.json +6 -0
  545. package/templates/apps/frontend-ssr/tsconfig.json +11 -0
  546. package/templates/apps/frontend-ssr-api/README.md +50 -0
  547. package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
  548. package/templates/apps/frontend-ssr-api/package.json +30 -0
  549. package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
  550. package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
  551. package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
  552. package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
  553. package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
  554. package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
  555. package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
  556. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
  557. package/templates/apps/frontend-ssr-api/template.json +6 -0
  558. package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
  559. package/templates/apps/frontend-static-blog/README.md +49 -0
  560. package/templates/apps/frontend-static-blog/app.config.ts +34 -0
  561. package/templates/apps/frontend-static-blog/package.json +28 -0
  562. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
  563. package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
  564. package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
  565. package/templates/apps/frontend-static-blog/src/globals.css +75 -0
  566. package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
  567. package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
  568. package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
  569. package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
  570. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
  571. package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
  572. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
  573. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
  574. package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
  575. package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
  576. package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
  577. package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
  578. package/templates/apps/frontend-static-blog/template.json +6 -0
  579. package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
  580. package/templates/baselines/bare/.env.example +37 -0
  581. package/templates/baselines/bare/README.md +47 -0
  582. package/templates/baselines/bare/baseline.json +31 -0
  583. package/templates/baselines/bare/deploy/README.md +43 -0
  584. package/templates/baselines/bare/deploy/voltro.service.example +36 -0
  585. package/templates/baselines/compose/.env.example +46 -0
  586. package/templates/baselines/compose/README.md +69 -0
  587. package/templates/baselines/compose/baseline.json +51 -0
  588. package/templates/baselines/compose/docker/.dockerignore +38 -0
  589. package/templates/baselines/compose/docker/api.Dockerfile +57 -0
  590. package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
  591. package/templates/baselines/compose/docker/web.Dockerfile +59 -0
  592. package/templates/baselines/compose/docker-compose.dev.yml +89 -0
  593. package/templates/baselines/compose/docker-compose.prod.yml +87 -0
  594. package/templates/baselines/compose/docker-compose.yml +41 -0
  595. package/templates/baselines/compose-mariadb/.env.example +57 -0
  596. package/templates/baselines/compose-mariadb/README.md +78 -0
  597. package/templates/baselines/compose-mariadb/baseline.json +51 -0
  598. package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
  599. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
  600. package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
  601. package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
  602. package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
  603. package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
  604. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
  605. package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
  606. package/templates/baselines/helm/.env.example +39 -0
  607. package/templates/baselines/helm/README.md +98 -0
  608. package/templates/baselines/helm/baseline.json +53 -0
  609. package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
  610. package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
  611. package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
  612. package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
  613. package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
  614. package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
  615. package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
  616. package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
  617. package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
  618. package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
  619. package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
  620. package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
  621. package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
  622. package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
  623. package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
  624. package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
  625. package/templates/baselines/helm/deploy/README.md +94 -0
  626. package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
@@ -0,0 +1,1351 @@
1
+ # Workflows
2
+
3
+ > Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/workflows/overview.md -->
10
+ ## Overview
11
+
12
+ _Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries._
13
+
14
+ A **workflow** in Voltro is a long-running Effect program that can survive process restarts. Durable work is expressed with `workflow({...})`, checkpointed `step({...})` calls, durable `sleep({...})`, optional external `awaitSignal(...)` waits, and synchronous `awaitUpdate(...)` messages.
15
+
16
+ The implementation is `@effect/workflow` + `@effect/cluster`. No Restate, Temporal, Inngest, BullMQ, or separate worker runtime is required.
17
+
18
+ Live — start a 3-step durable workflow and watch its timeline stream as each step
19
+ completes (durable-execution UI no other framework ships):
20
+
21
+ ```tsx
22
+ const run = useAction('app', 'demo.runPipeline')
23
+ const { runId } = await run.run({})
24
+ <WorkflowProgress api="app" runId={runId} /> {/* live step timeline */}
25
+ ```
26
+
27
+ ## What's in this section
28
+
29
+ - [Defining workflows](/docs/workflows/definition) — `workflow`, `payload`, `success`, default execute factories, and checkpointed steps
30
+ - [Retries & failure handling](/docs/workflows/retries) — per-step retries, failed runs, retry controls, and compensation
31
+ - [Sleep & wakeups](/docs/workflows/sleep) — `sleep`, schedules, and `awaitSignal`
32
+ - [Clustering](/docs/workflows/cluster) — multi-instance workflow distribution and schedule coordination
33
+ - [Flow control](/docs/workflows/flow-control) — durable queues, bounded concurrency, and rate limits
34
+ - [Versioning](/docs/workflows/versioning) — definition versions, compatibility metadata, and patch markers
35
+ - [Debugging & inspection](/docs/workflows/debugging) — dashboard, inspect endpoints, CLI, tables, and tests
36
+
37
+ ## When to use a workflow
38
+
39
+ | You have... | Use |
40
+ |---|---|
41
+ | Multi-step job that must survive a deploy | Workflow |
42
+ | External side effects that need retry/resume semantics | Workflow |
43
+ | Human approval or webhook callback in the middle of work | Workflow + `awaitSignal` |
44
+ | External command that must validate and return a result | Workflow + `awaitUpdate` |
45
+ | Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.emit(...)` |
46
+ | One-off durable delay inside an operation | Workflow + `sleep` |
47
+ | Recurring job such as "daily at 3am" | [Schedule](/docs/scheduling/overview) (`*.cron.tsx`) |
48
+ | Atomic database write | Mutation |
49
+ | Request-scoped HTTP/email/payment call | Action |
50
+ | Progressive server-to-client output | Stream |
51
+ | Request-scoped LLM chat | Agent or stream |
52
+
53
+ The wedge: if the operation must survive the process, use a workflow. If it is just a short request, keep it in a mutation, action, query, or stream.
54
+
55
+ ## A minimal workflow
56
+
57
+ A workflow is split into two files paired by basename — a **descriptor**
58
+ (`*.workflow.tsx`, browser-safe, imports `@voltro/workflow/define`) that the
59
+ codegen pulls into the client rpcGroup, and a **server executor**
60
+ (`*.workflow.server.tsx`) that holds the database/AI/cluster imports:
61
+
62
+ ```tsx
63
+ // apps/api/workflows/notes.summarise.workflow.tsx — descriptor (browser-safe)
64
+ import { workflow } from '@voltro/workflow/define'
65
+ import { Schema } from 'effect'
66
+
67
+ export const SummariseNote = workflow({
68
+ name: 'notes.summarise',
69
+ payload: { noteId: Schema.String },
70
+ success: Schema.Struct({ summary: Schema.String }),
71
+ idempotencyKey: ({ noteId }) => `notes.summarise:${noteId}`,
72
+ })
73
+ ```
74
+
75
+ ```tsx
76
+ // apps/api/workflows/notes.summarise.workflow.server.tsx — executor (server-only)
77
+ import { step } from '@voltro/workflow'
78
+ import { Effect, Schema } from 'effect'
79
+ import type { AppContext } from '@voltro/runtime'
80
+
81
+ const buildExecute = (ctx: AppContext) =>
82
+ ({ noteId }: { noteId: string }) =>
83
+ Effect.gen(function* () {
84
+ const note = yield* step({
85
+ name: 'load-note',
86
+ input: { noteId },
87
+ success: Schema.Struct({ id: Schema.String, body: Schema.String }),
88
+ execute: Effect.tryPromise(() => ctx.store.query({
89
+ table: 'notes',
90
+ predicate: { field: 'id', op: 'eq', value: noteId },
91
+ } as never).then((rows) => rows[0] as never)),
92
+ })
93
+
94
+ const summary = yield* step({
95
+ name: 'summarise-with-llm',
96
+ input: { noteId },
97
+ success: Schema.String,
98
+ execute: summariseWithLlm(note.body),
99
+ })
100
+
101
+ yield* step({
102
+ name: 'save-summary',
103
+ input: { noteId },
104
+ success: Schema.Void,
105
+ execute: Effect.tryPromise(() =>
106
+ ctx.store.update('notes', noteId, { summary } as never).then(() => undefined),
107
+ ),
108
+ })
109
+
110
+ return { summary }
111
+ })
112
+
113
+ export default buildExecute
114
+ ```
115
+
116
+ `step({...})` is the checkpoint boundary. A plain `yield* someEffect` composes Effect logic, but it is not automatically recorded as a workflow step. Put external I/O, database writes, and expensive work inside `step`.
117
+
118
+ ## Starting a workflow
119
+
120
+ Every discovered `*.workflow.tsx` is emitted into `rpcGroup.generated.ts` as a unary RPC. The payload and error schemas come from the workflow, but the RPC success type is always a `WorkflowRunHandle`. Calling it starts durable work and returns that handle immediately:
121
+
122
+ ```tsx
123
+ import {
124
+ useWorkflow,
125
+ useWorkflowRun,
126
+ useWorkflowRunEvents,
127
+ useWorkflowRuns,
128
+ useWorkflowRunSteps,
129
+ useWorkflowUpdate,
130
+ } from '@voltro/client'
131
+
132
+ const summarise = useWorkflow<{ noteId: string }>(
133
+ 'app',
134
+ 'notes.summarise',
135
+ )
136
+
137
+ const run = await summarise.start({ noteId })
138
+ const { run: live } = useWorkflowRun('app', run.id)
139
+ ```
140
+
141
+ `run.id` is the durable execution id and can be passed to `useWorkflowRun(...)`. The hook subscribes to Voltro's built-in reactive workflow-run query, so the UI updates when `_voltro_workflow_runs` changes. Waiting for the success payload is explicit: use `ctx.workflows.wait(...)` on the server when another server-side operation really must block; otherwise render the live run row and let the workflow finish in the background.
142
+
143
+ For app-level job centers and detail pages, use the rest of the workflow hook family:
144
+
145
+ ```tsx
146
+ const { runs } = useWorkflowRuns('app', { tag: 'notes.summarise', limit: 25 })
147
+ const { steps } = useWorkflowRunSteps('app', live?.id)
148
+ const { events } = useWorkflowRunEvents('app', live?.id)
149
+ const approve = useWorkflowUpdate('app')
150
+ await approve.update({ id: live!.id }, 'approve', { decision: true })
151
+ ```
152
+
153
+ Those hooks are backed by reactive framework tables too, so status, checkpointed steps, timers, signals, and update events flow through Voltro subscriptions instead of polling.
154
+
155
+ From server code, use `ctx.workflows`:
156
+
157
+ ```ts
158
+ export default async ({ noteId }, ctx) => {
159
+ await ctx.store.update('notes', noteId, { summaryStatus: 'queued' })
160
+ const run = await ctx.workflows.start('notes.summarise', { noteId })
161
+ return { runId: run.id }
162
+ }
163
+ ```
164
+
165
+ Inside a mutation, `ctx.workflows.start(...)` is transaction-aware: Voltro computes the deterministic run handle immediately, queues the actual start, and only launches the workflow after the mutation commits. If the mutation rolls back, the workflow never starts. Actions and incoming webhooks start immediately after their own verification/idempotency work has succeeded.
166
+
167
+ Workflow controls use the same facade:
168
+
169
+ ```ts
170
+ await ctx.workflows.signal({ id: runId }, 'approval', { approved: true })
171
+ const approved = await ctx.workflows.update({ id: runId }, 'approve', { decision: true })
172
+ await ctx.workflows.cancel('notes.summarise', executionId)
173
+ await ctx.workflows.resume('notes.summarise', executionId)
174
+ const snapshot = await ctx.workflows.query('notes.summarise', executionId)
175
+ ```
176
+
177
+ Inside a workflow body, use `ctx.workflows.child(...)` for hierarchical work. Voltro records the parent execution id in `_voltro_workflow_start_contexts` before submitting the child run, so a different cluster runner still sees the same lineage when it starts executing the child.
178
+
179
+ ```ts
180
+ const child = yield* Effect.promise(() =>
181
+ ctx.workflows.child('orders.shipOne', { orderId }, {
182
+ parentClosePolicy: 'cancel',
183
+ }),
184
+ )
185
+
186
+ const childSnapshot = yield* Effect.promise(() => ctx.workflows.wait(child))
187
+ ```
188
+
189
+ `parentClosePolicy` controls what happens when the parent closes. `cancel` is the default and interrupts an open child. `terminate` also interrupts the child and records the decision as a hard parent-close action. `abandon` leaves the child running. The dashboard shows child runs, their parent execution id, and the chosen policy.
190
+
191
+ Inside the workflow body, `awaitUpdate(...)` receives the tracked message, validates the payload, and records either `update-completed` with the result or `update-failed` with the validation/handler error:
192
+
193
+ ```tsx
194
+ import { awaitUpdate } from '@voltro/workflow'
195
+ import { Effect, Schema } from 'effect'
196
+
197
+ const result = yield* awaitUpdate(ctx, {
198
+ name: 'approve',
199
+ schema: Schema.Struct({ decision: Schema.Boolean }),
200
+ success: Schema.Struct({ accepted: Schema.Boolean }),
201
+ handle: ({ decision }) => Effect.succeed({ accepted: decision }),
202
+ })
203
+ ```
204
+
205
+ ## Event-triggered workflows
206
+
207
+ Use `*.trigger.ts` files when durable work should start from domain events instead of a direct RPC or action call.
208
+
209
+ ```ts
210
+ // apps/api/triggers/user.signup.trigger.ts
211
+ import { triggerWorkflow } from '@voltro/runtime'
212
+
213
+ export default triggerWorkflow<{ userId: string; plan: string }, { userId: string }>({
214
+ event: 'user.signup',
215
+ workflow: 'onboarding.start',
216
+ filter: (event) => event.data.plan !== 'free',
217
+ payload: (event) => ({ userId: event.data.userId }),
218
+ idempotencyKey: (event) => event.id,
219
+ })
220
+ ```
221
+
222
+ Then emit the event from a mutation, action, incoming webhook, schedule, or workflow:
223
+
224
+ ```ts
225
+ await ctx.events.emit('user.signup', { userId, plan })
226
+ ```
227
+
228
+ Voltro records the event in `_voltro_workflow_events`, records each trigger delivery in `_voltro_workflow_event_deliveries`, and starts matching workflows with `source: event:<name>`. Emits inside mutations are post-commit safe: if the mutation rolls back, the workflow fan-out is not launched.
229
+
230
+ Use `event: '*'` for audit-style wildcard triggers that should see every emitted domain event. The local devtools and Voltro Cloud workflow dashboards mirror domain events and delivery rows live in the Events tab, including filtered/skipped deliveries, failed fan-out, and the spawned execution id.
231
+
232
+ ## Runtime boundaries
233
+
234
+ - The workflow executor receives an `AppContext` built for the run. Starts from RPC handlers, mutations, actions, schedules, verified incoming handlers, and inspect tooling record a `source` such as `workflow-rpc`, `app-context`, `schedule:<name>`, `incoming:<id>`, or `inspect`. Request starts also carry the starter trace; authenticated request starts carry the resolved subject into the run record and executor context.
235
+ - `ctx.events.emit(...)` is available when the app declares workflow event triggers. Triggered workflow starts carry the starter subject/trace and a `source` of `event:<name>`.
236
+ - Put tenant/user ids that the workflow must enforce into the payload. That keeps business authorization deterministic across retries, resumes, and future cross-replica handoff.
237
+ - Child workflows should be started with `ctx.workflows.child(...)`. Parent lineage and parent-close policy are persisted before the child start, so they survive cross-runner execution.
238
+ - External incoming handlers created with `defineIncomingWebhook(...)` receive `context.workflows`, so a verified webhook can start, signal, or update a workflow after signature and idempotency checks.
239
+ - Operational controls such as retry, cancel, suspend, resume, signal injection, and tracked updates also live on the dashboard, `voltro workflows ...`, and the inspect endpoints.
240
+
241
+ ## What is intentionally different from Temporal
242
+
243
+ - **Polyglot workers.** Voltro workflows are TypeScript/Effect.
244
+ - **A separate workflow service.** This is intentional: the engine is mounted by the framework alongside your API.
245
+ - **Automatic code-version isolation.** A long-lived run resumed after a deploy executes the current workflow code. Use workflow `version`, `compatibleWith`, and `patches` metadata for deploy safety.
246
+
247
+ What you gain: workflows live beside queries, mutations, actions, streams, schedules, schema, and plugins. The same codegen and inspect surfaces see them.
248
+
249
+ ## Where to go next
250
+
251
+ - [Defining workflows](/docs/workflows/definition)
252
+ - [Retries & failure handling](/docs/workflows/retries)
253
+ - [Sleep & wakeups](/docs/workflows/sleep)
254
+ - [Flow control](/docs/workflows/flow-control)
255
+ - [Versioning](/docs/workflows/versioning)
256
+ - [Debugging & inspection](/docs/workflows/debugging)
257
+
258
+
259
+
260
+ ---
261
+
262
+ <!-- source: en/workflows/definition.md -->
263
+ ## Defining workflows
264
+
265
+ _The *.workflow.tsx convention, workflow schemas, execute factories, checkpointed steps, child workflows, and replay rules._
266
+
267
+ A workflow file is discovered by suffix: `*.workflow.tsx`. It exports one `workflow({...})` definition and a default factory that receives `AppContext` and returns the workflow executor.
268
+
269
+ ## Anatomy
270
+
271
+ ```tsx
272
+ // apps/api/workflows/notes.summarise.workflow.tsx
273
+ import { workflow, step } from '@voltro/workflow'
274
+ import { Effect, Schema } from 'effect'
275
+ import type { AppContext } from '@voltro/runtime'
276
+
277
+ export const SummariseNote = workflow({
278
+ name: 'notes.summarise',
279
+ payload: { noteId: Schema.String },
280
+ success: Schema.Struct({ summary: Schema.String }),
281
+ idempotencyKey: ({ noteId }) => `notes.summarise:${noteId}`,
282
+ })
283
+
284
+ const buildExecute = (ctx: AppContext) =>
285
+ ({ noteId }: { noteId: string }, _executionId: string) =>
286
+ Effect.gen(function* () {
287
+ const note = yield* step({
288
+ name: 'load-note',
289
+ input: { noteId },
290
+ success: Schema.Struct({ id: Schema.String, body: Schema.String }),
291
+ execute: Effect.tryPromise(() => loadNote(ctx.store, noteId)),
292
+ })
293
+
294
+ const summary = yield* step({
295
+ name: 'summarise-with-llm',
296
+ input: { noteId },
297
+ success: Schema.String,
298
+ execute: summariseWithLlm(note.body),
299
+ })
300
+
301
+ yield* step({
302
+ name: 'save-summary',
303
+ input: { noteId },
304
+ success: Schema.Void,
305
+ execute: Effect.tryPromise(() => saveSummary(ctx.store, noteId, summary)),
306
+ })
307
+
308
+ return { summary }
309
+ })
310
+
311
+ export default buildExecute
312
+ ```
313
+
314
+ Two exports matter:
315
+
316
+ - `workflow({...})` — the durable definition. `payload`, `success`, optional `error`, and optional `idempotencyKey` are read by codegen and the runtime.
317
+ - `default` — a build-execute factory, `(ctx: AppContext) => (payload, executionId) => Effect`. The CLI calls `definition.toLayer(execute)` during registration.
318
+
319
+ If the default export is not a function, discovery skips the file with a "missing default-export build-execute factory" warning.
320
+
321
+ ## Schema fields
322
+
323
+ ```ts
324
+ export const ImportCustomers = workflow({
325
+ name: 'customers.import',
326
+ payload: {
327
+ uploadId: Schema.String,
328
+ dryRun: Schema.optional(Schema.Boolean),
329
+ },
330
+ success: Schema.Struct({
331
+ imported: Schema.Number,
332
+ skipped: Schema.Number,
333
+ }),
334
+ error: Schema.Union(InvalidCsv, ImportProviderDown),
335
+ idempotencyKey: ({ uploadId }) => `customers.import:${uploadId}`,
336
+ messages: {
337
+ signals: {
338
+ approval: Schema.Struct({ approved: Schema.Boolean }),
339
+ },
340
+ updates: {
341
+ approve: {
342
+ payload: Schema.Struct({ decision: Schema.Boolean }),
343
+ success: Schema.Struct({ accepted: Schema.Boolean }),
344
+ },
345
+ },
346
+ },
347
+ })
348
+ ```
349
+
350
+ `payload` is the start input. `success` is the resolved output. `error` is the typed failure channel. `idempotencyKey` deduplicates concurrent or repeated starts with the same logical input. `messages` is optional codegen metadata; it emits `WorkflowSignals`, `WorkflowUpdates`, `WorkflowQueries`, and `WorkflowMessages` type maps, while runtime validation still happens at `awaitSignal(...)` / `awaitUpdate(...)`.
351
+
352
+ ## Step boundaries
353
+
354
+ `step({...})` wraps `@effect/workflow`'s `Activity.make`. It is the checkpointed unit that is journaled by the workflow engine and recorded into `_voltro_workflow_run_steps`.
355
+
356
+ ```ts
357
+ const customer = yield* step({
358
+ name: 'fetch-customer',
359
+ input: { customerId },
360
+ success: Customer,
361
+ execute: Effect.tryPromise(() => crm.fetchCustomer(customerId)),
362
+ })
363
+ ```
364
+
365
+ The optional `input` field is not passed to the executor. It is persisted for inspection, truncated to a dashboard-safe size, and shown beside output/errors in the run timeline.
366
+
367
+ Plain Effect composition is still useful:
368
+
369
+ ```ts
370
+ const normalised = normaliseCustomer(customer)
371
+ const enriched = yield* Effect.succeed(addDerivedFields(normalised))
372
+ ```
373
+
374
+ But it is not a durable activity boundary by itself. If it does external I/O, writes to storage, charges a card, sends an email, or calls an LLM, put that work inside `step`.
375
+
376
+ ## Deterministic replay
377
+
378
+ The workflow body can be replayed by the engine. The structure of checkpointed steps must be stable for the same payload.
379
+
380
+ OK:
381
+
382
+ ```ts
383
+ const note = yield* step({ name: 'load-note', success: Note, execute: loadNote(noteId) })
384
+
385
+ if (note.archived) {
386
+ yield* step({ name: 'notify-archived', execute: notifyArchived(note.id) })
387
+ } else {
388
+ yield* step({ name: 'summarise', execute: summarise(note.body) })
389
+ }
390
+ ```
391
+
392
+ The branch depends on a checkpointed value.
393
+
394
+ Not OK:
395
+
396
+ ```ts
397
+ if (Math.random() > 0.5) {
398
+ yield* step({ name: 'a', execute: doA })
399
+ } else {
400
+ yield* step({ name: 'b', execute: doB })
401
+ }
402
+ ```
403
+
404
+ If time, randomness, or external state influences structure, capture it in a step first:
405
+
406
+ ```ts
407
+ const choice = yield* step({
408
+ name: 'choose-branch',
409
+ success: Schema.Boolean,
410
+ execute: Effect.sync(() => Math.random() > 0.5),
411
+ })
412
+ ```
413
+
414
+ ## Starting from the client
415
+
416
+ 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:
417
+
418
+ ```tsx
419
+ import { useWorkflow, useWorkflowRun } from '@voltro/client'
420
+
421
+ const startImport = useWorkflow<{ uploadId: string }>('app', 'customers.import')
422
+
423
+ const run = await startImport.start({ uploadId })
424
+ const { run: liveRun } = useWorkflowRun('app', run.id)
425
+ ```
426
+
427
+ Use `liveRun.status` and `liveRun.output` to render progress/result state. Waiting for the success payload is explicit on the server with `ctx.workflows.wait(...)`; UI code should usually subscribe to the run row instead of blocking the interaction.
428
+
429
+ ## Child workflows
430
+
431
+ Inside a workflow body, start child runs through `ctx.workflows.child(...)`. The child gets its own durable run, step history, events, cancellation controls, and dashboard detail page. Voltro persists the parent execution id and parent-close policy before submitting the child start, so the relationship survives cross-runner execution in a cluster.
432
+
433
+ ```ts
434
+ const child = yield* Effect.promise(() =>
435
+ ctx.workflows.child('documents.embed', { documentId }, {
436
+ parentClosePolicy: 'cancel',
437
+ }),
438
+ )
439
+
440
+ const result = yield* Effect.promise(() => ctx.workflows.wait(child))
441
+ ```
442
+
443
+ Fan out with normal Effect concurrency:
444
+
445
+ ```ts
446
+ const children = yield* Effect.all(
447
+ documents.map((doc) =>
448
+ Effect.promise(() =>
449
+ ctx.workflows.child('documents.embed', { documentId: doc.id }, {
450
+ parentClosePolicy: 'abandon',
451
+ }),
452
+ ),
453
+ ),
454
+ { concurrency: 8 },
455
+ )
456
+ ```
457
+
458
+ Parent-close policies:
459
+
460
+ | Policy | Behavior |
461
+ |---|---|
462
+ | `cancel` | Default. Interrupt open children when the parent closes. |
463
+ | `terminate` | Interrupt open children and record a hard parent-close action. |
464
+ | `abandon` | Leave the child running when the parent closes. |
465
+
466
+ The dashboard shows child runs, their parent execution id, and the selected policy. `ctx.workflows.wait(child)` accepts the run handle directly when the parent needs the child's success/failure snapshot.
467
+
468
+ ## Tenant and subject
469
+
470
+ Workflow starts persist the starter subject, trace id, source, parent execution id, and parent-close policy in `_voltro_workflow_start_contexts`. Whichever runner first executes the workflow loads that context before building the executor `AppContext`.
471
+
472
+ Still include tenant/user ids that the business process must enforce in `payload`, validate them in the first step, and scope store reads/writes deliberately. Payload data is replay-safe and makes authorization decisions auditable across retries and deploys.
473
+
474
+ ## Anti-patterns
475
+
476
+ - **Using `input` in `workflow({...})`.** The current API is `payload`.
477
+ - **Assuming every `yield*` is a persisted step.** Use `step({...})` for checkpoints.
478
+ - **Starting workflows through `useMutation(...)`.** Use `useWorkflow(...)`; workflows are durable runs, not optimistic writes.
479
+ - **Branching step structure on randomness or live external state.** Capture the value in a step first.
480
+ - **Long-lived workflows without payload versioning.** A run resumed after a deploy uses the current workflow code.
481
+
482
+
483
+
484
+ ---
485
+
486
+ <!-- source: en/workflows/retries.md -->
487
+ ## Retries & failure handling
488
+
489
+ _Per-step retries, failed workflow runs, retry controls, compensation, and timeouts._
490
+
491
+ Workflow durability and retry are related, but not identical. The engine checkpoints completed `step({...})` activities so an interrupted run can resume without re-running successful steps. Transient failures still need an explicit retry policy around the step that can fail.
492
+
493
+ ## Per-step retries
494
+
495
+ Use `stepModule.retry(...)` when you want retry attempts to appear as separate step attempts in the dashboard:
496
+
497
+ ```tsx
498
+ import { workflow, step, stepModule } from '@voltro/workflow'
499
+ import { Effect, Schedule, Schema } from 'effect'
500
+
501
+ class ProviderDown extends Schema.TaggedError<ProviderDown>()('ProviderDown', {
502
+ message: Schema.String,
503
+ }) {}
504
+
505
+ const summary = yield* stepModule.retry(
506
+ step({
507
+ name: 'summarise-with-llm',
508
+ input: { noteId },
509
+ success: Schema.String,
510
+ error: ProviderDown,
511
+ retry: {
512
+ strategy: 'exponential',
513
+ maxAttempts: 5,
514
+ baseDelay: '500 millis',
515
+ },
516
+ execute: callLlm(note.body),
517
+ }),
518
+ Schedule.exponential('500 millis').pipe(Schedule.recurs(4)),
519
+ )
520
+ ```
521
+
522
+ The `retry` field on `step({...})` is metadata for inspection. The actual retry behaviour comes from `stepModule.retry(...)`, `Effect.retry(...)`, or another Effect retry combinator.
523
+
524
+ ## What to retry
525
+
526
+ | Failure kind | Recommended handling |
527
+ |---|---|
528
+ | HTTP 5xx, connection reset, provider 429 | Retry the step with backoff and jitter |
529
+ | Validation error, malformed payload | Fail the workflow |
530
+ | Permission error | Fail the workflow; fix caller or payload |
531
+ | Timeout | Convert to a typed transient error, then retry if safe |
532
+ | Cancel/suspend by operator | Do not catch unless you are deliberately cleaning up |
533
+
534
+ Keep retry windows close to the side effect. A flaky LLM call should retry inside the LLM step, not by restarting the whole workflow body.
535
+
536
+ ## Failed runs
537
+
538
+ When a workflow ultimately fails, Voltro records the run as `failed`. Query failed runs from any handler, action, or mutation with the typed SDK — no raw SQL, no knowledge of internal tables:
539
+
540
+ ```ts
541
+ const failed = await ctx.workflows.listRuns({
542
+ status: 'failed',
543
+ workflowName: 'notes.summarise',
544
+ })
545
+ // failed: WorkflowRunSummary[] — id, workflowName, status, payload,
546
+ // errorTag, errorMessage, startedAt, completedAt, durationMs, …
547
+ ```
548
+
549
+ `listRuns(filter?)` accepts `{ workflowName / tag, status, limit, offset }` (all optional) and returns the most-recent runs first. The inspect dashboard surfaces the same data live, but the SDK is the primary path — it's typed and reactive-friendly.
550
+
551
+ There is no separate `voltro_workflow_dlq` table in the current runtime. Failed runs are the triage queue.
552
+
553
+ ## Retrying a failed run
554
+
555
+ Re-run a failed run by its id from a handler, action, or mutation:
556
+
557
+ ```ts
558
+ for (const run of failed) {
559
+ // Re-run against the original payload …
560
+ await ctx.workflows.retry(run.id)
561
+ // … or replay against a corrected input after fixing bad data:
562
+ await ctx.workflows.retry(run.id, { payloadOverride: { ...run.payload, retries: 1 } })
563
+ }
564
+ ```
565
+
566
+ `retry(runId, options?)` resolves the workflow by its tag, re-executes it, and returns `{ executionId }` — the engine-assigned id of the fresh run. By default it uses the original run's recorded payload; pass `payloadOverride` to replay against a different input.
567
+
568
+ For ad-hoc ops, the same operation is available manually — the CLI (`voltro workflows retry <runId>`) and the dashboard's "Retry" button both delegate to the same runtime code:
569
+
570
+ ```sh
571
+ voltro workflows retry wfrun_01H...
572
+ ```
573
+
574
+ ## Compensation
575
+
576
+ For saga-style workflows, model compensation explicitly with `Effect.catchAll` around the step that can fail after an earlier side effect:
577
+
578
+ ```ts
579
+ yield* step({
580
+ name: 'reserve-inventory',
581
+ success: Schema.Void,
582
+ execute: reserveInventory(orderId),
583
+ })
584
+
585
+ yield* step({
586
+ name: 'charge-card',
587
+ success: Schema.Void,
588
+ execute: chargeCard(orderId).pipe(
589
+ Effect.catchAll((error) =>
590
+ releaseInventory(orderId).pipe(
591
+ Effect.zipRight(Effect.fail(error)),
592
+ ),
593
+ ),
594
+ ),
595
+ })
596
+ ```
597
+
598
+ There is no `defineSaga(...)` helper today. Compensation is ordinary Effect code, checkpointed when you put it inside a step.
599
+
600
+ ## Timeouts
601
+
602
+ A hung external call is not a failure until it errors. Wrap it:
603
+
604
+ ```ts
605
+ const response = yield* step({
606
+ name: 'fetch-provider',
607
+ success: ProviderResponse,
608
+ error: ProviderDown,
609
+ execute: fetchProvider(input).pipe(
610
+ Effect.timeoutFail({
611
+ duration: '30 seconds',
612
+ onTimeout: () => new ProviderDown({ message: 'provider timed out' }),
613
+ }),
614
+ ),
615
+ })
616
+ ```
617
+
618
+ If the operation is not idempotent, pass an idempotency key to the provider and also enforce a unique key in your own database.
619
+
620
+ ## Failure observability
621
+
622
+ Every run has three inspectable layers:
623
+
624
+ - `_voltro_workflow_runs` — run status, payload, output, top-level error, subject, start source, timing, trace id, parent execution id, parent-close policy.
625
+ - `_voltro_workflow_run_steps` — each step attempt, recorded input/output/error, retry metadata, duration.
626
+ - `_voltro_workflow_run_events` — run lifecycle, timers, signals, suspend/resume/cancel events.
627
+
628
+ The dashboard and `voltro workflows show <runId>` read these same tables through inspect endpoints.
629
+
630
+ ## Anti-patterns
631
+
632
+ - **Catching errors and returning success.** Operators lose the failed run.
633
+ - **Retrying non-idempotent side effects.** Double charges and duplicate emails are workflow bugs, not retry bugs.
634
+ - **Putting retry metadata on a step without `Effect.retry` or `stepModule.retry`.** Metadata only explains intent; it does not execute a policy.
635
+ - **Expecting a DLQ table.** The current triage surface is failed runs plus retry controls.
636
+
637
+
638
+
639
+ ---
640
+
641
+ <!-- source: en/workflows/sleep.md -->
642
+ ## Sleep & wakeups
643
+
644
+ _Durable sleep, cron schedules, external signals, and how parked workflows show up in inspection._
645
+
646
+ Use `sleep({...})` from `@voltro/workflow` for durable waits inside a workflow. It wraps `@effect/workflow`'s `DurableClock.sleep` and records timer events for the dashboard.
647
+
648
+ ## Basic sleep
649
+
650
+ ```tsx
651
+ import { workflow, step, sleep } from '@voltro/workflow'
652
+ import { Effect, Schema } from 'effect'
653
+
654
+ const buildExecute = (ctx: AppContext) =>
655
+ ({ userId }: { userId: string }) =>
656
+ Effect.gen(function* () {
657
+ yield* step({
658
+ name: 'send-welcome',
659
+ success: Schema.Void,
660
+ execute: sendEmail(ctx, userId, 'welcome'),
661
+ })
662
+
663
+ yield* sleep({ name: 'retention-delay', duration: '7 days' })
664
+
665
+ yield* step({
666
+ name: 'send-retention-check-in',
667
+ success: Schema.Void,
668
+ execute: sendEmail(ctx, userId, 'retention-check-in'),
669
+ })
670
+ })
671
+ ```
672
+
673
+ The workflow is not meant to hold a worker thread for the whole delay. The durable clock parks the workflow in the engine; Voltro records `timer-set` and `timer-fired` events in `_voltro_workflow_run_events`.
674
+
675
+ ## Sleep until a wall-clock time
676
+
677
+ Compute a duration, then pass it to `sleep`:
678
+
679
+ ```ts
680
+ const millisUntilNextThreeUtc = () => {
681
+ const now = new Date()
682
+ const target = new Date(now)
683
+ target.setUTCHours(3, 0, 0, 0)
684
+ if (target <= now) target.setUTCDate(target.getUTCDate() + 1)
685
+ return target.getTime() - now.getTime()
686
+ }
687
+
688
+ yield* sleep({
689
+ name: 'wait-until-3am-utc',
690
+ duration: `${millisUntilNextThreeUtc()} millis`,
691
+ })
692
+ ```
693
+
694
+ Use this for one-off waits that are part of a larger workflow. For recurring cadence, use schedules.
695
+
696
+ ## Recurring work belongs in schedules
697
+
698
+ Do not build a forever loop with `sleep` for "run every day" jobs. Use `*.cron.tsx`:
699
+
700
+ ```tsx
701
+ // apps/api/schedules/daily-rollup.cron.tsx
702
+ import { defineSchedule } from '@voltro/runtime'
703
+
704
+ export default defineSchedule({
705
+ name: 'rollup.daily',
706
+ cron: '0 3 * * *',
707
+ timezone: 'UTC',
708
+ onOverlap: 'skip',
709
+ handler: async ({ app, scheduledAt }) => {
710
+ await runRollup(app.store, scheduledAt)
711
+ },
712
+ })
713
+ ```
714
+
715
+ Schedules give you timezone parsing, overlap policy, backfill policy, cluster-wide coordination, run history, and manual "Run now" controls.
716
+
717
+ ## External wakeups with `awaitSignal`
718
+
719
+ Use `awaitSignal(ctx, ...)` when a workflow must wait for an outside decision, such as a human approval or webhook callback.
720
+
721
+ ```tsx
722
+ import { awaitSignal, step } from '@voltro/workflow'
723
+ import { Effect, Schema } from 'effect'
724
+
725
+ const decision = yield* awaitSignal(ctx, {
726
+ name: 'approval',
727
+ schema: Schema.Struct({
728
+ approved: Schema.Boolean,
729
+ reviewerId: Schema.String,
730
+ }),
731
+ timeoutMs: 24 * 60 * 60_000,
732
+ })
733
+
734
+ if (!decision.approved) {
735
+ yield* step({
736
+ name: 'cancel-order',
737
+ success: Schema.Void,
738
+ execute: cancelOrder(orderId, decision.reviewerId),
739
+ })
740
+ }
741
+ ```
742
+
743
+ Signals are matched by name inside one workflow run. The payload is decoded with the supplied schema.
744
+
745
+ ## Sending signals
746
+
747
+ From the dashboard, open a running workflow and use "Send signal".
748
+
749
+ From the CLI:
750
+
751
+ ```sh
752
+ voltro workflows signal wfrun_01H... --name approval --payload '{"approved":true,"reviewerId":"usr_123"}'
753
+ ```
754
+
755
+ From HTTP:
756
+
757
+ ```http
758
+ POST /_voltro/inspect/workflows/runs/:id/signal
759
+ Content-Type: application/json
760
+
761
+ { "signalName": "approval", "payload": { "approved": true, "reviewerId": "usr_123" } }
762
+ ```
763
+
764
+ The runtime records `signal-awaited`, `signal-sent`, and `signal-received` events in `_voltro_workflow_run_events`. The polling activity inside `awaitSignal` is checkpointed; once it receives a payload, replay returns the journaled value.
765
+
766
+ ## Cancellation and suspend/resume
767
+
768
+ Use operational controls, not sentinel rows:
769
+
770
+ ```sh
771
+ voltro workflows cancel wfrun_01H...
772
+ voltro workflows suspend wfrun_01H...
773
+ voltro workflows resume wfrun_01H...
774
+ ```
775
+
776
+ The inspect API exposes the same actions at:
777
+
778
+ ```http
779
+ POST /_voltro/inspect/workflows/runs/:id/cancel
780
+ POST /_voltro/inspect/workflows/runs/:id/suspend
781
+ POST /_voltro/inspect/workflows/runs/:id/resume
782
+ ```
783
+
784
+ ## Anti-patterns
785
+
786
+ - **Using `Effect.sleep` for durable workflow waits.** Use `sleep({...})` so timer events are recorded and the durable clock is used.
787
+ - **Forever-loop cron workflows.** Use `*.cron.tsx` schedules for recurring cadence.
788
+ - **Polling an external system with long sleeps.** Prefer webhooks or `awaitSignal`.
789
+ - **Sleeping inside a database transaction.** Finish the write boundary before the wait.
790
+
791
+
792
+
793
+ ---
794
+
795
+ <!-- source: en/workflows/cluster.md -->
796
+ ## Clustering
797
+
798
+ _How Voltro scales workflows, crons, and reactivity across multiple instances — the built-in @effect/cluster runtime, schedule claims, and per-dialect CDC._
799
+
800
+ Clustering is **built in**. There is no plugin to install and no config flag to flip: run more than one instance of your app against the same database and they form a cluster automatically. Durable workflows resume on any surviving instance, each cron fires exactly once across the whole fleet, and writes on one instance become reactive subscriptions on every other.
801
+
802
+ `@effect/cluster` is a core dependency of `@voltro/workflow` — the CLI wires it for you when `voltro dev` or `voltro start` boots an app on a SQL store. You scale **out** by running N instances; the coordination machinery is already there.
803
+
804
+ ## How it's enabled
805
+
806
+ There is nothing to enable in code. The wiring is automatic and driven by the store dialect:
807
+
808
+ | Store dialect | Runner storage | Horizontal scale-out |
809
+ |---|---|---|
810
+ | `postgres`, `mysql`, `mariadb`, `mssql` | `sql` | yes — workflows resume on any instance |
811
+ | `sqlite` | `memory` | no — durable replay within one process |
812
+ | `memory` (dev default) | — | single process |
813
+
814
+ On a SQL store, the runtime composes the dialect's `SqlClient` → `@effect/cluster` `SingleRunner` → `@effect/workflow` `ClusterWorkflowEngine` into one layer and provides it to your program. `sqlite` falls back to in-process durable replay (no cross-instance scale-out) because `@effect/cluster`'s runner-lock acquisition has no SQLite branch.
815
+
816
+ You do not call the cluster layer yourself — the CLI does. The boot log tells you which mode resolved:
817
+
818
+ ```text
819
+ [voltro:dev] workflow engine: cluster-sql, dialect=mariadb
820
+ ```
821
+
822
+ ### The deployment contract
823
+
824
+ A real multi-instance deployment has to give each pod a routable identity. This is the entire operational surface of clustering, and it is what the framework's Helm baseline injects:
825
+
826
+ - **Routable runner host (`POD_IP`).** When a workflow that started on a now-dead pod has to resume elsewhere, the surviving pod addresses the dead pod's runner by the address it advertised. The default is `localhost`, which is unreachable cross-pod. Inject the Kubernetes downward-API `status.podIP` as `POD_IP` (or set `VOLTRO_WORKFLOW_RUNNER_HOST`) so each pod advertises its real address. The runtime warns at boot if a SQL-backed runner is still on `localhost`.
827
+ - **Fixed runner port.** `SingleRunner` otherwise picks a random port, which breaks restart-resume because the runner table still references the previous process's port. Pin it via `VOLTRO_WORKFLOW_RUNNER_PORT` (default `34000`) so a restarted pod inherits the same cluster identity.
828
+ - **Unique, stable `server_id` per pod (MariaDB CDC).** The binlog reader needs a `server_id` that is unique across the fleet and stable across reschedules — duplicate ids silently break binlog streams. The runtime derives it by hashing a per-pod replica id (`POD_NAME` → FNV-1a). With a `StatefulSet` the pod name is ordinal and deterministic.
829
+ - **Replication user (MariaDB).** The binlog CDC reader connects with a dedicated user holding `REPLICATION SLAVE, REPLICATION CLIENT`, against a server configured `binlog_format=ROW`, `binlog_row_image=FULL`, `gtid_strict_mode=ON`.
830
+
831
+ The relevant downward-API wiring, from the framework's Helm baseline (the AWB 4-replica MariaDB deployment is the worked example):
832
+
833
+ ```yaml
834
+ env:
835
+ - name: POD_NAME
836
+ valueFrom: { fieldRef: { fieldPath: metadata.name } } # → stable per-pod CDC server_id
837
+ - name: POD_IP
838
+ valueFrom: { fieldRef: { fieldPath: status.podIP } } # → routable workflow-runner host
839
+ - name: VOLTRO_WORKFLOW_RUNNER_HOST
840
+ value: "$(POD_IP)"
841
+ - name: VOLTRO_WORKFLOW_RUNNER_PORT
842
+ value: "34000"
843
+ - name: DB_DIALECT
844
+ value: "mariadb"
845
+ - name: CDC
846
+ value: "1"
847
+ ```
848
+
849
+ That is the whole contract: stable per-pod identity (`POD_NAME` → `server_id`), routable runner host (`POD_IP`), a pinned runner port, the shared `DB_URL`, and CDC on. No application code changes between one instance and four.
850
+
851
+ ## Durable workflows resume on any instance
852
+
853
+ Workflow durability lives in `@effect/cluster`'s engine, not in any Voltro table. Each workflow's journal (steps, idempotency keys, signals) is persisted to the cluster's storage on the shared database. When a pod dies mid-step:
854
+
855
+ 1. Its runner lease stops being renewed.
856
+ 2. Another runner reclaims the dead runner's shards.
857
+ 3. The reclaiming runner reads the workflow journal, fast-forwards past completed steps, and resumes from the unfinished step.
858
+
859
+ Because steps are journaled with idempotency keys, a re-executed step replays its cached result rather than running its side effects twice. This is why the `POD_IP` runner host matters: the reclaiming pod must be able to reach the workflow's runner address to take it over.
860
+
861
+ `@effect/cluster` is dialect-agnostic here — its `SqlRunnerStorage` and `SqlMessageStorage` dispatch internally via `sql.onDialectOrElse({ mssql, mysql, sqlite, orElse: postgres })`. MariaDB rides the `mysql` branch (`GET_LOCK`, `ON DUPLICATE KEY UPDATE`, native `RETURNING` since 10.5).
862
+
863
+ > **mssql only:** `@effect/cluster`'s mssql storage has two driver-level bugs the framework fixes with a patch that a plain install can't carry. Run **`voltro add mssql`** once, then `pnpm install` — see [SQL Server → Workflow cluster](/docs/database/dialects/mssql#workflow-cluster). No-op on every other dialect.
864
+
865
+ ## Crons fire once cluster-wide
866
+
867
+ Schedule coordination is app-wide. On a postgres store the default is `advisoryLock`; you can opt into `cluster`, or force `single` for a one-instance deployment with `app.config.ts` (`scheduling: { coordination }`). Individual `*.cron.tsx` files stay topology-agnostic.
868
+
869
+ | `coordination` | Exactly-once mechanism | When |
870
+ |---|---|---|
871
+ | `single` | none — always fires | dev, single pod |
872
+ | `advisoryLock` | claims row in `_voltro_schedule_claims` | multi-instance, default on a SQL store |
873
+ | `cluster` | `@effect/cluster` shard ownership (`ClusterCron`) | when you already run the workflow engine and want one ownership model |
874
+
875
+ **`advisoryLock`** is the lightweight tier. Every replica computes the same deterministic `scheduledAt` from the cron expression (not its own `Date.now()`, so clock skew is irrelevant), derives a claim key `${scheduleName}@${secondBucket}`, and races to `INSERT` a row into `_voltro_schedule_claims`. The primary-key conflict makes exactly one replica win; the rest lose cleanly and skip. The second-precision bucket makes claims self-expiring — a crashed winner doesn't block the next firing, because the next firing is a new bucket and therefore a new key. This is deliberately a claims **row**, not a session-level lock, because a connection pool can hand a `GET_LOCK`/`pg_advisory_lock` connection to another query before the unlock.
876
+
877
+ **`cluster`** rides the same sharding machinery as durable workflows: `ClusterCron` fires on exactly one runner (the shard owner) and migrates ownership on runner death. The in-app timer is disarmed in this mode (`@effect/cluster` owns the clock); the firing is recorded with `coordinationOutcome: 'cluster'`. It needs a SQL store with cluster storage (everything except `sqlite`/`memory`), and degrades to `single` with a warning when that's unavailable.
878
+
879
+ Overlap policy (`onOverlap: 'skip' | 'queue' | 'parallel'`) is also cluster-aware: `skip` checks for a `status='running'` row from **any** pod, not just the local one, so a long run on pod A suppresses a new firing on pod B.
880
+
881
+ ## Cross-instance reactivity (per dialect)
882
+
883
+ A write on instance A has to become a reactive subscription delta on instance B. The transport is change-data-capture, and it is **not** Postgres-only — each dialect uses its native change feed:
884
+
885
+ | Dialect | Transport | Latency |
886
+ |---|---|---|
887
+ | `postgres` | `LISTEN`/`NOTIFY` | fast |
888
+ | `mariadb` | binlog CDC (ROW image, GTID) | push-based, per-replica reader |
889
+ | `mysql` | inline emit only (no binlog CDC) | single-process |
890
+ | `sqlite` | in-process bus | single-process |
891
+
892
+ On MariaDB the binlog **is** the message bus — each replica tails it itself, resuming from its own `_voltro_cdc_offsets` row keyed by replica id. There is no Redis, NATS, or external broker. CDC is on by default for any SQL dialect (`CDC=1`); set `CDC=0` to fall back to single-process inline emit. The boot log reports the resolved transport:
893
+
894
+ ```text
895
+ [voltro:dev] sql dialect resolved: mariadb — CDC: binlog CDC (ROW), RETURNING: native (INSERT/DELETE); UPDATE then SELECT
896
+ ```
897
+
898
+ ## Observability
899
+
900
+ - **Dashboard.** Workflow runs, steps, and schedule runs stream live to the devtools/cloud dashboard.
901
+ - **Tables.** Introspection rows live on the shared database — query them directly:
902
+ - `_voltro_workflow_start_contexts` — durable starter subject/trace/source plus parent execution id and parent-close policy, loaded by whichever runner first executes the workflow or child workflow.
903
+ - `_voltro_workflow_runs`, `_voltro_workflow_run_steps`, `_voltro_workflow_run_events` — workflow execution history (these are fire-and-forget introspection; durability itself lives in the cluster engine's own tables).
904
+ - `_voltro_schedule_runs` — every firing, with `replicaId` (which instance fired it) and `coordinationOutcome` (`single` / `wonLock` / `cluster` / …).
905
+ - `_voltro_schedule_claims` — the live exactly-once claim rows for `advisoryLock` schedules.
906
+ - **Logs and traces.** `voltro logs --tail 100` shows the resolved dialect, CDC flavor, coordinator, and runner identity at boot, plus every firing and resume. `voltro traces` correlates a workflow's steps across pods.
907
+
908
+ ### `voltro cluster status`
909
+
910
+ `voltro cluster status` renders the clustering snapshot of every running api — discovered the same way as `voltro logs` / `voltro traces` (the runtime registry), then merged with the live coordination state each instance reads from the shared store:
911
+
912
+ ```sh
913
+ voltro cluster status # pretty table of every instance
914
+ voltro cluster status --process myApi # restrict to one registered api
915
+ voltro cluster status --format json # machine-readable, pipe to jq
916
+ ```
917
+
918
+ For each instance it shows the `replicaId`, the runner address (`host:port`), the resolved dialect and clustering tier (full / inline-only / single-process / none), the CDC flavor, the coordination kind, and — on the MariaDB CDC path — the binlog `server_id`. A SQL-backed runner advertising `localhost` / `127.0.0.1` is flagged loudly: other pods can't reach it, so cross-pod workflow resume silently breaks (inject `POD_IP` via the K8s downward API or set `VOLTRO_WORKFLOW_RUNNER_HOST`). A footer summarises the shared coordination state — active replicas seen in recent runs, current schedule claims, and in-flight schedule + workflow runs. The same view is on the dashboard's **Cluster** panel.
919
+
920
+ Quick "which instance is running what" query:
921
+
922
+ ```sql
923
+ SELECT "replicaId", count(*)
924
+ FROM "_voltro_schedule_runs"
925
+ WHERE status = 'running'
926
+ GROUP BY "replicaId";
927
+ ```
928
+
929
+ ## The multi-dialect story
930
+
931
+ Clustering works on every SQL dialect, with one capability gradient:
932
+
933
+ - **`postgres` / `mariadb` / `mssql`** — full clustering: durable workflow resume across instances, `advisoryLock` or `cluster` crons, native cross-instance reactivity (`LISTEN`/`NOTIFY` on postgres, binlog CDC on mariadb).
934
+ - **`mysql`** — durable workflow resume and schedule coordination work; cross-instance reactivity is inline-only (no binlog CDC path), so subscriptions don't fan out across instances.
935
+ - **`sqlite`** — durable workflow **replay** within a single process (`runnerStorage: 'memory'`); no horizontal scale-out.
936
+ - **`memory`** — single-process dev store; no clustering.
937
+
938
+ The same app code runs unchanged across all of them — the runtime resolves the right runner storage, coordinator, and CDC transport from the dialect at boot.
939
+
940
+ > **`clusterPlugin()` — deliberately not built.** A convenience marker plugin (declaring an app "expects multi-instance", optionally pinning the runner host/port) was considered and rejected: clustering is already automatic for any SQL store with N instances, so there is no behaviour for such a plugin to toggle. Its only knobs — runner host/port — are env-driven (`VOLTRO_WORKFLOW_RUNNER_HOST` / `VOLTRO_WORKFLOW_RUNNER_PORT`) and injected by the deployment (K8s downward API), which is where pod identity belongs. The idea is on record but will not ship.
941
+
942
+ ## Anti-patterns
943
+
944
+ - **Assuming clustering is Postgres-only.** It isn't. MariaDB is a first-class clustering target (binlog CDC), and the AWB 4-replica MariaDB deployment is the production reference. `wal_level=logical` is a Postgres detail, not a clustering requirement.
945
+ - **Leaving the runner host on `localhost` in multi-pod deployments.** Resume-on-another-pod silently breaks — pods can't reach a `localhost` runner. Inject `POD_IP`. The runtime warns about this at boot.
946
+ - **Reusing a `server_id` across MariaDB pods.** Duplicate `server_id` silently breaks binlog streams. Derive it from a stable per-pod `POD_NAME` (the framework does this for you).
947
+ - **Session-level locks for cron coordination.** `advisoryLock` uses a claims **row**, not `pg_advisory_lock`/`GET_LOCK`, precisely because pooled connections make session locks unreliable. Don't reach for session locks.
948
+
949
+
950
+
951
+ ---
952
+
953
+ <!-- source: en/workflows/flow-control.md -->
954
+ ## Flow control
955
+
956
+ _Durable queues, bounded concurrency, and rate limits inside workflow code._
957
+
958
+ Use flow control when the workflow is durable, but the resource it touches is limited: a third-party API, a tenant import lane, a GPU job, or a webhook fan-out.
959
+
960
+ Voltro exposes the workflow engine's durable queue and rate limiter through `@voltro/workflow`:
961
+
962
+ ```ts
963
+ import {
964
+ durableQueue,
965
+ processQueue,
966
+ queueWorker,
967
+ rateLimit,
968
+ step,
969
+ workflow,
970
+ } from '@voltro/workflow'
971
+ import { Effect, Schema } from 'effect'
972
+ ```
973
+
974
+ ## Bounded concurrency
975
+
976
+ Define a queue once, then process items from workflow code. A worker layer controls concurrency and is backed by the same SQL/cluster workflow engine, so another runner can resume after a crash.
977
+
978
+ ```ts
979
+ export const ThumbnailQueue = durableQueue({
980
+ name: 'thumbnails',
981
+ payload: {
982
+ imageId: Schema.String,
983
+ sourceUrl: Schema.String,
984
+ },
985
+ success: Schema.Struct({ thumbnailUrl: Schema.String }),
986
+ idempotencyKey: ({ imageId }) => imageId,
987
+ })
988
+
989
+ export const thumbnailWorker = queueWorker(
990
+ ThumbnailQueue,
991
+ ({ imageId, sourceUrl }) =>
992
+ step({
993
+ name: 'render-thumbnail',
994
+ input: { imageId },
995
+ success: Schema.Struct({ thumbnailUrl: Schema.String }),
996
+ execute: renderThumbnail(sourceUrl),
997
+ }),
998
+ { concurrency: 4 },
999
+ )
1000
+ ```
1001
+
1002
+ Export the worker from any discovered `*.workflow.tsx` module. `voltro dev` and `voltro serve` auto-mount branded `queueWorker(...)` layers at boot; no `app.config.ts` layer plumbing is needed.
1003
+
1004
+ Inside a workflow:
1005
+
1006
+ ```ts
1007
+ const result = yield* processQueue(ThumbnailQueue, {
1008
+ imageId,
1009
+ sourceUrl,
1010
+ })
1011
+ ```
1012
+
1013
+ `processQueue(...)` is a workflow activity. The item is persisted, the workflow parks while a worker processes it, and replay returns the recorded worker result instead of re-enqueueing.
1014
+
1015
+ ## Rate limits
1016
+
1017
+ Use `rateLimit(...)` before a step that talks to a constrained service:
1018
+
1019
+ ```ts
1020
+ yield* rateLimit({
1021
+ name: 'stripe-write',
1022
+ window: '1 minute',
1023
+ limit: 100,
1024
+ key: tenantId,
1025
+ })
1026
+
1027
+ const invoice = yield* step({
1028
+ name: 'create-invoice',
1029
+ input: { tenantId, orderId },
1030
+ execute: createStripeInvoice(orderId),
1031
+ })
1032
+ ```
1033
+
1034
+ The wait uses the durable workflow clock. A deploy or worker crash does not lose the delay.
1035
+
1036
+ ## Dashboard behavior
1037
+
1038
+ Queue workers and rate-limited steps still record normal workflow steps, timers, and run events. The Workflows dashboard shows the parked run, the step that is waiting, and the later continuation through the same live run/step/event subscriptions. The Flow tab groups visible runs by workflow lane, queued/running/waiting status, and start source so pressure is visible in both local devtools and Voltro Cloud.
1039
+
1040
+ ## Rules of thumb
1041
+
1042
+ - Use a durable queue for **bounded concurrency** and per-resource lanes.
1043
+ - Use `rateLimit(...)` for **API budgets** where excess work should wait, not fail.
1044
+ - Put the limiting key in your payload or workflow state so replay is deterministic.
1045
+ - Keep external I/O inside `step(...)` or the queue worker body.
1046
+
1047
+
1048
+
1049
+ ---
1050
+
1051
+ <!-- source: en/workflows/versioning.md -->
1052
+ ## Versioning
1053
+
1054
+ _Workflow definition versions, compatibility metadata, and patch markers for long-running runs._
1055
+
1056
+ Long-running workflows can outlive a deploy. Voltro does not run old JavaScript forever; a resumed run executes the current code. Make that explicit by versioning the workflow definition.
1057
+
1058
+ ```ts
1059
+ import { workflow } from '@voltro/workflow'
1060
+ import { Schema } from 'effect'
1061
+
1062
+ export const ImportCustomers = workflow({
1063
+ name: 'customers.import',
1064
+ payload: { uploadId: Schema.String },
1065
+ success: Schema.Struct({ imported: Schema.Number }),
1066
+ idempotencyKey: ({ uploadId }) => uploadId,
1067
+ version: 3,
1068
+ compatibleWith: [2, 3],
1069
+ patches: ['split-validate-and-write'],
1070
+ })
1071
+ ```
1072
+
1073
+ Voltro stores `workflowVersion` and `workflowPatches` on every `_voltro_workflow_runs` row when the run starts. The local devtools and Voltro Cloud dashboard show the version chip on run rows, so operators can spot old or incompatible runs during a deploy.
1074
+
1075
+ ## Compatibility
1076
+
1077
+ `compatibleWith` is operator metadata. Use it to document which run versions the current code can still resume safely. If a change cannot resume old payloads or step layout, bump `version` and leave the old version out of `compatibleWith`.
1078
+
1079
+ ## Patch Markers
1080
+
1081
+ `patches` are named change markers. Keep them short and stable:
1082
+
1083
+ ```ts
1084
+ patches: [
1085
+ 'split-validate-and-write',
1086
+ 'rename-export-step',
1087
+ ]
1088
+ ```
1089
+
1090
+ Patch markers are recorded with the run. They make deploy review and dashboard triage concrete: you can answer which branching changes were active when the run started.
1091
+
1092
+ ## Rules
1093
+
1094
+ - Bump `version` for payload shape changes, step-order changes that affect replay, or changed external side-effect semantics.
1095
+ - Keep old payload decoders inside the workflow body only while their version remains compatible.
1096
+ - Prefer additive payload changes with defaults over breaking changes.
1097
+ - Use the dashboard version chip during deploys to find runs that started on an older contract.
1098
+
1099
+
1100
+
1101
+ ---
1102
+
1103
+ <!-- source: en/workflows/debugging.md -->
1104
+ ## Debugging & inspection
1105
+
1106
+ _The dashboard's Workflows panel, inspect endpoints, CLI controls, workflow tables, and testing helpers._
1107
+
1108
+ Voltro records workflow runs, step attempts, and lifecycle events into framework tables. The dashboard, CLI, and inspect endpoints all read the same data.
1109
+
1110
+ ## What is recorded
1111
+
1112
+ | Table | Contents |
1113
+ |---|---|
1114
+ | `_voltro_workflow_start_contexts` | One row per started execution id: starter subject, trace id, source, parent execution id, parent-close policy, and creation time. Used for cross-runner context handoff. |
1115
+ | `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy. |
1116
+ | `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
1117
+ | `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
1118
+ | `_voltro_workflow_events` | Domain events emitted through `ctx.events.emit(...)`: event id, name, payload, source, subject, trace id, occurred time. |
1119
+ | `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
1120
+
1121
+ Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
1122
+
1123
+ ## Dashboard
1124
+
1125
+ When `voltro dev` is running, the Workflows panel lists recent runs, their status, start source, timing, payload, output/error, step attempts, and events. The run detail view is the fastest way to answer:
1126
+
1127
+ - Which step is currently running?
1128
+ - Was the run started by an RPC, `ctx.workflows`, a schedule, an incoming webhook, or inspect tooling?
1129
+ - Is this run a child, and will it be cancelled, terminated, or abandoned when the parent closes?
1130
+ - Which attempt failed?
1131
+ - What input did the step receive?
1132
+ - What output or typed error did it produce?
1133
+ - Did a timer or signal fire?
1134
+
1135
+ The panel also exposes run controls for users with the right capability: cancel, retry, suspend, resume, send signal, and send a tracked update.
1136
+
1137
+ Run filters can be saved as named views. The selected view and ad-hoc
1138
+ filters are mirrored into the URL query string, so a teammate can open the
1139
+ same filtered run list. The Workflows panel also includes dedicated
1140
+ Incoming and Flow tabs: Incoming groups runs whose source is
1141
+ `incoming:<id>` beside incoming-sourced domain events, while Flow groups
1142
+ queued/running/waiting work by workflow lane and start source.
1143
+
1144
+ ## CLI
1145
+
1146
+ ```sh
1147
+ voltro workflows list --status running --tail 50
1148
+ voltro workflows list --tag notes.summarise --format json
1149
+ voltro workflows start notes.summarise --payload '{"noteId":"note_123"}'
1150
+ voltro workflows show wfrun_01H...
1151
+ voltro workflows retry wfrun_01H...
1152
+ voltro workflows cancel wfrun_01H...
1153
+ voltro workflows suspend wfrun_01H...
1154
+ voltro workflows resume wfrun_01H...
1155
+ voltro workflows signal wfrun_01H... --name approval --payload '{"approved":true}'
1156
+ voltro workflows update wfrun_01H... --name approve --payload '{"decision":true}'
1157
+ voltro workflows children exec_01H...
1158
+ ```
1159
+
1160
+ The CLI discovers live API processes the same way as `voltro logs` and `voltro traces`, then calls inspect endpoints.
1161
+
1162
+ ## Inspect endpoints
1163
+
1164
+ ```http
1165
+ GET /_voltro/inspect/workflows/runs
1166
+ GET /_voltro/inspect/workflows/runs?tag=notes.summarise&status=failed
1167
+ GET /_voltro/inspect/workflows/runs/:id/steps
1168
+ GET /_voltro/inspect/workflows/runs/:id/events
1169
+ GET /_voltro/inspect/workflows/events
1170
+ GET /_voltro/inspect/workflows/events/:id/deliveries
1171
+ GET /_voltro/inspect/workflows/children?parentExecutionId=exec_01H...
1172
+
1173
+ POST /_voltro/inspect/workflows/runs/:id/cancel
1174
+ POST /_voltro/inspect/workflows/runs/:id/retry
1175
+ POST /_voltro/inspect/workflows/runs/:id/suspend
1176
+ POST /_voltro/inspect/workflows/runs/:id/resume
1177
+ POST /_voltro/inspect/workflows/runs/:id/signal
1178
+ POST /_voltro/inspect/workflows/runs/:id/update
1179
+ ```
1180
+
1181
+ Signal body:
1182
+
1183
+ ```json
1184
+ {
1185
+ "signalName": "approval",
1186
+ "payload": { "approved": true }
1187
+ }
1188
+ ```
1189
+
1190
+ Update body:
1191
+
1192
+ ```json
1193
+ {
1194
+ "updateName": "approve",
1195
+ "payload": { "decision": true },
1196
+ "timeoutMs": 30000
1197
+ }
1198
+ ```
1199
+
1200
+ Retry normally uses the original payload. The inspect handler also supports payload override for operator tooling.
1201
+
1202
+ ## SQL inspection
1203
+
1204
+ Recent failed runs:
1205
+
1206
+ ```sql
1207
+ SELECT id, tag, status, source, "errorTag", "errorMessage", "startedAt", "completedAt"
1208
+ FROM "_voltro_workflow_runs"
1209
+ WHERE status = 'failed'
1210
+ ORDER BY "startedAt" DESC
1211
+ LIMIT 50;
1212
+ ```
1213
+
1214
+ Slow steps:
1215
+
1216
+ ```sql
1217
+ SELECT "stepName", percentile_cont(0.95) WITHIN GROUP (ORDER BY "durationMs")
1218
+ FROM "_voltro_workflow_run_steps"
1219
+ WHERE status = 'succeeded'
1220
+ AND "startedAt" > now() - interval '24 hours'
1221
+ GROUP BY "stepName"
1222
+ ORDER BY 2 DESC
1223
+ LIMIT 10;
1224
+ ```
1225
+
1226
+ Events for one run:
1227
+
1228
+ ```sql
1229
+ SELECT "eventType", payload, "occurredAt"
1230
+ FROM "_voltro_workflow_run_events"
1231
+ WHERE "runId" = 'wfrun_01H...'
1232
+ ORDER BY "occurredAt" ASC;
1233
+ ```
1234
+
1235
+ ## Inspecting from code
1236
+
1237
+ `inspectWorkflow(idOrExecutionId, store)` assembles one run into the same shape used by the dashboard detail view:
1238
+
1239
+ ```ts
1240
+ import { inspectWorkflow } from '@voltro/workflow'
1241
+
1242
+ const state = await inspectWorkflow(runId, ctx.store)
1243
+ ```
1244
+
1245
+ It groups step attempts by step name, so a step that failed twice and succeeded on attempt three reports `attempts: 3` with the latest output/error surfaced.
1246
+
1247
+ ## Testing workflows
1248
+
1249
+ Use `makeWorkflowRunner` from `@voltro/testing` to run a workflow in-process over the in-memory workflow engine and an in-memory recorder:
1250
+
1251
+ ```ts
1252
+ import { makeTestContext, makeWorkflowRunner } from '@voltro/testing'
1253
+ import { SummariseNote } from '../workflows/notes.summarise.workflow'
1254
+ import buildSummariseNote from '../workflows/notes.summarise.workflow'
1255
+
1256
+ test('notes.summarise retries the LLM step', async () => {
1257
+ const ctx = makeTestContext()
1258
+ const runner = makeWorkflowRunner({
1259
+ ctx,
1260
+ workflows: [{
1261
+ workflow: SummariseNote as never,
1262
+ execute: buildSummariseNote(ctx) as never,
1263
+ }],
1264
+ })
1265
+
1266
+ const result = await runner.start('notes.summarise', { noteId: 'n_1' })
1267
+ expect(result.status).toBe('succeeded')
1268
+ expect(result.steps.find((s) => s.name === 'summarise-with-llm')?.attempts).toBe(3)
1269
+ })
1270
+ ```
1271
+
1272
+ The result includes `{ status, output, error, steps, runId }`. `runner.inspect(runId)` returns the assembled run later without re-running it.
1273
+
1274
+ ## Anti-patterns
1275
+
1276
+ - **Querying old table names.** Use `_voltro_workflow_runs`, `_voltro_workflow_run_steps`, and `_voltro_workflow_run_events`.
1277
+ - **Filtering for `status=dead`.** Current failed runs use `status=failed`.
1278
+ - **Expecting replay-from-step APIs.** Current operator retry starts a new execution from the workflow payload; completed steps are not selectively replayed through a public API.
1279
+ - **Logging giant step inputs/outputs.** Step input/output is persisted for inspection. Store references to huge blobs instead of the blob itself.
1280
+
1281
+
1282
+
1283
+ ---
1284
+
1285
+ <!-- source: en/workflows/event-triggers.md -->
1286
+ ## Event triggers
1287
+
1288
+ _defineEventTrigger — bind a domain event name to a workflow. ctx.events.emit(name, data) fans the event out to every matching trigger with filter, payload-mapping, and idempotency._
1289
+
1290
+ A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a domain **event name** to a **workflow name**, and any handler that calls `ctx.events.emit('event.name', data)` fans the event out to every trigger listening for it. The emitter never names the workflow — "something happened" is separated from "run this workflow", so you add reactions without touching the code that emits.
1291
+
1292
+ ```tsx
1293
+ // triggers/orderPaid.trigger.tsx
1294
+ import { defineEventTrigger } from '@voltro/runtime'
1295
+
1296
+ export default defineEventTrigger({
1297
+ event: 'order.paid', // the event name to listen for
1298
+ workflow: 'fulfilment.run', // the workflow to start
1299
+ // Optional: skip when the predicate returns false.
1300
+ filter: (e) => e.data.total > 0,
1301
+ // Optional: map the event envelope → the workflow's payload (default: e.data).
1302
+ payload: (e) => ({ orderId: e.data.orderId, tenantId: e.data.tenantId }),
1303
+ // Optional: dedup key — two emits with the same key start the workflow once.
1304
+ idempotencyKey: (e) => `fulfil:${e.data.orderId}`,
1305
+ })
1306
+ ```
1307
+
1308
+ Discovery walks every `*.trigger.tsx`; the `default` export must be a `defineEventTrigger(...)` descriptor (one per file). The workflow runtime is active whenever the app has any workflow OR any trigger.
1309
+
1310
+ ## Emitting an event
1311
+
1312
+ From any handler, emit through `ctx.events`:
1313
+
1314
+ ```ts
1315
+ const execute = async (input: { orderId: string }, ctx: AppContext) => {
1316
+ // ... mark the order paid ...
1317
+ const result = await ctx.events.emit('order.paid', {
1318
+ orderId: input.orderId,
1319
+ tenantId: ctx.request.subject.tenantId,
1320
+ total: order.total,
1321
+ })
1322
+ // result.eventId — the persisted event id
1323
+ // result.triggered — one entry per matched trigger:
1324
+ // { triggerId, workflowName, status: 'started' | 'skipped' | 'failed', run?, reason? }
1325
+ }
1326
+ ```
1327
+
1328
+ `emit(name, data, options?)` accepts `{ id?, source? }` (override the event id / source label). It returns `{ eventId, triggered }` as soon as the workflows are kicked off — the workflows themselves run durably in the background, exactly like `ctx.workflows.start`.
1329
+
1330
+ The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
1331
+
1332
+ ```ts
1333
+ { id, name, data, occurredAt, source, subject?, traceId? }
1334
+ ```
1335
+
1336
+ `subject` + `traceId` are inherited from the emitting request, so a triggered workflow continues the same trace and carries the same actor.
1337
+
1338
+ ## Matching + delivery
1339
+
1340
+ - **Exact + wildcard.** A trigger matches its exact `event` name; a trigger registered for `'*'` matches every event (and runs in addition to exact matches).
1341
+ - **Idempotency.** Before starting, the runtime checks for an existing delivery with the same `idempotencyKey` (default `<eventId>:<triggerId>`) — a duplicate is recorded as `status: 'skipped', reason: 'duplicate'` and the workflow is not started twice.
1342
+ - **Every emit is audited.** The event itself lands in `_voltro_workflow_events`; one row per trigger lands in `_voltro_workflow_event_deliveries` with `status` (`starting → started`, or `skipped` / `failed`), the started `executionId`, and any error message. Both tables are reactive — the dashboard's Workflows view surfaces deliveries live.
1343
+
1344
+ ## When to use it
1345
+
1346
+ Reach for an event trigger when ONE thing happening should fan out to several independent reactions, or when you want the emitter to stay ignorant of the consumers:
1347
+
1348
+ - `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `emit`.
1349
+ - A mutation emits `user.signedUp`; an onboarding workflow trigger starts the drip sequence. The signup mutation never imports the onboarding workflow.
1350
+
1351
+ If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling emit from react, or fanning one event out to many.