@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,1273 @@
1
+ # Plugins
2
+
3
+ > How Voltro plugins compose into the runtime, what they can intercept, the catalogue, and writing your own.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/overview.md -->
10
+ ## Overview
11
+
12
+ _How Voltro plugins compose into the runtime, what they can intercept, the catalogue, and writing your own._
13
+
14
+ A Voltro plugin is a server-side extension that hooks into the runtime. Plugins can intercept mutations / queries / actions, contribute schema mixins + tables + migrations, mount raw-HTTP and inspect routes, contribute an Effect service layer, and run install / activate / deactivate lifecycles.
15
+
16
+ The framework ships some plugins; you write your own; the contract is small enough to learn in a single read.
17
+
18
+ ## What's in this section
19
+
20
+ - [The plugin contract](/docs/plugins/contract) — `definePlugin`, lifecycle hooks, rpc interceptors, framework-version compatibility
21
+ - [plugin-audit](/docs/plugins/audit) — mutation audit log + `audit()` mixin
22
+ - [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
23
+ - [plugin-multitenancy](/docs/plugins/multitenancy) — `tenant()` schema mixin + `assertOwnTenant` write-guard + `TenantMismatch`
24
+ - [plugin-soft-delete](/docs/plugins/soft-delete) — `softDelete()` schema mixin (hide on delete, `hardDelete()` bypass)
25
+ - [plugin-rbac](/docs/plugins/rbac) — roles + permissions + the `permission()` guard
26
+ - [plugin-ratelimit](/docs/plugins/ratelimit) — per-endpoint / per-subject / per-tenant request limits
27
+ - [plugin-billing](/docs/plugins/billing) — subscriptions, plans, entitlements + usage metering (Stripe + mock provider); seats, mid-cycle proration + dunning; money as integer minor units
28
+ - [plugin-licensing](/docs/plugins/licensing) — offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; plan entitlements + pricing decided server-side, never baked into a published version
29
+ - [plugin-mail](/docs/plugins/mail) — transactional email (Resend / Postmark / SendGrid / SES / Mailgun / SMTP, templates, suppression, scheduling, batch, idempotency)
30
+ - [plugin-storage](/docs/plugins/storage) — file storage: public (CDN-direct) + private (access policy + per-object grants), S3 / R2 / GCS / MinIO / filesystem
31
+ - [plugin-ai-flows](/docs/plugins/ai-flows) — durable multi-step AI pipelines (deterministic + agentic) with human-in-the-loop, chaining, and cadence; code-first `defineFlow` or data-driven rows
32
+ - [plugin-postgis](/docs/plugins/postgis) — postgres-native `geography` / `geometry` columns + spatial operators
33
+ - [plugin-broadcast](/docs/plugins/broadcast) — cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects
34
+ - [plugin-webhooks](/docs/plugins/webhooks) — durable incoming + outgoing webhooks (HMAC signing, retries, idempotency)
35
+ - [plugin-atlassian](/docs/plugins/atlassian) — `JiraService` + `ConfluenceService` over the Atlassian APIs
36
+ - [plugin-deactivation](/docs/plugins/deactivation) — `deactivation()` schema mixin (visible, can't log in)
37
+ - [plugin-prometheus](/docs/plugins/prometheus) — Prometheus exporter at `GET /metrics`; scrapes the unified Metrics-API (the same source the dashboard Metrics panel reads)
38
+ - [plugin-datadog](/docs/plugins/datadog) — deep Datadog integration; agentless metrics + opt-in logs + traces (OTLP→Agent) + profiler, trace-correlated
39
+ - [plugin-sentry](/docs/plugins/sentry) — deep Sentry integration; trace-correlated errors + breadcrumbs from the log sink + opt-in performance traces
40
+ - [plugin-flags](/docs/plugins/flags) — feature flags: per-subject / per-tenant targeting, deterministic % rollouts, kill-switch, declarative rpc gating + client UI gating
41
+ - [plugin-notifications](/docs/plugins/notifications) — unified notifications: one send API across email / Slack / SMS / push (first-class APNs/FCM factory) / in-app, channel preferences + in-app inbox, digest/batching, quiet hours, broadcast/topics, durable DataStore-backed store by default
42
+ - [plugin-logship](/docs/plugins/logship) — ship structured logs to Better Stack / Axiom / Loki / any HTTP sink; batched, redacted, fail-soft
43
+ - [plugin-moderation](/docs/plugins/moderation) — moderate user content before commit: keyword or AI provider, block / flag via interceptor + in-handler redact
44
+ - [plugin-search](/docs/plugins/search) — keep an external index (Typesense / Meilisearch / Algolia) in sync via the ChangeEvent tap; tenant-scoped `search.query` + hook
45
+ - [plugin-cdc-out](/docs/plugins/cdc-out) — declarative reverse-ETL: mirror table changes outward to a webhook / Kafka / Snowflake / BigQuery sink through a durable outbox; ordered per pipe, at-least-once from enqueue, dead-lettered
46
+ - [plugin-governance](/docs/plugins/governance) — data governance: retention TTL sweep, GDPR export + erasure, consent ledger, field encryption
47
+ - [plugin-openapi](/docs/plugins/openapi) — OpenAPI 3.1 spec + Swagger-UI docs generated from your `defineRestRoute` descriptors and (opt-in) rpc procedures
48
+ - [plugin-versioning](/docs/plugins/versioning) — full row history + time-travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions`); what-changed-to-what on every write
49
+ - [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, cross-instance
50
+ - [plugin-scim](/docs/plugins/scim) — SCIM 2.0 provisioning (Users + Groups at `/scim/v2`) so an enterprise IdP can create/deactivate users
51
+ - [plugin-sso-saml](/docs/plugins/sso-saml) — enterprise SAML 2.0 SSO: SP-initiated login + Single Logout, ACS, metadata (+ IdP-metadata-URL auto cert rotation, encrypted assertions, SP request signing); mints a framework session
52
+ - [API keys](/docs/configuration/api-keys) — **first-class** (not a plugin): `apiKeys: true` enables Bearer-key auth + admin-gated issue/list/revoke
53
+ - [Analytics & warehouse sinks](/docs/plugins/analytics) — `AnalyticsSink` contract + five first-party sink plugins (postgres-lite, DuckDB, ClickHouse, Tinybird, PostHog)
54
+ - [External identity providers](/docs/authentication/external-idp) — the six auth-adapter packages (WorkOS, Kinde, Clerk, Auth0, Supabase, generic OIDC)
55
+
56
+ ## The catalogue at a glance
57
+
58
+ Status legend: ✓ shipped · ◐ partial · — planned.
59
+
60
+ | Plugin | Status | What it does |
61
+ |---|---|---|
62
+ | `@voltro/plugin-audit` | ✓ | Mutation audit log + `audit()` mixin |
63
+ | `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
64
+ | `@voltro/plugin-multitenancy` | ✓ | `tenant()` schema mixin (read-scope + write-fill) + `assertOwnTenant` guard + typed `TenantMismatch` |
65
+ | `@voltro/plugin-soft-delete` | ✓ | `softDelete()` schema mixin — `deletedAt` / `deletedBy`; `delete` → UPDATE, `hardDelete()` bypass |
66
+ | `@voltro/plugin-rbac` | ✓ | Roles compile to scopes + the `permission()` handler guard + typed `Forbidden` |
67
+ | `@voltro/plugin-ratelimit` | ✓ | Per-endpoint / per-subject / per-tenant limits; sliding-window / fixed-window / token-bucket; memory / postgres / redis stores |
68
+ | `@voltro/plugin-billing` | ✓ | Subscriptions, plans, entitlements + usage metering over a pluggable provider (Stripe + mock); seat-based billing + mid-cycle proration + dunning (failed-payment retries); `requireEntitlement()` guard + `enforce` interceptor; `/billing/webhook` via plugin-webhooks; money as integer minor units |
69
+ | `@voltro/plugin-mail` | ✓ | Transactional email — Resend / Postmark / SendGrid / SES / Mailgun / SMTP, *.email.tsx templates, per-tenant suppression, send-time scheduling, bulk/batch send, per-send idempotency, durable via workflows |
70
+ | `@voltro/plugin-storage` | ✓ | File storage — public (CDN-direct) + private (access policy + per-object grants), S3 / R2 / GCS / MinIO / filesystem providers, presigned URLs, `listRefs` browse/search, HTTP Range (206) serving, dashboard browser |
71
+ | `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
72
+ | `@voltro/plugin-broadcast` | ✓ | Cross-replica reactivity — fans out app-mutation change events to every replica over a pub/sub bus (Redis / NATS). Closes the single-instance gap for every non-postgres dialect. [→ details](/docs/plugins/broadcast) |
73
+ | `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `defineOutgoingEvent` (durable delivery workflow, HMAC signing, retries, filters). [→ details](/docs/plugins/webhooks) |
74
+ | `@voltro/plugin-auth-{workos,kinde,clerk,auth0,supabase,oidc}` | ✓ | Six IdP adapters over the shared `jwtBearerStrategy` — JWKS verify + claims→tenant mapping; WorkOS additionally ships hosted-login OAuth primitives (`workosAuthorizationUrl` / `workosAuthenticateWithCode`) for a redirect-based SSO login flow. [→ details](/docs/authentication/external-idp) |
75
+ | `@voltro/plugin-analytics-postgres` | ✓ | First-party lite — events on the main DataStore, cross-dialect (postgres / mysql / mariadb / mssql / sqlite / turso). [→ details](/docs/plugins/analytics#voltroplugin-analytics-postgres) |
76
+ | `@voltro/plugin-duckdb` | ✓ | Embedded DuckDB sidecar — real OLAP performance, no external service. [→ details](/docs/plugins/analytics#voltroplugin-duckdb) |
77
+ | `@voltro/plugin-clickhouse` | ✓ | Production OLAP via the official ClickHouse client. [→ details](/docs/plugins/analytics#voltroplugin-clickhouse) |
78
+ | `@voltro/plugin-tinybird` | ✓ | Hosted ClickHouse via Events API + Pipes. [→ details](/docs/plugins/analytics#voltroplugin-tinybird) |
79
+ | `@voltro/plugin-posthog` | ✓ | Product analytics — track-only; compose with another sink for reads. [→ details](/docs/plugins/analytics#voltroplugin-posthog) |
80
+ | `@voltro/plugin-atlassian` | ✓ | `JiraService` + `ConfluenceService` over the Atlassian REST / Greenhopper / Agile APIs — PAT **or** OAuth 2.0 (3LO) auth, transient retry, SSRF guard, comment-write, signature-verified inbound webhooks, avatar proxy, per-tenant cache. [→ details](/docs/plugins/atlassian) |
81
+ | `@voltro/plugin-deactivation` | ✓ | `deactivation()` schema mixin — `deactivatedAt` + `deactivatedBy` (→ Actor); subject can't log in but data stays visible. [→ details](/docs/plugins/deactivation) |
82
+ | `@voltro/plugin-prometheus` | ✓ | Prometheus exporter — `GET /metrics` in text exposition format over the unified Metrics-API (Effect `MetricRegistry`); counters / histograms / gauges + custom metrics, optional bearer gate + node process metrics. [→ details](/docs/plugins/prometheus) |
83
+ | `@voltro/plugin-datadog` | ✓ | Deep Datadog integration — agentless metrics push to `/api/v2/series` + opt-in logs (`/api/v2/logs`, `dd.trace_id`-correlated) + traces (OTLP→Agent) + dd-trace profiler; `DD_API_KEY`/`DD_SITE` + unified service tagging, fail-soft. [→ details](/docs/plugins/datadog) |
84
+ | `@voltro/plugin-sentry` | ✓ | Deep Sentry integration — mutation/query/action errors reported correlated to the active trace (trace_id + span_id) + breadcrumbs from the framework log sink; opt-in performance traces (`SentrySpanProcessor`, OTel-consumer mode) + profiler. `@sentry/*` optional + lazy. [→ details](/docs/plugins/sentry) |
85
+ | `@voltro/plugin-flags` | ✓ | Feature flags — per-subject / per-tenant targeting + deterministic % rollout (FNV-1a bucket) + kill-switch; multivariate variant flags + scheduled / ramping rollouts + a durable kill-switch audit trail; declarative `gatedBy` (typed `FlagDisabled`) + `requireFlag` guard + `flags.evaluate` / `flags.variants` routes + `useFlags`/`useFlag`/`useVariant` hooks; memory / postgres store. [→ details](/docs/plugins/flags) |
86
+ | `@voltro/plugin-notifications` | ✓ | Unified notifications — one `send` across email / Slack / SMS / push (first-class `pushChannel` APNs/FCM factory) / in-app channels, per-user channel preferences, in-app inbox + unread count + delivery records; digest/batching rollup, per-subject quiet hours (DND), broadcast/topic fan-out; `NotificationService` + `useInbox`/`useUnreadCount`/`useMarkRead`/`useTopicSubscription`/`useQuietHours` hooks; durable DataStore-backed store by default (auto-migrated `notification_*` tables). [→ details](/docs/plugins/notifications) |
87
+ | `@voltro/plugin-logship` | ✓ | Ship structured logs to Better Stack / Axiom / Loki / any HTTP sink — rides the log-sink hook, batched + redacted + fail-soft, trace-correlated. [→ details](/docs/plugins/logship) |
88
+ | `@voltro/plugin-moderation` | ✓ | Content moderation — keyword denylist or AI provider (fails open), block (typed `ContentRejected`) / flag via rpc interceptor + in-handler `moderate()` redact helper. [→ details](/docs/plugins/moderation) |
89
+ | `@voltro/plugin-search` | ✓ | External search index sync — rides the ChangeEvent tap to mirror tables into Typesense / Meilisearch / Algolia (memory default), tenant-scoped `search.query` action (facets · highlighting · fuzziness · range/negation filters · engine-param passthrough) + `useSearch` hook + `backfillIndex` + durable cross-replica sync stats. [→ details](/docs/plugins/search) |
90
+ | `@voltro/plugin-cdc-out` | ◐ | Declarative reverse-ETL — mirror table changes outward to external sinks (webhook / Kafka / Snowflake / BigQuery) through a durable outbox; ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. Engine + memory/webhook sinks shipped; warehouse connectors implement the `CdcSink` interface. |
91
+ | `@voltro/plugin-governance` | ✓ | Data governance — retention TTL sweep (delete / anonymise), GDPR subject export + erasure (admin-gated routes + `GovernanceService`), consent ledger, field encryption. [→ details](/docs/plugins/governance) |
92
+ | `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
93
+ | `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
94
+ | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator, swept `_voltro_presence` table, cross-instance. [→ details](/docs/plugins/presence) |
95
+ | `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `scim_users`/`scim_groups`. [→ details](/docs/plugins/scim) |
96
+ | `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
97
+
98
+ API keys are **first-class** (not a plugin): `apiKeys: true` in `app.config.ts` → Bearer-key auth + admin-gated `/v1/api-keys` management, hash-only storage. [→ details](/docs/configuration/api-keys)
99
+
100
+ Every plugin carries a design doc in the framework's `plans/` directory before it ships.
101
+
102
+ ## Configuration shape
103
+
104
+ ```ts
105
+ // app.config.ts
106
+ import { auditPlugin } from '@voltro/plugin-audit'
107
+ import { rateLimitPlugin } from '@voltro/plugin-ratelimit'
108
+
109
+ export default {
110
+ type: 'api' as const,
111
+ name: 'api',
112
+ plugins: [
113
+ rateLimitPlugin({ default: { limit: 60, window: '1m' } }),
114
+ auditPlugin({ sink: 'console' }),
115
+ ],
116
+ }
117
+ ```
118
+
119
+ Order matters: the framework composes outer→inner, so the rate-limit interceptor runs *before* the audit interceptor sees the request. Rejected requests never enter the audit log.
120
+
121
+ ## What plugins can do
122
+
123
+ | Surface | What it lets you do |
124
+ |---|---|
125
+ | `interceptMutation` / `interceptQuery` / `interceptAction` | Wrap every mutation / query-setup / action — gate, audit, transform input/output. |
126
+ | `extendSchema` | Contribute tables + custom SQL migrations (tracked in `_voltro_plugin_migrations`). |
127
+ | `services` | Provide an Effect `Layer` whose Tags every handler can `yield*` (e.g. `MailService`, `StorageService`). |
128
+ | `routes` | Register plugin-owned rpc queries / mutations / actions (alias-prefixed tags). |
129
+ | `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). |
130
+ | `inspectEndpoints` | Mount tooling under `/_voltro/inspect/plugins/<alias>/…`. |
131
+ | `onScheduleFire` / `onWorkflowStep` / `onHttpRequest` | Wrap every cron firing, every workflow `step()`, every pre-auth HTTP request. |
132
+ | `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
133
+ | Schema mixins (`defineMixin`) | The OTHER plugin shape — `audit()`, `tenant()`, `softDelete()` — declared in `@voltro/database`, not via the runtime contract. |
134
+
135
+ ## When NOT to write a plugin
136
+
137
+ - **One-off side effect** — just call it from the mutation directly.
138
+ - **App-specific behaviour** — keep it in app code, not a reusable plugin.
139
+ - **Anything cross-cutting that only affects ONE mutation** — a single `await ctx.audit.log(...)` call beats a plugin's hook.
140
+
141
+ Plugins are for cross-cutting concerns. Audit-log every write, rate-limit every mutation, send a `user.created` event from every sign-up: that's plugin territory.
142
+
143
+ ## Where to read next
144
+
145
+ - [The plugin contract](/docs/plugins/contract) — write your own
146
+ - [plugin-audit](/docs/plugins/audit) — most complete reference implementation
147
+
148
+
149
+
150
+ ---
151
+
152
+ <!-- source: en/plugins/contract.md -->
153
+ ## Plugin contract
154
+
155
+ _definePlugin, lifecycle hooks, rpc interceptors, framework-version compatibility — the surface every Voltro plugin implements._
156
+
157
+ A **plugin** is the unit of cross-cutting framework extension. Examples in the wild:
158
+
159
+ - `@voltro/plugin-audit` records every mutation invocation to a sink.
160
+ - `@voltro/plugin-multitenancy` adds the `tenant()` schema mixin + write-guard.
161
+ - `@voltro/plugin-webhooks` ships incoming + outgoing webhook tables + workflow.
162
+ - `@voltro/plugin-auth-workos` (and 5 siblings) plug an external IdP into the auth chain.
163
+
164
+ Plugins live as npm packages, get listed in `app.config.ts`'s `plugins:` array, and the framework wires their hooks at boot. The contract is intentionally narrow — a plugin is NOT a full app extension; it's a focused cross-cutting concern that pairs with the existing query / mutation / action / workflow primitives.
165
+
166
+ ## The shape
167
+
168
+ ```ts
169
+ import { definePlugin } from '@voltro/protocol'
170
+
171
+ export const myPlugin = (options: MyOptions) =>
172
+ definePlugin({
173
+ name: '@vendor/my-plugin', // stable id — surfaced in boot logs + dashboard
174
+ description: 'rate-limits outbound HTTP per tenant',
175
+ framework: '^1.0.0', // semver range — soft-checked at boot
176
+ interceptMutation: async (next, ctx) => { /* wraps every mutation */ },
177
+ interceptAction: async (next, ctx) => { /* wraps every action */ },
178
+ onActivate: (lifecycle) => { /* one-shot boot: open pools, register metrics */ },
179
+ onDeactivate: (lifecycle) => { /* graceful shutdown */ },
180
+ })
181
+ ```
182
+
183
+ `definePlugin` is an identity function with type-level enforcement — it returns the input unchanged at runtime, but the type-checker catches missing required fields, misnamed hooks, and excess properties at the declaration site. Always use it over plain object literals.
184
+
185
+ ## Hooks
186
+
187
+ ### `interceptMutation` / `interceptAction` (Effect-native)
188
+
189
+ Wrap every mutation or action call respectively. **Both are Effect-based** —
190
+ the framework is Effect end-to-end and the plugin boundary preserves that
191
+ shape so tracing, interruption, and the typed error channel flow through
192
+ without round-tripping to Promise:
193
+
194
+ ```ts
195
+ type RpcInterceptor = (
196
+ next: Effect.Effect<unknown, unknown>,
197
+ context: {
198
+ readonly tag: string // 'todos.create', 'support.ping', …
199
+ readonly kind: 'mutation' | 'action' // discriminator
200
+ readonly input: unknown // the validated payload
201
+ readonly subject: Subject // resolved by AuthMiddleware
202
+ readonly traceId: string // matches OTel spans + logs
203
+ },
204
+ ) => Effect.Effect<unknown, unknown>
205
+ ```
206
+
207
+ The interceptor MUST flow `next` through somehow — yield it, pipe through it,
208
+ or return it. Whatever the returned Effect produces becomes the final result
209
+ UNLESS the interceptor substitutes a different one (cache hits, etc).
210
+ Failing the returned Effect skips the rest of the chain and surfaces the
211
+ error to the rpc layer.
212
+
213
+ ```ts
214
+ // Pre-only: short-circuit before the executor runs.
215
+ const guard: RpcInterceptor = (next, ctx) =>
216
+ ctx.tag.startsWith('admin.') && ctx.subject.type !== 'user'
217
+ ? Effect.fail(new Forbidden({ tag: ctx.tag }))
218
+ : next
219
+
220
+ // Post-only: tap the success/failure channels.
221
+ const log: RpcInterceptor = (next, ctx) =>
222
+ next.pipe(
223
+ Effect.tap((result) => recordAudit(ctx.tag, ctx.input, result, ctx.subject)),
224
+ Effect.tapError((err) => recordAuditError(ctx.tag, err)),
225
+ )
226
+
227
+ // Tracing: spans nest automatically.
228
+ const trace: RpcInterceptor = (next, ctx) =>
229
+ next.pipe(Effect.withSpan(`plugin.${ctx.tag}`))
230
+ ```
231
+
232
+ **Composition.** When multiple plugins each install `interceptMutation`, the framework composes them in declaration order: the first plugin in `app.config.ts`'s `plugins:` array is the OUTERMOST wrapper, the last is closest to the executor. That mirrors how middleware composition normally reads top-to-bottom in user code.
233
+
234
+ **Why three hooks instead of one.** A plugin that only wants to govern outbound HTTP (rate limiting, tenant-scoped IO quotas) installs `interceptAction` without touching mutations. A plugin that records writes (audit log) installs `interceptMutation` without touching reads or actions. The split lets plugins opt in narrowly.
235
+
236
+ `interceptQuery` wraps subscription **setup** — the one-time call that produces the `QueryDescriptor`. Use it for pre-setup authz (deny before the subscription opens), per-tenant subscription-rate-limiting, or subscription-open audit logging. **It does NOT wrap every snapshot/delta delivery** — per-delta observability flows through the framework's OTel spans (`subscription.<tag>.snapshot` / `.delta`); wrapping every delivery would add per-event latency the streaming model is specifically designed to avoid.
237
+
238
+ ```ts
239
+ interceptQuery: (next, ctx) =>
240
+ ctx.subject.type === 'anonymous' && ctx.tag.startsWith('admin.')
241
+ ? Effect.fail(new Unauthenticated({ reason: 'admin queries require authentication' }))
242
+ : next,
243
+ ```
244
+
245
+ A failure inside an `interceptQuery` aborts the subscription open; the rpc layer surfaces it to the client as a subscription error. Substituting the return Effect is supported but rare — usually interceptors do pre-only or post-only side effects.
246
+
247
+ **Per-plugin observability is automatic.** Every interceptor is wrapped at compose time with `Effect.withSpan('plugin.<name>.intercept-<kind>')` + a `plugin.*` metric sample — no opt-in. The DevTools / cloud dashboard's Plugins panel filters `/_voltro/inspect/metrics` by the `plugin.*` prefix to render per-plugin latency + count buckets, and the trace waterfall shows the plugin layer as a nested span inside the handler boundary.
248
+
249
+ ### `onChangeEvent` — the post-commit ChangeEvent tap
250
+
251
+ ```ts
252
+ onChangeEvent: (event: PluginChangeEvent) => Effect.Effect<void, unknown>
253
+ // PluginChangeEvent = {
254
+ // table, op: 'insert'|'update'|'delete', new: Row | null, old: Row | null,
255
+ // origin?: 'inline' | 'injected', changeScope: 'local' | 'fleet',
256
+ // }
257
+ ```
258
+
259
+ A tap on the store's post-commit change stream — the plugin sees every committed insert/update/delete. It returns an `Effect<void, E>` that the runtime **supervises**: it forks the Effect under the plugin's supervision scope (so the tap is non-blocking and can never back-pressure the change stream or the writing mutation) and routes the Effect's failure channel to the plugin-scoped logger. That gives a change-tap a real, typed error channel — `Effect.retry`, `Effect.timeout`, `Effect.catchTag`, a durable enqueue — instead of fire-and-forget glue.
260
+
261
+ ```ts
262
+ onChangeEvent: (event) =>
263
+ event.table !== 'orders'
264
+ ? Effect.void // not my table — no-op
265
+ : Effect.tryPromise(() => mirror(event)).pipe( // failure is logged by the runtime
266
+ Effect.retry({ times: 3 }),
267
+ )
268
+ ```
269
+
270
+ Runs under BOTH `voltro dev` and `voltro serve` (the prod serve path fans out the same way). Requires the `'store:changes:read'` permission. It is NOT durable at the framework layer — a crash between commit and the fork loses the event; build durability INSIDE the Effect (insert into an outbox and retry against the typed error channel, the way `@voltro/plugin-cdc-out` does). Exactly-once / change-scope semantics are unchanged: read `event.origin` + `event.changeScope` inside the Effect to act once per change fleet-wide (skip `origin: 'injected'` on `'local'` scope; elect one worker on `'fleet'`). Used by `@voltro/plugin-search` to mirror rows into an external index. For in-transaction reactions use a mutation; for best-effort per-table reactions in app code prefer a `*.subscribe.ts` — `onChangeEvent` is the plugin-level equivalent.
271
+
272
+ ### `onInstall` / `onUninstall` / `onActivate` / `onDeactivate` lifecycle
273
+
274
+ Four lifecycle hooks, all accepting Effect / Promise / sync return values.
275
+ The framework awaits each in turn — sync returns settle synchronously,
276
+ Promises and Effects are awaited.
277
+
278
+ ```ts
279
+ interface PluginLifecycleContext {
280
+ readonly app: {
281
+ readonly name: string
282
+ readonly type: 'api' | 'web'
283
+ readonly voltroVersion: string
284
+ }
285
+ readonly logger: { info; warn; error; debug } // scoped to the plugin's name
286
+ readonly env: NodeJS.ProcessEnv
287
+ readonly config: unknown // validated against configSchema if declared
288
+ }
289
+ ```
290
+
291
+ - **`onInstall`** runs ONCE per process per `plugin@version` — the
292
+ FIRST time the host sees this plugin. Use for schema migrations + resource
293
+ provisioning that survive across deactivate/activate cycles. In v1 the
294
+ install marker is in-memory; persisted state (`_voltro_plugin_installs`
295
+ table) lands with the migration runner. Failure aborts boot.
296
+ - **`onUninstall`** is the teardown mirror of `onInstall` — declare it
297
+ to drop tables, delete queues, etc. when the plugin is removed from
298
+ the app. Keep it idempotent (re-running is a no-op once torn down).
299
+ - **`onActivate`** runs every boot. Use for warming caches, opening
300
+ connection pools, registering metrics sinks. The framework awaits each
301
+ hook sequentially in declaration order. **Throwing/failing aborts boot** —
302
+ better to surface a misconfigured plugin loudly than start the rpc
303
+ server with half-wired plugins.
304
+ - **`onDeactivate`** runs in **REVERSE declaration order** (mirror of
305
+ activate — the most-recently-activated plugin tears down first). Each
306
+ hook has a **5-second per-plugin grace window** and the whole sequence
307
+ is capped at **30 seconds total**. A plugin that throws or exceeds its
308
+ grace window logs a warning + the sequence moves on; shutdown never
309
+ blocks on a stuck plugin. **Idempotent** — repeated SIGTERM doesn't
310
+ re-run hooks. Hookup covers SIGINT + SIGTERM in both `voltro dev` and
311
+ `voltro start`.
312
+
313
+ ### `framework` compatibility
314
+
315
+ ```ts
316
+ definePlugin({
317
+ name: '@vendor/x',
318
+ framework: '^1.0.0', // accepts 1.x, rejects 0.x and 2.x
319
+ // OR: '~1.2.0', '>=1.0.0 <2.0.0', '1.2.3' (exact), '*' (any)
320
+ })
321
+ ```
322
+
323
+ At boot the framework runs `checkFrameworkCompat(plugin.name, plugin.framework, runningVoltroVersion)`. Mismatch logs a `WARN` with the constraint + the running version, and **boot continues by default** — the operator decides whether to pin a different version or upgrade. Set `VOLTRO_STRICT_PLUGIN_COMPAT=1` to make a mismatch a hard boot-abort instead (the plugin throws at boot) — for CI or regulated deployments that must refuse to run a plugin built against a different framework version.
324
+
325
+ Absent `framework` field → no compat check. Suitable for in-tree plugins that ship lockstep with the framework.
326
+
327
+ ## What plugins CAN'T do (initial policy)
328
+
329
+ - **Mutate other plugins' state.** Plugins don't talk to each other directly. If two plugins need to coordinate, it's via the rpc layer (one plugin's interceptor sees the other's `subject.metadata`, for example).
330
+ - **Bypass tenant scoping.** Interceptors run AFTER the runtime's tenant predicate merge. A mutation interceptor can't query rows from another tenant by manipulating `subject.tenantId`.
331
+ - **Access raw secrets directly.** Plugins get config via their own factory function's options object. `lifecycle.env` exposes `process.env` but only the plugin's own factory chooses which env vars to read.
332
+ - **Modify the core schema DSL or query builder.** Schema extension happens through schema mixins (the `*.mixin.ts` pattern in `@voltro/database`); not through plugin runtime hooks.
333
+
334
+ These boundaries hold for v1; some may relax for verified plugins once the marketplace ships.
335
+
336
+ ## Stacking
337
+
338
+ ```ts
339
+ // app.config.ts
340
+ import { auditPlugin } from '@voltro/plugin-audit'
341
+ import { rateLimitPlugin } from '@vendor/rate-limit'
342
+ import { metricsPlugin } from '@vendor/metrics'
343
+
344
+ export default {
345
+ type: 'api' as const,
346
+ name: 'myApi',
347
+ plugins: [
348
+ metricsPlugin({ sink: 'datadog' }), // OUTERMOST — sees every request first
349
+ rateLimitPlugin({ perTenant: 100 }),
350
+ auditPlugin({ sink: 'console' }), // INNERMOST — closest to executor
351
+ ],
352
+ }
353
+ ```
354
+
355
+ The first plugin's `interceptMutation` wraps the second's, which wraps the third's, which wraps the executor. Same composition for `interceptAction`. The chain runs in deterministic order independent of import order or filesystem walk — only `plugins:` array order matters.
356
+
357
+ ### `services: Layer` — contribute Tags into the per-request runtime
358
+
359
+ A plugin can provide an Effect Layer whose Tags become available to
360
+ EVERY handler in the app. Handlers `yield* MyTag` to read; plugins own
361
+ the implementation; consumers don't import the plugin directly.
362
+
363
+ ```ts
364
+ import { Effect, Layer } from 'effect'
365
+ import { definePlugin, definePluginService } from '@voltro/protocol'
366
+
367
+ interface AuditService {
368
+ readonly record: (event: { tag: string; actor: string }) => Effect.Effect<void>
369
+ }
370
+
371
+ const { Tag: Audit, Live: AuditLive } = definePluginService<AuditService, AuditService>(
372
+ '@vendor/audit/Service',
373
+ {
374
+ record: (event) => Effect.sync(() => console.log('AUDIT', event)),
375
+ },
376
+ )
377
+
378
+ export const auditPlugin = (): VoltroPlugin =>
379
+ definePlugin({
380
+ name: '@vendor/audit',
381
+ services: AuditLive,
382
+ })
383
+
384
+ // In any handler:
385
+ // import { Audit } from '@vendor/audit'
386
+ // const audit = yield* Audit
387
+ // yield* audit.record({ tag: 'todos.create', actor: 'u_42' })
388
+ ```
389
+
390
+ Layer composition is independent of declaration order across plugins
391
+ (Effect's `mergeAll` is commutative on Tag identity); collisions on the
392
+ SAME Tag resolve to the LAST layer in the merge list, so a user layer
393
+ in `apiConfig.layers` can override a plugin layer with the same Tag.
394
+
395
+ ### `routes: PluginRpcRoute[]` — plugins ship their own RPC endpoints
396
+
397
+ Plugins can register their own queries / mutations / actions alongside
398
+ user-authored queries. Same wire protocol, same dashboard surface, same
399
+ interceptor + tracing wiring. Query tags carry a plugin-derived prefix
400
+ so plugin queries never collide with user queries:
401
+
402
+ ```ts
403
+ import { definePlugin, definePluginRoute } from '@voltro/protocol'
404
+ import { Effect, Schema } from 'effect'
405
+
406
+ export const auditPlugin = (): VoltroPlugin =>
407
+ definePlugin({
408
+ name: '@voltro/plugin-audit',
409
+ routes: [
410
+ definePluginRoute({
411
+ kind: 'query',
412
+ name: 'list', // tag = 'audit.list' (plugin alias prepended)
413
+ input: Schema.Struct({ limit: Schema.optional(Schema.Number) }),
414
+ output: Schema.Array(AuditEventSchema),
415
+ execute: (input) => Effect.gen(function* () {
416
+ const buffer = yield* AuditBuffer // service Tag from plugin's `services` layer
417
+ return buffer.read(input.limit ?? 100)
418
+ }),
419
+ }),
420
+ definePluginRoute({
421
+ kind: 'mutation',
422
+ name: 'clear', // tag = 'audit.clear'
423
+ input: Schema.Struct({}),
424
+ output: Schema.Struct({ cleared: Schema.Number }),
425
+ execute: () => Effect.sync(() => {
426
+ // clear logic — runs inside the framework's transactional wrap
427
+ return { cleared: 0 }
428
+ }),
429
+ }),
430
+ ],
431
+ })
432
+ ```
433
+
434
+ Tag derivation: `<plugin-alias>.<query.name>` unless `query.name` contains
435
+ a dot (escape hatch). Plugin alias strips `@scope/` + the `plugin-` prefix
436
+ and kebab→camelCase:
437
+
438
+ | Plugin name | Alias |
439
+ |---|---|
440
+ | `@voltro/plugin-audit` | `audit` |
441
+ | `@scope/plugin-rateLimit` | `rateLimit` |
442
+ | `@scope/plugin-rate-limit` | `rateLimit` |
443
+ | `plain-name` | `plainName` |
444
+ | `@voltro/audit` | `audit` (no `plugin-` to strip) |
445
+
446
+ Boot fails with a clear error on tag collisions (between two plugins, or
447
+ with a user-authored tag).
448
+
449
+ #### Reactive plugin queries — `source`
450
+
451
+ A plugin query can be **push-driven** instead of poll-only. Add a `source`
452
+ table (or tables) to a `kind: 'query'` route and the framework re-runs the
453
+ executor and pushes a fresh result over the SAME subscription/WS transport
454
+ app reactive queries use — every time one of those tables changes. No new
455
+ push system: it reuses the framework's computed-reactive-query machinery, so
456
+ the client just subscribes and receives live updates.
457
+
458
+ ```ts
459
+ definePluginRoute({
460
+ kind: 'query',
461
+ name: 'list',
462
+ source: '_voltro_presence', // ← reactive: re-run + push on any change to this table
463
+ input: Schema.Struct({ channel: Schema.String }),
464
+ output: Schema.Array(MemberSchema),
465
+ // The executor returns the query's VALUE (the same shape it returns when
466
+ // polled); the framework recomputes + pushes it on change.
467
+ execute: (input, ctx) => Effect.gen(function* () {
468
+ // read the table, shape the roster, return it
469
+ return roster
470
+ }),
471
+ })
472
+ ```
473
+
474
+ `source` must name a table declared `.reactive()`. Omit `source` for a plain
475
+ poll-only plugin query, a mutation, or an action. This is exactly how
476
+ `@voltro/plugin-presence` makes `presence.list` push-driven — the
477
+ `usePresence` roster updates live with no client polling.
478
+
479
+ ### `permissions: PluginPermission[]` + `configSchema: Schema` — manifest fields
480
+
481
+ ```ts
482
+ definePlugin({
483
+ name: '@vendor/audit',
484
+ version: '1.2.3',
485
+ permissions: [
486
+ 'rpc:intercept:mutation', // matches interceptMutation
487
+ 'rpc:intercept:query', // matches interceptQuery
488
+ 'secrets:read:audit:*', // pattern-perm (runtime check)
489
+ ],
490
+ configSchema: Schema.Struct({
491
+ sink: Schema.Literal('console', 'memory', 'postgres'),
492
+ verbose: Schema.optional(Schema.Boolean),
493
+ }),
494
+ })
495
+ ```
496
+
497
+ `permissions` is a **typed enum**. The framework audits every plugin's
498
+ declared set against its hook surfaces at boot — a plugin that ships
499
+ `onHttpRequest` without declaring `'http:intercept'` (or any other
500
+ hook-without-perm combination) fails boot with the specific scope name.
501
+ No silent strip; no advisory warnings; the operator either grants the
502
+ perm or the plugin doesn't run.
503
+
504
+ The static surface is a union of well-known scopes; pattern perms
505
+ (`'secrets:read:foo:*'`, `'network:outbound:api.stripe.com'`,
506
+ `'plugin:hook:<other-plugin>'`) fall through the template-literal arm.
507
+ Runtime resource checks use `permissionMatches(declared, required)` —
508
+ a declared `'secrets:read:auth0:*'` covers a required
509
+ `'secrets:read:auth0:clientSecret'`.
510
+
511
+ | Hook surface | Required permission |
512
+ |---|---|
513
+ | `interceptMutation` | `'rpc:intercept:mutation'` |
514
+ | `interceptQuery` | `'rpc:intercept:query'` |
515
+ | `interceptAction` | `'rpc:intercept:action'` |
516
+ | `onScheduleFire` | `'schedule:fire'` |
517
+ | `onWorkflowStep` | `'workflow:step'` |
518
+ | `onHttpRequest` | `'http:intercept'` |
519
+ | `onChangeEvent` | `'store:changes:read'` |
520
+ | `inspectEndpoints` (non-empty) | `'inspect:read'` |
521
+ | `dashboard` (non-empty) | `'dashboard:mount'` |
522
+ | `extendSchema.tables` (non-empty) | `'store:write'` |
523
+ | `extendSchema.migrations` (non-empty) | `'migration:run'` |
524
+
525
+ `configSchema` decodes the operator's user-supplied config payload at
526
+ boot. Decode failures abort boot with a typed error pointing at the
527
+ plugin. The decoded value lands in `PluginLifecycleContext.config` so
528
+ lifecycle hooks consume the already-validated shape.
529
+
530
+ ## Writing a plugin — minimal example
531
+
532
+ ```ts
533
+ // packages/plugin-rate-limit/src/index.ts
534
+ import { Effect } from 'effect'
535
+ import { definePlugin, type VoltroPlugin } from '@voltro/protocol'
536
+
537
+ interface RateLimitOptions {
538
+ readonly perTenant: number // requests/minute
539
+ }
540
+
541
+ const buckets = new Map<string, { count: number; windowStart: number }>()
542
+
543
+ export const rateLimitPlugin = (options: RateLimitOptions): VoltroPlugin =>
544
+ definePlugin({
545
+ name: '@vendor/rate-limit',
546
+ framework: '^1.0.0',
547
+ interceptMutation: (next, ctx) => {
548
+ if (ctx.subject.type === 'anonymous') return next // no per-tenant limit
549
+ const key = ctx.subject.tenantId
550
+ const now = Date.now()
551
+ const bucket = buckets.get(key) ?? { count: 0, windowStart: now }
552
+ if (now - bucket.windowStart > 60_000) {
553
+ bucket.count = 0
554
+ bucket.windowStart = now
555
+ }
556
+ bucket.count++
557
+ buckets.set(key, bucket)
558
+ if (bucket.count > options.perTenant) {
559
+ return Effect.fail(new Error(`rate limit exceeded for tenant ${key}`))
560
+ }
561
+ return next
562
+ },
563
+ onActivate: ({ logger }) => {
564
+ logger.info('rate-limit plugin armed', { perTenant: options.perTenant })
565
+ },
566
+ })
567
+ ```
568
+
569
+ That's the whole plugin. Drop it in `app.config.ts`, `voltro dev` picks it up, every mutation goes through the rate limiter.
570
+
571
+ ## Schema mixins — the OTHER plugin shape
572
+
573
+ Not every cross-cutting concern needs runtime hooks. Schema extensions live in their own surface — `defineMixin({...})` from `@voltro/database` — and don't go through the `VoltroPlugin` contract at all. See [database/mixins](/docs/database/mixins) for that pattern.
574
+
575
+ A package can ship BOTH a schema mixin AND a runtime plugin (`@voltro/plugin-audit` does — the `audit()` mixin adds columns; the `auditPlugin()` factory installs the mutation interceptor). They're independent — apps that want the schema but not the interceptor `.with(audit())` without `plugins: [auditPlugin()]`, and vice versa.
576
+
577
+ ## More extension surfaces
578
+
579
+ The plugin contract carries eight additional surfaces beyond the
580
+ interceptors + lifecycle + manifest fields covered above:
581
+
582
+ ### `inspectEndpoints: PluginInspectEndpoint[]` — plugin-mounted HTTP endpoints
583
+
584
+ ```ts
585
+ import { definePlugin } from '@voltro/protocol'
586
+ import { Effect } from 'effect'
587
+
588
+ export const auditDebugPlugin = (): VoltroPlugin =>
589
+ definePlugin({
590
+ name: '@voltro/plugin-audit-debug',
591
+ inspectEndpoints: [
592
+ {
593
+ method: 'GET',
594
+ path: 'buffer', // → /_voltro/inspect/plugins/auditDebug/buffer
595
+ description: 'Returns the last 1000 audit events',
596
+ handler: () => Effect.sync(() => ({
597
+ kind: 'json',
598
+ data: { events: readAuditBuffer() },
599
+ })),
600
+ },
601
+ {
602
+ method: 'POST',
603
+ path: 'clear',
604
+ handler: () => Effect.sync(() => {
605
+ clearAuditBuffer()
606
+ return { kind: 'json' as const, data: { cleared: true } }
607
+ }),
608
+ },
609
+ ],
610
+ })
611
+ ```
612
+
613
+ Plugin endpoints inherit the framework's auth resolver — plugins can't
614
+ bypass `VOLTRO_INSPECT_TOKEN`. Path: `/_voltro/inspect/plugins/<plugin-alias>/<path>`.
615
+ Boot fails loudly on collisions (two plugins claiming the same path+method).
616
+
617
+ ### `onScheduleFire: ScheduleFireInterceptor` — wrap every cron firing
618
+
619
+ ```ts
620
+ definePlugin({
621
+ name: '@vendor/schedule-gate',
622
+ onScheduleFire: async (next, ctx) => {
623
+ // Suppress all firings of `nightlyBilling` when the kill-switch is on.
624
+ if (ctx.name === 'nightlyBilling' && (await isKilled('billing'))) {
625
+ return // skip — handler doesn't run
626
+ }
627
+ await next()
628
+ },
629
+ })
630
+ ```
631
+
632
+ Composed across plugins in declaration order; runs INSIDE the schedule's
633
+ `maxRuntimeMs` watchdog. Per-plugin span (`plugin.<name>.schedule-fire`)
634
+ + metric auto-emitted.
635
+
636
+ ### `onWorkflowStep: WorkflowStepInterceptor` — wrap every `step()`
637
+
638
+ ```ts
639
+ import { Effect } from 'effect'
640
+
641
+ definePlugin({
642
+ name: '@vendor/step-observer',
643
+ onWorkflowStep: (next, ctx) =>
644
+ next.pipe(
645
+ Effect.withSpan(`vendor.workflow.${ctx.stepName}`, {
646
+ attributes: { 'workflow.attempt': ctx.attempt },
647
+ }),
648
+ Effect.tap((output) => Effect.sync(() => recordStep(ctx, output))),
649
+ ),
650
+ })
651
+ ```
652
+
653
+ Wraps every `step()` (== `Activity.make`) inside a workflow body. Effect-
654
+ native — the interceptor sees `next` as an Effect and can pipe through
655
+ `tap`/`retry`/`withSpan`. **Bypassed on workflow REPLAY** — the interceptor
656
+ only fires on the first execution per step; on resume after a crash, the
657
+ cached Activity output is replayed without re-running the user effect or
658
+ the plugin interceptor.
659
+
660
+ ### `codegen: PluginCodegen` — emit typed bindings into `rpcGroup.generated.ts`
661
+
662
+ ```ts
663
+ definePlugin({
664
+ name: '@voltro/plugin-audit',
665
+ codegen: (ctx) => `
666
+ // Typed accessor for the audit plugin's inspect endpoint
667
+ export const useAuditBuffer = () =>
668
+ fetch('/_voltro/inspect/plugins/${ctx.pluginAlias}/buffer')
669
+ .then((r) => r.json() as Promise<{ events: ReadonlyArray<AuditEvent> }>)
670
+ `,
671
+ })
672
+ ```
673
+
674
+ The framework emits the returned string into `rpcGroup.generated.ts`
675
+ between `// <plugin:@voltro/plugin-audit>` / `// </plugin:@voltro/plugin-audit>`
676
+ markers. The plugin sees the api name + the list of discovered user-query
677
+ rpc tags so it can emit per-rpc bindings. Returning `null` contributes
678
+ nothing.
679
+
680
+ ### `templates: PluginTemplate[]` — ship `voltro init` templates
681
+
682
+ ```ts
683
+ definePlugin({
684
+ name: '@vendor/plugin-stripe-webhooks',
685
+ templates: [
686
+ {
687
+ id: 'stripe-receiver',
688
+ title: 'Stripe webhook receiver',
689
+ description: 'Verified Stripe-webhook endpoint with replay protection',
690
+ kind: 'api',
691
+ sourcePath: 'templates/stripe-receiver',
692
+ postInstallSteps: [
693
+ 'Set VOLTRO_WEBHOOK_SECRET_STRIPE in your env',
694
+ 'Configure the endpoint URL in your Stripe dashboard',
695
+ ],
696
+ },
697
+ ],
698
+ })
699
+ ```
700
+
701
+ Templates surface under the plugin alias in `voltro list-templates`. The
702
+ registration shape + manifest are part of the public contract, and a
703
+ plugin-provided template is scaffolded exactly like a built-in one —
704
+ `scaffoldFromTemplate` copies the declared tree and substitutes the
705
+ `{{…}}` tokens in both file content AND file/directory names.
706
+
707
+ ### `onHttpRequest: HttpRequestInterceptor` — pre-auth HTTP-pipeline hook
708
+
709
+ Fires at the **very top** of every HTTP request — BEFORE auth resolution,
710
+ BEFORE rpc routing, BEFORE inspect. Composed across plugins in
711
+ declaration order (first listed = outermost). Returning a
712
+ `HttpInterceptResponse` short-circuits the pipeline; returning `null`
713
+ (or calling `next()` and returning its result) lets the request flow
714
+ through to the framework's normal handlers.
715
+
716
+ This is **not** an auth replacement — `AuthMiddleware` still runs on
717
+ the rpc path. Use this hook for concerns that need to fire BEFORE auth:
718
+ rate-limit, geo-block, bot detection, header injection for downstream
719
+ observability. Requires the `'http:intercept'` permission.
720
+
721
+ ```ts
722
+ import { definePlugin } from '@voltro/protocol'
723
+
724
+ export const rateLimitPlugin = (opts: { perMinute: number }) =>
725
+ definePlugin({
726
+ name: '@vendor/plugin-rate-limit',
727
+ permissions: ['http:intercept'],
728
+ onHttpRequest: async (next, ctx) => {
729
+ const remoteAddr = ctx.headers['x-forwarded-for'] ?? ctx.remoteAddr ?? 'unknown'
730
+ if (buckets.consume(remoteAddr, opts.perMinute) === 'exhausted') {
731
+ return {
732
+ status: 429,
733
+ headers: { 'retry-after': '60', 'content-type': 'application/json' },
734
+ body: JSON.stringify({ error: 'rate_limit_exceeded' }),
735
+ }
736
+ }
737
+ return next() // let the request continue
738
+ },
739
+ })
740
+ ```
741
+
742
+ A per-plugin `plugin.<name>.http-intercept` metric is auto-emitted so
743
+ the dashboard's Plugins panel surfaces HTTP-intercept latency next to
744
+ RPC-intercept latency.
745
+
746
+ ### `extendSchema: { tables, migrations }` — contribute schema + migrations
747
+
748
+ A plugin contributes BOTH declarative table descriptors AND custom SQL
749
+ migrations. Tables merge into the user's schema and flow through the
750
+ same idempotent `applySchema()` path. Migrations run after `applySchema`
751
+ against the live `SqlClient` and are tracked in
752
+ `_voltro_plugin_migrations` so each runs exactly once per app database.
753
+
754
+ The ledger key is `<plugin-alias>__<migration.id>` so two plugins can
755
+ each ship `'001-init'` without collision. Failure aborts boot;
756
+ re-runs are no-ops.
757
+
758
+ ```ts
759
+ import { Effect, Schema } from 'effect'
760
+ import { definePlugin } from '@voltro/protocol'
761
+ import { table, id, text, timestamp } from '@voltro/database'
762
+
763
+ const auditLogs = table('audit_logs', {
764
+ id: id({ prefix: 'audit' }),
765
+ actorId: text(),
766
+ action: text(),
767
+ payload: text(),
768
+ createdAt: timestamp().default('now'),
769
+ })
770
+
771
+ export const auditPlugin = () =>
772
+ definePlugin({
773
+ name: '@voltro/plugin-audit',
774
+ permissions: ['store:write', 'migration:run', 'rpc:intercept:mutation'],
775
+ extendSchema: {
776
+ tables: [auditLogs],
777
+ migrations: [
778
+ {
779
+ id: '001-pgcrypto-extension',
780
+ description: 'enable pgcrypto for hashing actor ids',
781
+ up: (sql) => Effect.gen(function* () {
782
+ yield* sql`CREATE EXTENSION IF NOT EXISTS pgcrypto;`.pipe(Effect.orDie)
783
+ }),
784
+ },
785
+ ],
786
+ },
787
+ })
788
+ ```
789
+
790
+ `tables` requires `'store:write'`; `migrations` requires
791
+ `'migration:run'`. Either one missing → boot fails with the specific
792
+ permission name.
793
+
794
+ ### `dashboard: PluginDashboardMount[]` — remote-mounted dashboard surfaces
795
+
796
+ Plugins surface UI in the cloud / devtools dashboard by declaring
797
+ remote-loaded ESM modules. The host runtime fetches the bundle URL at
798
+ mount time, dynamic-imports it, and renders the exported component
799
+ inline (NOT iframe — true in-process mount).
800
+
801
+ Trade-offs vs iframe:
802
+ - The plugin's bundle MUST be ESM with React/Effect as peerDeps; the
803
+ host pins versions. Mismatch logs a warning at mount time but still
804
+ mounts.
805
+ - No CSS isolation. Plugin authors are expected to use scoped Tailwind
806
+ classes (the dashboard ships the framework token set) or CSS modules.
807
+ - Plugin code shares the host JS realm — honor-system sandboxing. The
808
+ `dashboard:mount` permission gate keeps the operator audit trail
809
+ explicit; signing/marketplace verification is the longer-term answer.
810
+
811
+ ```ts
812
+ definePlugin({
813
+ name: '@vendor/plugin-audit',
814
+ permissions: ['dashboard:mount'],
815
+ dashboard: [
816
+ {
817
+ id: 'overview',
818
+ slot: 'page', // 'page' | 'widget' | 'nav'
819
+ query: 'overview', // mounted at /plugins/audit/overview
820
+ label: 'Audit log',
821
+ icon: 'shield', // optional lucide-react icon name
822
+ bundleUrl: 'https://cdn.example.com/audit/v1/dashboard.mjs',
823
+ exportName: 'AuditOverview', // optional; defaults to 'default'
824
+ dashboardVersion: '^1.0.0', // optional compat range
825
+ },
826
+ {
827
+ id: 'recent-events',
828
+ slot: 'widget', // card on the dashboard home
829
+ label: 'Recent audit events',
830
+ bundleUrl: 'https://cdn.example.com/audit/v1/widget.mjs',
831
+ },
832
+ ],
833
+ })
834
+ ```
835
+
836
+ The registry surfaces at `/_voltro/inspect/plugins/dashboard-mounts`:
837
+
838
+ ```bash
839
+ curl http://localhost:4000/_voltro/inspect/plugins/dashboard-mounts | jq
840
+ # { "mounts": [...], "capturedAt": 1717142400000 }
841
+ ```
842
+
843
+ Boot validates within-plugin id uniqueness AND that `slot: 'page'`
844
+ mounts carry a `query`. Cross-plugin ids are namespaced as
845
+ `<plugin-alias>:<mount.id>` (e.g. `audit:overview`).
846
+
847
+ The actual dashboard-host runtime — the React shell that dynamic-imports
848
+ + mounts the component — lives in `voltro-cloud-dashboard` +
849
+ `voltro-devtools`. The framework owns the registration shape + manifest;
850
+ the consumers own the renderer.
851
+
852
+ ### `bindDataStore(store, ctx)` — bind DB-backed resources + framework handles
853
+
854
+ `onActivate` runs BEFORE the app's `DataStore` exists (the framework
855
+ activates plugins, THEN opens the store). `bindDataStore` is the
856
+ post-store hook: the framework calls it once, after the store + pool are
857
+ open, so a plugin can bind a resource it couldn't build from static
858
+ config — e.g. swap an in-memory ref store for a `DataStore`-backed one.
859
+
860
+ The second argument, `ctx: PluginBindContext`, hands the plugin the
861
+ framework's ALREADY-OPEN handles so it never rebuilds them:
862
+
863
+ - **`ctx.sql`** — the framework's live `SqlClient` (from `@effect/sql`),
864
+ the SAME pool the app's store uses. Run raw SQL through it instead of
865
+ standing up your OWN `ManagedRuntime` + pool from env. `undefined` on
866
+ the in-memory store (no SQL engine) — guard with `if (ctx.sql)`.
867
+ - **`ctx.scheduleCoordinated(name, intervalMs, effect)`** — run a periodic
868
+ task on ONLY ONE replica per tick, cluster-coordinated via the same
869
+ claim-table exactly-once gate the cron scheduler uses. Replaces the
870
+ hand-rolled `setInterval` a plugin would otherwise run inside
871
+ `bindDataStore` — which fires on EVERY replica, so a 10-pod deployment
872
+ runs the same full-table sweep 10× per interval. On a single-process /
873
+ memory / sqlite deployment it simply runs every tick locally (correct —
874
+ one process needs no fan-out dedup). Returns a handle whose `stop()`
875
+ cancels the task; the framework also stops every armed task at shutdown.
876
+
877
+ ```ts
878
+ import { definePlugin } from '@voltro/protocol'
879
+
880
+ definePlugin({
881
+ name: '@vendor/plugin-presence',
882
+ permissions: ['store:write'],
883
+ bindDataStore: (store, ctx) => {
884
+ // Reuse the framework's open pool — no connFromEnv, no second pool.
885
+ if (ctx?.sql) {
886
+ // ctx.sql is the app's live SqlClient (Effect-native).
887
+ }
888
+ // One coordinated sweep fleet-wide, not one setInterval per replica.
889
+ ctx?.scheduleCoordinated('presence.sweep', 60_000, async () => {
890
+ await store.deleteMany('_voltro_presence', {
891
+ where: { column: 'lastSeen', op: 'lt', value: Date.now() - 120_000 },
892
+ })
893
+ })
894
+ },
895
+ })
896
+ ```
897
+
898
+ `store` is typed as the framework `DataStore`; `ctx` is optional in the
899
+ type (the framework always passes it) so an older single-argument
900
+ `bindDataStore(store)` still compiles. Requires no extra permission
901
+ beyond whatever the store operations themselves need (`store:write` for
902
+ writes).
903
+
904
+ ## Inspecting plugins at runtime
905
+
906
+ Every running app exposes its plugin manifest at `/_voltro/inspect/plugins`:
907
+
908
+ ```bash
909
+ curl http://localhost:4000/_voltro/inspect/plugins | jq
910
+ ```
911
+
912
+ Returns one entry per plugin in `app.config.ts`'s `plugins:` array with:
913
+ - `name`, `version`, `description`, `framework` semver range
914
+ - `permissions: string[]` — declared scopes
915
+ - `hooks` — which interceptor + lifecycle hooks the plugin installed
916
+ - `queries` — `kind:name` tuples for every plugin-contributed query
917
+ - `services: boolean` — whether the plugin contributes a service layer
918
+ - `hasConfigSchema: boolean` — whether `configSchema` is declared
919
+ - `activated`, `installed` — runtime state
920
+
921
+ The DevTools + cloud dashboard's Plugins panel consumes this; AI agents
922
+ grep it to answer "what is this deployment running, with what permissions".
923
+
924
+ Per-plugin metrics surface under the SAME `/_voltro/inspect/metrics`
925
+ endpoint that carries `rpc.*` / `request.*` buckets — plugin buckets
926
+ have `kind: 'plugin'` and tag `plugin.<name>.intercept-<mutation|query|action>`.
927
+ Filter client-side by the `plugin.` prefix to render the per-plugin
928
+ latency table.
929
+
930
+ ## Building integration plugins — `@voltro/integration-http`
931
+
932
+ A plugin that talks to a third-party REST API (Jira, GitHub, GitLab, …) needs
933
+ the same server-side transport concerns every time: token auth, transient
934
+ retry with backoff, per-request timeout, an SSRF host guard, and typed errors.
935
+ `@voltro/integration-http` is that transport core — one implementation shared
936
+ by `@voltro/plugin-atlassian` and any integration plugin you write, so you
937
+ don't re-invent it.
938
+
939
+ `makeHttpClient` binds a base URL, an auth strategy, a retry/timeout policy,
940
+ and a `fetch` impl once, then hands you typed request methods. Every method
941
+ returns `Effect.Effect<A, YourError>` — failures land on **your** error
942
+ channel via the `makeError` you supply, so each integration keeps its own
943
+ `Schema.TaggedError`:
944
+
945
+ ```ts
946
+ import { makeHttpClient, type FetchLike } from '@voltro/integration-http'
947
+ import { Schema } from 'effect'
948
+
949
+ class GithubError extends Schema.TaggedError<GithubError>()('GithubError', {
950
+ message: Schema.String,
951
+ transient: Schema.Boolean,
952
+ status: Schema.optional(Schema.Number),
953
+ code: Schema.optional(Schema.Literal('session_expired')),
954
+ }) {}
955
+
956
+ const github = makeHttpClient<GithubError>({
957
+ baseUrl: 'https://api.github.com',
958
+ auth: () => ({
959
+ authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
960
+ accept: 'application/vnd.github+json',
961
+ }),
962
+ makeError: (a) => new GithubError(a),
963
+ fetchImpl: fetch as unknown as FetchLike,
964
+ })
965
+
966
+ // Each method returns Effect.Effect<…, GithubError>.
967
+ const repo = yield* github.getJson('/repos/acme/widgets')
968
+ const created = yield* github.postJson('/repos/acme/widgets/issues', {
969
+ body: { title: 'bug', body: 'it broke' },
970
+ })
971
+ ```
972
+
973
+ What the core does for you: retries `408/425/429/5xx` with capped exponential
974
+ backoff (jittered, honouring `Retry-After` within the `maxDelayMs` ceiling),
975
+ fails a hanging request as transient after the timeout, pins every request to
976
+ the base URL's host (an off-host or malformed URL is rejected **before** any
977
+ fetch, fail-closed), maps a `401` to a non-transient `session_expired`
978
+ (re-auth, never retried), and wraps each request in an
979
+ `integration-http.request` span (method + host only — never a token). When the
980
+ token is resolved per request (per-subject credentials), give the client a
981
+ context type and pass `ctx` on each call:
982
+
983
+ ```ts
984
+ const client = makeHttpClient<GithubError, { token: string }>({
985
+ baseUrl: 'https://api.github.com',
986
+ auth: (ctx) => ({ authorization: `Bearer ${ctx.token}` }),
987
+ makeError: (a) => new GithubError(a),
988
+ fetchImpl: fetch as unknown as FetchLike,
989
+ })
990
+
991
+ const me = yield* client.getJson('/user', { ctx: { token: perSubjectToken } })
992
+ ```
993
+
994
+ > **Server-side only.** This module performs outbound network I/O — never
995
+ > import it (or a module that imports it) from a browser-loaded query /
996
+ > mutation / action / workflow descriptor.
997
+
998
+ ## Related
999
+
1000
+ - [Schema mixins](/docs/database/mixins) — the OTHER plugin shape (schema-only, no runtime hooks).
1001
+ - [Auth strategies](/docs/authentication/strategies) — a specialised plugin pattern for identity providers.
1002
+
1003
+
1004
+
1005
+ ---
1006
+
1007
+ <!-- source: en/plugins/analytics.md -->
1008
+ ## Analytics & warehouse sinks
1009
+
1010
+ _AnalyticsSink contract — narrow cross-provider API for track / aggregate / timeseries / topN — plus the five first-party sink plugins (postgres-lite, DuckDB, ClickHouse, Tinybird, PostHog) and composeAnalytics for dual-write._
1011
+
1012
+ Voltro ships a narrow, cross-provider analytics contract (`AnalyticsSink`) and five first-party plugins that implement it. Mirror of the auth-strategy pattern: one typed interface, multiple providers, swap them in `app.config.ts` without touching handler code.
1013
+
1014
+ The intent is **"go very far before you reach for an external tool"** — but make external tools a one-line install when you do. The first-party lite plugin (`postgres-analytics`) works on day 1 with zero external services and carries you up to ~10M events/day before queries get slow.
1015
+
1016
+ ## The contract — `AnalyticsSink`
1017
+
1018
+ Four methods. Effect-typed throughout. Every sink implements all four; sinks that genuinely can't support a capability return `AnalyticsCapabilityNotSupported` on the typed error channel rather than throwing.
1019
+
1020
+ ```ts
1021
+ import { useAnalytics } from '@voltro/runtime'
1022
+
1023
+ export default (input, ctx) => Effect.gen(function* () {
1024
+ const analytics = yield* useAnalytics()
1025
+
1026
+ yield* analytics.track({
1027
+ name: 'match_completed',
1028
+ subjectId: ctx.request.subject.id,
1029
+ properties: { mapId: 'dust2', durationSec: 1284, mvp: 'player_abc' },
1030
+ })
1031
+
1032
+ // Total over a window
1033
+ const total = yield* analytics.aggregate({
1034
+ event: 'match_completed',
1035
+ metric: 'count',
1036
+ range: { from: hoursAgo(24) },
1037
+ })
1038
+
1039
+ // Time-bucketed series
1040
+ const daily = yield* analytics.timeseries({
1041
+ event: 'match_completed',
1042
+ metric: 'count',
1043
+ bucket: 'day',
1044
+ range: { from: daysAgo(30) },
1045
+ })
1046
+
1047
+ // Top-N by a property
1048
+ const topMaps = yield* analytics.topN({
1049
+ event: 'match_completed',
1050
+ groupBy: 'mapId',
1051
+ metric: 'count',
1052
+ n: 10,
1053
+ range: { from: daysAgo(30) },
1054
+ })
1055
+ })
1056
+ ```
1057
+
1058
+ The contract is deliberately narrow. **No raw SQL, no funnels, no cohorts, no custom dashboards.** Anything provider-specific lives outside the contract — call the provider's API/client directly where you need it (the plugins expose no raw-client escape hatch).
1059
+
1060
+ ## Plugins at a glance
1061
+
1062
+ | Plugin | Type | Best for | Ceiling |
1063
+ |---|---|---|---|
1064
+ | [`@voltro/plugin-analytics-postgres`](#voltroplugin-analytics-postgres) | Lite | Day-1 zero-setup, dev + early production | ~10M events/day |
1065
+ | [`@voltro/plugin-duckdb`](#voltroplugin-duckdb) | Embedded OLAP | Real column-store performance, no external service | Vertical scale: ~hundreds of GB in one process |
1066
+ | [`@voltro/plugin-clickhouse`](#voltroplugin-clickhouse) | External OLAP | Production-scale analytics, self-hosted or ClickHouse Cloud | Billions of events comfortably |
1067
+ | [`@voltro/plugin-tinybird`](#voltroplugin-tinybird) | Hosted ClickHouse | Pay-as-you-go without operating ClickHouse | Tinybird's own limits |
1068
+ | [`@voltro/plugin-posthog`](#voltroplugin-posthog) | Product analytics | Sessions, feature flags, funnels in PostHog's UI | `track()` only — compose with another sink for reads |
1069
+
1070
+ ## Picking one
1071
+
1072
+ Decision rubric in order:
1073
+
1074
+ 1. **First service you don't want to operate?** → `postgres-analytics`. Works against the main DataStore. Cross-dialect.
1075
+ 2. **First-party experience but real OLAP performance?** → `duckdb`. Embedded sidecar, no external service to run.
1076
+ 3. **Past 10M events/day OR want billions-of-rows queries to stay under a second?** → `clickhouse` (self-hosted or Cloud) or `tinybird` (hosted).
1077
+ 4. **Already invested in PostHog for product analytics?** → `posthog` for ingest (alongside a primary sink for aggregates).
1078
+
1079
+ Switching providers later is a one-line config change. Handlers stay identical because they consume the same `AnalyticsSink` contract.
1080
+
1081
+ ## `@voltro/plugin-analytics-postgres`
1082
+
1083
+ First-party lite. Stores events in `_voltro_events` on the main DataStore. Cross-dialect: works on postgres / mysql / mariadb / mssql / sqlite / turso via `sql.onDialectOrElse`.
1084
+
1085
+ ```ts
1086
+ // app.config.ts
1087
+ import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
1088
+
1089
+ export default defineApi({
1090
+ name: 'myApi',
1091
+ store: 'postgres',
1092
+ analytics: postgresAnalytics(),
1093
+ })
1094
+ ```
1095
+
1096
+ What the plugin owns:
1097
+
1098
+ - The `_voltro_events` table — auto-created on first boot (numeric `id`, `name`, `subject_id`, `properties` JSONB, `occurred_at`). Two composite indexes (`name+occurred_at`, `subject_id+occurred_at`) for the hot read paths.
1099
+ - All four methods: `track` writes a row; `aggregate` / `timeseries` / `topN` compile to `date_trunc` / JSON-extract SQL that runs across all six SQL dialects.
1100
+
1101
+ **Honest ceiling**: by ~10M events/day, time-range aggregates over 30 days take >5s on Postgres. The `_voltro_events` table is bounded by a retention sweep (`VOLTRO_EVENTS_TTL_HOURS`, default 365 days) so it never grows without limit; past ~10M events/day, swap to a real OLAP sink before queries get slow.
1102
+
1103
+ **Tenant-scoped reads**: `useAnalytics()` stamps the caller's `subject.tenantId` onto every `track` and every `aggregate` / `timeseries` / `topN`, so a handler can't write or read across tenants. A system/background context with no subject gets the raw (cross-tenant) sink; set an explicit `tenantId` on the event/query only for a deliberate cross-tenant op.
1104
+
1105
+ ## `@voltro/plugin-duckdb`
1106
+
1107
+ Embedded DuckDB sidecar via `@duckdb/node-api`. Real column-store + vectorized execution, in-process — no external service.
1108
+
1109
+ ```ts
1110
+ import { duckdbAnalytics } from '@voltro/plugin-duckdb'
1111
+
1112
+ analytics: duckdbAnalytics({
1113
+ path: '.voltro/analytics.duckdb', // or omit / ':memory:' for ephemeral
1114
+ }),
1115
+ ```
1116
+
1117
+ DuckDB is the quiet win for ~80% of growth-stage apps: real OLAP performance with zero external service to deploy. The plugin owns its own `voltro_events` table inside the DuckDB instance and queries against it.
1118
+
1119
+ Scope:
1120
+
1121
+ - **Events + opt-in CDC-mirror.** `track()` writes to DuckDB; queries read from DuckDB. Pass `mirrorTables` to stream the main DataStore's reactive-table changes into `voltro_mirror_<table>` tables inside DuckDB so analytical queries can JOIN events against live user data (see [CDC-mirror](#cdc-mirror-of-reactive-tables) below). Without `mirrorTables` the sink is events-only.
1122
+ - **Single-process.** DuckDB can't open the same file from multiple workers. For multi-instance deployments either pin analytics traffic to one replica or use `clickhouse` instead.
1123
+
1124
+ ## `@voltro/plugin-clickhouse`
1125
+
1126
+ Production OLAP via the official `@clickhouse/client`. Self-hosted ClickHouse OR ClickHouse Cloud.
1127
+
1128
+ ```ts
1129
+ import { clickhouseAnalytics } from '@voltro/plugin-clickhouse'
1130
+
1131
+ analytics: clickhouseAnalytics({
1132
+ url: process.env.CLICKHOUSE_URL!,
1133
+ database: 'voltro_events',
1134
+ username: process.env.CLICKHOUSE_USER,
1135
+ password: process.env.CLICKHOUSE_PASSWORD,
1136
+ }),
1137
+ ```
1138
+
1139
+ The plugin owns the events table schema (MergeTree engine, `ORDER BY (name, occurred_at, id)`, `LowCardinality(String)` on the event name, ZSTD-compressed properties JSON). First boot creates it; the plugin pings the cluster to fail-fast on bad config.
1140
+
1141
+ **Opt-in batching.** By default every `track()` is one immediate HTTP insert. Pass `batch: { maxSize?, flushIntervalMs? }` to buffer rows and flush them in ONE multi-row insert by size (default 1000), on a timer (default 5000 ms), and on shutdown (a graceful drain before the client closes). This trades per-event delivery confirmation for far fewer round-trips under load — with batching a successful `track()` means "buffered", and a later flush failure is logged + the batch dropped (best-effort), so leave `batch` unset when you need per-event delivery confirmation. Retention (a `TTL` on the events table) stays the operator's job.
1142
+
1143
+ There is no raw-client escape hatch: HyperLogLog, dictionaries, materialised views — the things you actually picked ClickHouse for — live outside the cross-provider contract, and the plugin exposes no handle to the raw `@clickhouse/client`. Where you need them, query ClickHouse with your own client instance against the same tables. Stay on `useAnalytics()` for code that should remain provider-portable.
1144
+
1145
+ ## `@voltro/plugin-tinybird`
1146
+
1147
+ Hosted ClickHouse via Tinybird's Events API + Pipes.
1148
+
1149
+ ```ts
1150
+ import { tinybirdAnalytics } from '@voltro/plugin-tinybird'
1151
+
1152
+ analytics: tinybirdAnalytics({
1153
+ token: process.env.TINYBIRD_TOKEN!,
1154
+ region: 'eu', // 'eu' | 'us-east' | 'us-west' | 'asia-southeast'
1155
+ datasource: 'voltro_events',
1156
+ // pipes: { aggregate: 'my_agg', timeseries: 'my_ts', topN: 'my_top' },
1157
+ }),
1158
+ ```
1159
+
1160
+ **The plugin expects three canonical pipes to exist in your workspace:**
1161
+
1162
+ - `events_aggregate` — receives `event`, `from`, `to`, `metric_expr`, `filter_sql`
1163
+ - `events_timeseries` — same + `bucket_fn`
1164
+ - `events_topn` — same + `group_expr`, `limit`
1165
+
1166
+ We don't synthesize pipes from the framework — Tinybird's `.pipe` DSL is too rich to generate from a generic spec. Push the three canonical pipes once via the `tb` CLI; override names with `pipes: { ... }` if your team uses a different convention.
1167
+
1168
+ ## `@voltro/plugin-posthog`
1169
+
1170
+ PostHog forwards `track()` only. Funnels / cohorts / sessions / feature flags live in PostHog's own UI and SQL — the plugin's `aggregate` / `timeseries` / `topN` return `AnalyticsCapabilityNotSupported` so callers fall back cleanly.
1171
+
1172
+ ```ts
1173
+ import { posthogAnalytics } from '@voltro/plugin-posthog'
1174
+
1175
+ analytics: posthogAnalytics({
1176
+ apiKey: process.env.POSTHOG_KEY!,
1177
+ host: 'https://eu.posthog.com', // optional, default app.posthog.com
1178
+ }),
1179
+ ```
1180
+
1181
+ Typical wiring is **compose** with a primary sink that handles reads (see below) — PostHog mirrors every event for product-analytics insights while your primary sink owns the typed aggregate queries.
1182
+
1183
+ ## composeAnalytics — multi-sink
1184
+
1185
+ For dual-write (e.g. postgres-lite for typed reads + PostHog for product analytics):
1186
+
1187
+ ```ts
1188
+ import { composeAnalytics } from '@voltro/runtime'
1189
+ import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
1190
+ import { posthogAnalytics } from '@voltro/plugin-posthog'
1191
+
1192
+ export default defineApi({
1193
+ name: 'myApi',
1194
+ store: 'postgres',
1195
+ analytics: composeAnalytics([
1196
+ postgresAnalytics(), // primary — handles reads
1197
+ posthogAnalytics({ apiKey: process.env.POSTHOG_KEY! }), // mirrors every track()
1198
+ ]),
1199
+ })
1200
+ ```
1201
+
1202
+ Semantics:
1203
+
1204
+ - **`track()`** fans out to every sink in parallel. Failures are isolated per sink — one provider returning HTTP 503 doesn't fail the postgres insert.
1205
+ - **`aggregate` / `timeseries` / `topN`** query to the **first sink that supports the op**. PostHog doesn't support these → composition queries to the next sink (postgres-lite).
1206
+ - If no sink supports a read op, the composite returns `AnalyticsCapabilityNotSupported({ provider: 'compose' })`.
1207
+
1208
+ ## Capability-not-supported error handling
1209
+
1210
+ The cross-provider contract intentionally surfaces capability gaps on the typed error channel. Catch them when you want graceful fallback:
1211
+
1212
+ ```ts
1213
+ import { Effect } from 'effect'
1214
+
1215
+ const result = yield* analytics.aggregate({
1216
+ event: 'match_completed',
1217
+ metric: 'count',
1218
+ range: { from: daysAgo(7) },
1219
+ }).pipe(
1220
+ Effect.catchTag('AnalyticsCapabilityNotSupported', () => Effect.succeed(0)),
1221
+ Effect.catchTag('AnalyticsError', (err) => Effect.gen(function* () {
1222
+ yield* Effect.logWarning('analytics aggregate failed', { provider: err.provider, cause: err.cause })
1223
+ return 0
1224
+ })),
1225
+ )
1226
+ ```
1227
+
1228
+ The two typed errors:
1229
+
1230
+ - **`AnalyticsCapabilityNotSupported`** — the sink doesn't implement the operation (e.g. PostHog returning this for `aggregate`).
1231
+ - **`AnalyticsError`** — the underlying provider raised something (HTTP failure, query syntax, connection lost).
1232
+
1233
+ ## CDC-mirror of reactive tables
1234
+
1235
+ By default a sink stores only the events you `track()` — the main DataStore's reactive tables aren't in the warehouse, so analytical queries can't JOIN events against user data. Opt in with `mirrorTables`: the framework subscribes to the store's change stream and upserts/deletes the changed rows into a corresponding warehouse table, idempotently (keyed on the primary key).
1236
+
1237
+ ```ts
1238
+ import { duckdbAnalytics } from '@voltro/plugin-duckdb'
1239
+
1240
+ analytics: duckdbAnalytics({
1241
+ path: '.voltro/analytics.duckdb',
1242
+ mirrorTables: ['users', 'teams'], // reactive tables to mirror
1243
+ // mirrorPrimaryKey: 'id', // default 'id'
1244
+ }),
1245
+ ```
1246
+
1247
+ The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data }` — `id` is the source row's primary key, `data` is the full row as JSON. Analytical queries JOIN events against the mirror:
1248
+
1249
+ ```sql
1250
+ -- DuckDB: events per user tier
1251
+ SELECT json_extract_string(m.data, '$.tier'), COUNT(*)
1252
+ FROM voltro_events e
1253
+ JOIN voltro_mirror_users m ON m.id = e.subject_id
1254
+ GROUP BY 1
1255
+ ```
1256
+
1257
+ The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes remove by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite. Per-change failures are isolated into the log channel: the OLTP write that produced the change already committed, so a warehouse hiccup never surfaces to the request. On DuckDB the mirror table is a plain `(id, data)` table; on ClickHouse it's a `ReplacingMergeTree(version)` so re-inserts collapse to the latest version on merge (deletes write a `is_deleted = 1` tombstone — filter `is_deleted = 0` or use `FINAL`).
1258
+
1259
+ ## Default — no sink configured
1260
+
1261
+ If `app.config.ts` doesn't set `analytics`, the framework provides a no-op sink. `track()` calls drop silently (logged once at boot so operators notice); `aggregate` / `timeseries` / `topN` fail with `AnalyticsCapabilityNotSupported({ provider: 'noop' })`. Apps without analytics setup never crash on `useAnalytics().track(...)`.
1262
+
1263
+ ## Anti-patterns
1264
+
1265
+ - **Don't bypass `useAnalytics()` and call provider clients directly** unless you specifically need a provider-locked feature. Going through the contract keeps handler code provider-portable.
1266
+ - **Don't compose two reading-capable sinks expecting both to be queried.** Reads query to the first capable sink — that's by design (different sinks may hold different views of the data). If you genuinely need cross-sink reads, query each provider directly with its own client.
1267
+ - **Don't dump millions of properties per event.** Sink storage is JSON; cardinality at the property level slows JSON-extract queries. If you have unbounded dimensions, restructure into a separate events table that the analytical query reads through joins.
1268
+ - **Don't expect aggregate queries to reflect new tracks instantly on every sink.** By default ClickHouse and Tinybird ingest each `track()` as an immediate HTTP request, but the providers themselves make rows visible asynchronously (Tinybird's Events API acknowledges before the row is queryable). Two sinks add an opt-in client-side `batch` option (`clickhouseAnalytics({ batch })`, `posthogAnalytics({ batch })`) that buffers events and flushes them in one request by size / on a timer / on shutdown — a successful `track()` then means "buffered", not "delivered". Postgres-lite reflects writes immediately because they're synchronous to the main DB.
1269
+
1270
+ ## Related
1271
+
1272
+ - The cross-cutting [aggregate convention](/docs/data/aggregates) covers `*.aggregate.ts` files — pre-defined queries that materialise their result on a schedule. An aggregate's build function gets `ctx.analytics` (the configured sink) alongside `ctx.store`, so it can query the warehouse (`ctx.analytics.topN(...)`) for the "reduce 10B-row warehouse to a 100-row leaderboard" pattern. See the [cross-plan section](/docs/data/aggregates#cross-plan-querying-the-warehouse-from-a-build-function).
1273
+ - [`*.subscribe.ts`](/docs/data/subscribers) covers best-effort per-row reactive callbacks that fire after every commit. Different shape from `track()` (subscribe fires from CDC; track is explicit ingest).