@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,1235 @@
1
+ # database.seedsDialects
2
+
3
+ > Six SQL backends, one schema DSL. Decision matrix, configuration, boot-log shape, and the cross-dialect feature parity table the framework hides for you.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/database/seeds.md -->
10
+ ## Seeds
11
+
12
+ _defineSeed — idempotent data fixtures for reference data and demo data, with fingerprint-based re-run detection and lifecycle hooks._
13
+
14
+ A **seed** answers two questions: *where does my reference data live* (countries, plan tiers, feature flags) and *where does my demo data live* (a populated dashboard on first boot). A `*.seed.ts` file default-exports `defineSeed({...})`; the framework discovers it like any other primitive and runs it **idempotently**.
15
+
16
+ ```ts
17
+ // apps/api/seeds/plans.seed.ts
18
+ import { defineSeed } from '@voltro/database'
19
+
20
+ export default defineSeed({
21
+ id: 'plans',
22
+ name: 'Subscription plan tiers',
23
+ lifecycle: 'boot',
24
+ steps: ({ step }) => [
25
+ step('upsert plans', async ({ upsertByUnique }) => {
26
+ for (const p of [
27
+ { id: 'free', name: 'Free', priceCents: 0 },
28
+ { id: 'pro', name: 'Pro', priceCents: 2900 },
29
+ ]) {
30
+ await upsertByUnique('plans', { id: p.id }, p)
31
+ }
32
+ }),
33
+ ],
34
+ })
35
+ ```
36
+
37
+ ## Idempotency is the whole point
38
+
39
+ Seeds are meant to be safe to re-run. The `upsertByUnique(table, matchFields, fullRow)` helper does "row matching `matchFields` exists? update it : insert it", returning `{ row, created }`. Use stable string ids (not random tokens) so subsequent runs match the same rows instead of duplicating.
40
+
41
+ The runner also fingerprints each seed by hashing its source. On a `boot`-lifecycle seed it only re-runs when the fingerprint changes — so an unchanged seed doesn't re-execute on every `voltro dev` restart. Override the fingerprint when the seed depends on external state (env vars) that should force a re-run:
42
+
43
+ ```ts
44
+ fingerprint: ({ src }) => `${src}:${process.env.SEED_VERSION ?? ''}`,
45
+ ```
46
+
47
+ ## Lifecycles
48
+
49
+ | `lifecycle` | Runs… | Requires |
50
+ |---|---|---|
51
+ | `boot` | On every `voltro dev` boot, **only if the fingerprint changed** | — |
52
+ | `manual` | Only via `voltro db seed --id <name>` or the dashboard | — |
53
+ | `onTenantCreate` | After a successful `tenant()` insert | — |
54
+ | `onSchemaChange` | After a [migration](/docs/database/migrations) touches a watched table | `watchedTables` |
55
+ | `cron` | On a cron cadence (via the [scheduling](/docs/scheduling/overview) runtime) | `cron` |
56
+
57
+ ```ts
58
+ defineSeed({
59
+ id: 'refresh-search-index',
60
+ name: 'Rebuild search index nightly',
61
+ lifecycle: 'cron',
62
+ cron: '0 3 * * *', // required for lifecycle: 'cron'
63
+ steps: ({ step }) => [ /* … */ ],
64
+ })
65
+ ```
66
+
67
+ `defineSeed` validates at definition time: `cron` lifecycle without a `cron` field throws, `onSchemaChange` without `watchedTables` throws, and a seed with zero steps throws.
68
+
69
+ ## Steps
70
+
71
+ `steps` is a factory `({ step }) => SeedStep[]`. Each step is a named async function; the runner executes them as a workflow, so a long seed is observable and resumable. The step context gives you:
72
+
73
+ - `store` — the typed `DataStore` (`query` / `insert` / `update` / `delete`).
74
+ - `upsertByUnique(table, matchFields, fullRow)` — the idempotent upsert above.
75
+ - `progress(done, total)` — emit progress for a long-running step (wired through the workflow's progress stream).
76
+
77
+ ```ts
78
+ steps: ({ step }) => [
79
+ step('import countries', async ({ upsertByUnique, progress }) => {
80
+ const rows = COUNTRIES
81
+ let i = 0
82
+ for (const c of rows) {
83
+ await upsertByUnique('countries', { iso: c.iso }, c)
84
+ progress(++i, rows.length)
85
+ }
86
+ }),
87
+ ]
88
+ ```
89
+
90
+ ## Running seeds
91
+
92
+ ```bash
93
+ voltro db seed # run all boot-lifecycle seeds (forced, ignores fingerprint)
94
+ voltro db seed --id plans # run one seed by id
95
+ voltro db seed --lifecycle manual # run all seeds of a lifecycle
96
+ voltro db seed --store memory # explicit opt-in: in-memory smoke run
97
+ ```
98
+
99
+ > `voltro db seed` runs against the **same store the app runs on** — resolution is `DB_DIALECT` env → `STORE` env → `app.config.ts`'s `store` → `postgres`. Rows persist to the real database. Seeding to memory by default was a silent data-loss footgun (rows written, process exits, nothing persists), so it's no longer the default — pass `--store memory` for the rare smoke-test case where you genuinely want an in-memory run.
100
+
101
+ ## Reference data vs demo data
102
+
103
+ - **Reference data** (must exist in every environment): `lifecycle: 'boot'`, idempotent via `upsertByUnique`. Safe in production.
104
+ - **Demo data** (populate a fresh dashboard): `lifecycle: 'boot'` in dev, or `lifecycle: 'manual'` so it's operator-triggered and never auto-runs in prod.
105
+
106
+ See [migrations](/docs/database/migrations) for schema changes — seeds populate data, migrations shape the tables.
107
+
108
+
109
+
110
+ ---
111
+
112
+ <!-- source: en/database/dialects/index.md -->
113
+ ## SQL dialects
114
+
115
+ _Six SQL backends, one schema DSL. Decision matrix, configuration, boot-log shape, and the cross-dialect feature parity table the framework hides for you._
116
+
117
+ Voltro runs on six SQL backends. The application code — schema, queries, workflows, subscriptions, the reactive engine — is written ONCE and compiles down to the dialect-native idiom at runtime. Selecting a dialect is a single environment variable.
118
+
119
+ ```
120
+ postgres · mysql 8+ · mariadb 10.6+ · mssql 2019+ · sqlite 3.38+ · turso (beta)
121
+ ```
122
+
123
+ This index covers the cross-cutting bits: decision matrix, configuration, boot log, and the parity table. Each dialect has its own page below for the runtime-specific surface (driver quirks, framework workarounds, performance notes).
124
+
125
+ > **Hosting providers** — running on a managed database (Supabase, Neon, Vercel Postgres, Railway, Render, Fly.io, AWS RDS, DigitalOcean, Timescale, CockroachDB, PlanetScale, Azure SQL)? See [Database hosting](../providers) for per-provider connection strings, pooling, and — above all — how to enable CDC on each one.
126
+
127
+ > **Multi-replica reactivity** — behind a load balancer, how does a write on one replica reach clients on another? Postgres / MariaDB fan out natively; for every other dialect, [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS) closes the gap. See [Multi-replica reactivity](../multi-replica).
128
+
129
+ - [Postgres](./postgres) — the reference. LISTEN/NOTIFY CDC, advisory locks, streaming replication.
130
+ - [MySQL 8+](./mysql) — INSERT-then-SELECT instead of RETURNING, `LAST_INSERT_ID()` recovery for AUTO_INCREMENT ids, binlog CDC (cross-instance reactivity, at parity with mariadb), mysql2 driver quirks.
131
+ - [MariaDB 10.6+](./mariadb) — same wire driver as MySQL but UPDATE-RETURNING gap + ROW_NUMBER eager-load + binlog CDC (cross-instance reactivity, one shared reader with mysql).
132
+ - [MSSQL 2019+](./mssql) — TOP / OFFSET-FETCH instead of LIMIT, single-statement OUTPUT INSERTED/DELETED, native MERGE upsert, Change Tracking CDC, four upstream cluster patches.
133
+ - [SQLite 3.38+](./sqlite) — single-process, in-memory workflow runner, in-process CDC bus.
134
+ - [Turso (beta)](./turso) — the Rust rewrite of SQLite with MVCC concurrent writes (`BEGIN CONCURRENT` over a connection pool). SQLite-compatible; single-node; no generated columns / FTS; no Alpine/musl prebuilts.
135
+
136
+ ## Decision matrix
137
+
138
+ Pick a dialect by intersection of need + constraint.
139
+
140
+ | Need | Pick | Why |
141
+ |-----------------------------------------|----------|-----|
142
+ | Sub-second reactive UIs at scale | postgres / mariadb / mssql | Native `LISTEN/NOTIFY` (postgres), ROW-format binlog CDC (mariadb), or Change Tracking (mssql, polled) deliver cross-instance change events with zero extra infra. mysql / sqlite have no native fan-out — add [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS) for cross-instance reactivity, or they degrade to single-instance. |
143
+ | Multi-instance horizontal scaling | postgres / mysql 8+ / mariadb 10.6+ / mssql | Cluster workflow runners need a coordinatable lock primitive (advisory locks, GET_LOCK, sp_getapplock). SQLite is single-process by design. |
144
+ | MySQL-shop procurement | mysql 8.0.18+ | Mysql 8.0.0–8.0.17 rejects parameterized `LIMIT ?` — framework auto-inlines integers but other tooling may break. |
145
+ | MariaDB-shop procurement | mariadb 10.6+ | Same wire driver as mysql. The framework's dialect dispatcher branches on a few quirks (`UPDATE … RETURNING`, JSON-agg LATERAL gap). |
146
+ | Enterprise SQL Server | mssql 2019+ | 2016/2017 work but lose query-optimizer shortcuts for `OFFSET … FETCH NEXT`. |
147
+ | Single-binary distribution (dev tool, embedded device, CLI) | sqlite | No cluster scale-out: workflows run with `runnerStorage: 'memory'`. Durable replay within ONE process. |
148
+ | Single node that needs CONCURRENT writes (beta) | turso | The Rust SQLite rewrite: a connection pool + MVCC `BEGIN CONCURRENT` runs concurrent writers instead of sqlite's single-writer lock. Beta; no generated columns / FTS; glibc-only binaries. |
149
+ | Zero infrastructure for smoke tests | `store: 'memory'` | In-process map. Resets on restart. |
150
+
151
+ ## Configuration
152
+
153
+ Two sources, in priority order: env > `app.config.ts`.
154
+
155
+ ```sh
156
+ DB_DIALECT=postgres # default. Maps to @effect/sql-pg.
157
+ DB_DIALECT=mysql # @effect/sql-mysql2, variant='mysql'.
158
+ DB_DIALECT=mariadb # @effect/sql-mysql2, variant='mariadb'.
159
+ DB_DIALECT=mssql # @effect/sql-mssql.
160
+ DB_DIALECT=sqlite # @effect/sql-sqlite-node. DB_URL=file:./db.sqlite or :memory:
161
+ DB_DIALECT=turso # @voltro/sql-turso (Rust rewrite, MVCC). DB_URL=file:./db.turso or :memory:; DB_MAX_CONNECTIONS = pool size. BETA.
162
+ DB_DIALECT=memory # in-process DataStore, no SQL.
163
+ ```
164
+
165
+ Connection details come from the **same `DB_*` vars for every dialect** — there are no per-dialect `MYSQL_*` / `MARIADB_*` / `MSSQL_*` vars:
166
+
167
+ ```sh
168
+ # One URL…
169
+ DB_URL=mysql://app:app@db.internal:3306/app
170
+
171
+ # …or discrete fields (any dialect)
172
+ DB_HOST=… DB_PORT=… DB_USER=… DB_PASSWORD=… DB_DATABASE=…
173
+
174
+ # Pool size (any dialect)
175
+ DB_MAX_CONNECTIONS=10
176
+
177
+ # SQLite / Turso take a file URL
178
+ DB_URL=file:./db.sqlite # or `:memory:` for ephemeral
179
+ ```
180
+
181
+ `PG_HOST` / `PG_PORT` / `PG_USER` / `PG_PASSWORD` / `PG_DATABASE` / `PG_MAX_CONNECTIONS` are accepted as postgres-flavoured aliases for the same fields; `DB_SCHEMA` (postgres `search_path`) and `PG_SSL` are postgres-only knobs.
182
+
183
+ The `app.config.ts` `store:` field stays as the dev-friendly shortcut (`store: 'postgres'`, `store: 'memory'`) — env always wins.
184
+
185
+ ## Local development — bring up all five
186
+
187
+ The framework ships a docker-compose at `voltro/test/docker-compose.yml` that brings up postgres + mysql + mariadb + mssql on distinct ports so per-dialect tests can run side-by-side and the dev fixture never clashes with your starter postgres on `:5432`:
188
+
189
+ ```sh
190
+ cd voltro
191
+ docker compose -f test/docker-compose.yml up -d --wait
192
+ ```
193
+
194
+ | Service | Host port |
195
+ |---------------|-----------|
196
+ | postgres-test | `:55432` |
197
+ | mysql-test | `:33060` |
198
+ | mariadb-test | `:33061` |
199
+ | mssql-test | `:11433` (database `voltro_test`) |
200
+
201
+ SQLite needs no container — point `DB_URL=:memory:` for an ephemeral in-process DB.
202
+
203
+ ## Boot log
204
+
205
+ `voltro dev` and `voltro start` print a one-line summary of the resolved dialect + its enabled capabilities so `voltro logs --tail 50` answers "what's running" without grepping source:
206
+
207
+ ```
208
+ [voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
209
+ [voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
210
+ [voltro:dev] workflow engine: cluster-sql, dialect=postgres
211
+ ```
212
+
213
+ For sqlite:
214
+
215
+ ```
216
+ [voltro:dev] sql dialect resolved: sqlite — CDC: in-process bus, RETURNING: native
217
+ [voltro:dev] workflow engine: in-process durable (storage=memory) — no horizontal scale-out
218
+ ```
219
+
220
+ ## Feature parity matrix
221
+
222
+ What the framework hides for you vs what's worth knowing. Per-dialect pages drill into each row.
223
+
224
+ | Capability | postgres | mysql 8+ | mariadb 10.6+ | mssql 2019+ | sqlite 3.38+ | turso (beta) |
225
+ |-------------------------------------------|-----------------|-----------------|-----------------|------------------|---------------------|--------------------|
226
+ | CDC (change feed) | LISTEN/NOTIFY | binlog CDC (ROW) | binlog CDC (ROW) | Change Tracking (polled) | in-process bus | in-process bus |
227
+ | Concurrent writers (one node) | yes (MVCC) | yes (InnoDB) | yes (InnoDB) | yes | **no** — single-writer lock | **yes** — MVCC `BEGIN CONCURRENT` |
228
+ | Workflow cluster runners | advisory_lock | GET_LOCK | GET_LOCK | sp_getapplock | **n/a** — single process | **n/a** — single process |
229
+ | Read-replica routing | streaming repl | GTID repl | GTID repl | Always-On AG | **n/a** — single writer | **n/a** — single node |
230
+ | `RETURNING *` on INSERT | yes | no (INSERT then SELECT) | yes (10.5+) | OUTPUT INSERTED.*| yes | yes |
231
+ | `RETURNING *` on UPDATE | yes | no | **NO** (any version) | OUTPUT INSERTED.*| yes | yes |
232
+ | `RETURNING *` on DELETE | yes | no | yes (10.0+) | OUTPUT DELETED.* | yes | yes |
233
+ | Parameterized `LIMIT ?` | yes | no — integer-literal inlined | yes | no — integer-literal inlined | yes | yes |
234
+ | `LIMIT N OFFSET N` syntax | yes | yes | yes | no — `OFFSET … ROWS FETCH NEXT … ROWS ONLY` | yes | yes |
235
+ | DEFAULT on TEXT columns | yes | **NO** — auto-uses VARCHAR(255) | yes | yes (NVARCHAR(MAX)) | yes | yes |
236
+ | Native JSON column type | JSONB | JSON | JSON | NVARCHAR(MAX) | TEXT | TEXT |
237
+ | JSON columns returned as objects | yes | yes | yes | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses |
238
+ | Booleans | proper booleans | 0/1 (TINYINT) | 0/1 | BIT (proper bool) | 0/1 (INTEGER) | 0/1 (INTEGER) |
239
+ | Auto-incrementing numeric id | BIGSERIAL | AUTO_INCREMENT | AUTO_INCREMENT | IDENTITY | INTEGER PK AUTOINCREMENT | INTEGER PK — no AUTOINCREMENT |
240
+ | Identifier quoting | `"name"` | `` `name` `` | `` `name` `` | `[name]` | `"name"` | `"name"` |
241
+
242
+ ## Dialect-aware code
243
+
244
+ You almost never need to write dialect-branching SQL — the schema DSL is portable. When you reach for `sql.unsafe()` or hand-written queries, branch via `sql.onDialectOrElse`:
245
+
246
+ ```typescript
247
+ import { SqlClient } from '@effect/sql'
248
+
249
+ const fetchTopN = Effect.gen(function* () {
250
+ const sql = yield* SqlClient.SqlClient
251
+ return yield* sql.onDialectOrElse({
252
+ pg: () => sql<{ id: string }>`SELECT id FROM users LIMIT 10`,
253
+ mysql: () => sql<{ id: string }>`SELECT id FROM users LIMIT 10`, // mariadb rides this branch
254
+ mssql: () => sql<{ id: string }>`SELECT TOP 10 id FROM users`,
255
+ orElse: () => sql<{ id: string }>`SELECT id FROM users LIMIT 10`,
256
+ })
257
+ })
258
+ ```
259
+
260
+ The `orElse` branch catches sqlite + future dialects.
261
+
262
+ ## Migration between dialects
263
+
264
+ Switching `DB_DIALECT` is a deploy-time decision, not a runtime one. Migrating data from postgres to mysql (or vice versa) is a separate problem the framework doesn't solve — use `pg_dump` / `mysqldump` + a transform script. The schema you wrote against the framework's DSL re-applies cleanly on the new dialect (`voltro migrate` re-emits in the new idiom); production data does not.
265
+
266
+ ## Where the dialect lives in the codebase
267
+
268
+ - `voltro/packages/database/src/dialect.ts` — `SqlDialect` interface
269
+ - `voltro/packages/database/src/jsonEagerCompiler.ts` — per-dialect JSON-agg emission
270
+ - `voltro/packages/database/src/sqlCompiler.ts` — dialect-aware SELECT compile (TOP / LIMIT / FETCH NEXT)
271
+ - `voltro/packages/database/src/migrate.ts` — DDL emitter, identifier quoting, column-type mapping
272
+ - `voltro/packages/database/src/rowDecoder.ts` — read-path coercion for booleans / JSON / dates
273
+ - `voltro/packages/sql-postgres` / `sql-mysql` / `sql-mssql` / `sql-sqlite` — per-dialect store implementations + retry filters + CDC sources
274
+ - `voltro/packages/cli/src/dev.ts` `buildStore()` — dialect resolution from env
275
+
276
+ Each per-dialect package exports a `SqlDialect` value (`postgresDialect`, `mysqlDialect`, `mariadbDialect`, `mssqlDialect`, `sqliteDialect`). The CLI dynamically imports just the one the deployment needs — postgres-only deploys don't pull mysql / mssql into the bundle.
277
+
278
+
279
+
280
+ ---
281
+
282
+ <!-- source: en/database/dialects/postgres.md -->
283
+ ## Postgres
284
+
285
+ _The reference dialect. LISTEN/NOTIFY for low-latency CDC, advisory locks for cluster workflow runners, streaming replication for read replicas, JSONB for native JSON columns._
286
+
287
+ Postgres is the framework's reference dialect — every reactive feature was prototyped against it and the others were brought to parity. If you have no procurement constraint, this is the dialect that gives you the lowest latency + highest feature density without workarounds.
288
+
289
+ ## Why it's the default
290
+
291
+ - **LISTEN/NOTIFY** delivers change events with very low latency end-to-end. mariadb gets cross-instance CDC too (tailing the ROW-format binlog); mysql/mssql are inline-only (the writing instance emits its own deltas — single-instance reactivity); sqlite uses an in-process bus.
292
+ - **Advisory locks** (`pg_advisory_lock(key)`) give `@effect/cluster`'s `SqlRunnerStorage` a coordinatable primitive for shard ownership. Workflow runners migrate cleanly on instance loss; no manual recovery needed.
293
+ - **Streaming replication** is the basis of the framework's read-replica adapter — LSN positions track replica freshness for the per-subject RYW (read-your-writes) policy.
294
+ - **JSONB** stores `json()` columns natively; the driver parses on read, the framework never sees serialized strings.
295
+ - **`BIGSERIAL` / `GENERATED ALWAYS AS IDENTITY`** for numeric auto-increment without surprises.
296
+ - **`TIMESTAMPTZ`** keeps timezone offset on the wire; the framework's Date columns round-trip without ambiguity.
297
+
298
+ ## Configuration
299
+
300
+ ```sh
301
+ DB_DIALECT=postgres
302
+ PG_HOST=… PG_PORT=… PG_USER=… PG_PASSWORD=… PG_DATABASE=…
303
+
304
+ # Recommended for any deployment that uses CDC, replicas, or BOTH:
305
+ # `wal_level=logical` is required for logical-replication slots.
306
+ # `max_replication_slots ≥ 4` so the framework can open one per
307
+ # replica + leave headroom for ad-hoc pg_recvlogical sessions.
308
+ ```
309
+
310
+ `wal_level=logical` is required only for logical-replication consumers (the CDC replication adapter opens a slot). The LISTEN/NOTIFY reactivity path doesn't need WAL at all — it works on the default `wal_level`. If you enable replication and the slot can't open, the driver error surfaces the misconfiguration.
311
+
312
+ ## Driver: `@effect/sql-pg`
313
+
314
+ The framework wraps `pg` (`node-postgres`) via Effect's typed connection layer. Key settings the framework defaults sensibly + you can override:
315
+
316
+ | Env var | Default | What it does |
317
+ |-----------------------|---------|--------------|
318
+ | `PG_HOST` | `localhost` | hostname |
319
+ | `PG_PORT` | `5432` | port |
320
+ | `PG_USER` | `app` | role |
321
+ | `PG_PASSWORD` | `app` | password (use a secret manager in prod) |
322
+ | `PG_DATABASE` | `app` | database name |
323
+ | `PG_MAX_CONNECTIONS` | `10` | pool ceiling. Tune up for high concurrency; CPU-bound workloads rarely benefit past ~2× cores. |
324
+ | `PG_SSL` | unset | `'require'` (TLS without certificate verification — what production deployments behind RDS / Cloud SQL / Supabase want) / `'disable'` (force plaintext). Any other value — including libpq's `'prefer'`, which node-postgres cannot express (the driver has no TLS-then-plaintext fallback) — fails at boot instead of silently downgrading to plaintext. A `?sslmode=require\|disable` query on `DB_URL` works too; an explicit `PG_SSL` wins when both are set. |
325
+
326
+ ## CDC — LISTEN/NOTIFY
327
+
328
+ Every `*.reactive()` table gets an AFTER trigger that emits a `framework_changes` notification on each insert / update / delete. The dispatcher LISTENs on that channel once per process and fans out to subscribers in-memory.
329
+
330
+ Latency: very low on a local-network postgres (it varies with network and load). The framework instruments this — `voltro traces` shows the notification → dispatcher → subscriber waterfall.
331
+
332
+ Trade-offs of the LISTEN/NOTIFY path:
333
+
334
+ - ✅ Sub-frame latency (60fps UIs feel real-time).
335
+ - ✅ Zero polling load even on idle tables.
336
+ - ❌ Requires a long-lived connection per process. Connection-pooled deploys must use a sidecar listener or pgbouncer in session mode.
337
+ - ❌ NOTIFY payloads are capped at 8KB. The trigger sends the full row images (`row_to_json(OLD)`/`row_to_json(NEW)`) so subscribers get the pre/post values directly; when a wide row would exceed the cap the trigger drops the body and sends an ID-only notification (the subscriber re-queries), so a very wide reactive row degrades gracefully rather than failing.
338
+
339
+ Set `CDC=0` to disable + force the inline-emit path (single-process only, no cross-process fan-out). Useful for tests + single-binary deploys.
340
+
341
+ ## Workflow cluster
342
+
343
+ `@effect/cluster`'s `SqlRunnerStorage` uses `pg_advisory_lock(key)` to claim shard ownership. The framework wires this transparently — set `DB_DIALECT=postgres` + provide a SqlClient layer and `workflowEngineLayer({ runnerStorage: 'sql' })` does the rest.
344
+
345
+ Shard re-assignment on runner death: postgres releases advisory locks on session close, so a crashed runner's shards become acquirable by survivors automatically. Typical takeover time: 5–15s depending on heartbeat interval.
346
+
347
+ ## Read replicas
348
+
349
+ The framework's `ReplicatedDataStore` uses `pg_last_wal_replay_lsn()` to measure replica freshness against the primary's `pg_current_wal_lsn()`. Per-subject RYW (read-your-writes) waits until the replica catches up to the primary's LSN at write time, or falls back to the primary if `RYW_POLICY=fallback`.
350
+
351
+ Enable with:
352
+
353
+ ```sh
354
+ DB_REPLICA_URLS=postgresql://app:app@replica-1:5432/app,postgresql://app:app@replica-2:5432/app
355
+ RYW_POLICY=fallback # default. 'wait' is the alternative.
356
+ RYW_TTL_MS=30000 # how long a write keeps its subject on the primary.
357
+ ```
358
+
359
+ Without `DB_REPLICA_URLS` the wrapper isn't instantiated — zero overhead, every query goes to primary.
360
+
361
+ ## JSON columns
362
+
363
+ `json()` columns emit `JSONB` in DDL (binary stored format, indexable via GIN, comparison/membership operators native). The driver auto-parses on read; the framework never sees strings.
364
+
365
+ If you need raw JSON (text) storage for some reason — preserving formatting, embedding non-canonical UTF-8 — drop down to `unsafe()` and emit `JSON` explicitly. Rare.
366
+
367
+ ## Identifier quoting
368
+
369
+ `"users"` — double quotes. The framework emits these for every identifier when needed; user-written SQL that hand-quotes column names needs to use double quotes for portability.
370
+
371
+ ## Migration emitter
372
+
373
+ `voltro migrate` against postgres uses the standard DDL idiom:
374
+
375
+ - `CREATE TABLE … IF NOT EXISTS`
376
+ - `ALTER TABLE … ADD COLUMN … IF NOT EXISTS`
377
+ - `CREATE INDEX … IF NOT EXISTS`
378
+ - FK constraints with explicit `ON DELETE` / `ON UPDATE` clauses
379
+ - `BIGSERIAL PRIMARY KEY` for numeric id columns
380
+
381
+ The `_voltro_migrations` ledger is a regular table with a unique constraint on `(id, hash)` for idempotency — re-running the same migration is a no-op.
382
+
383
+ ## Known caveats
384
+
385
+ - **Long-running transactions hold ROW EXCLUSIVE locks**. The framework's `transactional()` wrapper auto-retries on serialization failures (`40001`) and deadlocks (`40P01`); if you build custom long-running flows, expect contention.
386
+ - **`pg_listen_notify` has a per-connection capacity**. The framework uses ONE dedicated LISTEN connection per process — never multiplexes through the pool.
387
+ - **`pg_advisory_lock` keys are 8-byte ints**. The framework hashes shard-id strings to int64; collisions are astronomically improbable but theoretically possible.
388
+
389
+ ## Where it lives
390
+
391
+ - `voltro/packages/sql-postgres/src/store.ts` — `PostgresDataStore` implementation + LISTEN/NOTIFY consumer
392
+ - `voltro/packages/sql-postgres/src/retry.ts` — `isRetryablePgFailure` (40001 / 40P01)
393
+ - `voltro/packages/sql-postgres/src/replicationAdapter.ts` — LSN compare for RYW
394
+ - `voltro/packages/database/src/migrate.ts` — postgres DDL emission (the orElse branch)
395
+
396
+
397
+
398
+ ---
399
+
400
+ <!-- source: en/database/dialects/mysql.md -->
401
+ ## MySQL 8+
402
+
403
+ _Wide procurement footprint, binlog CDC for cross-instance reactivity (at parity with mariadb), no RETURNING (INSERT-then-SELECT fallback), AUTO_INCREMENT id recovery via LAST_INSERT_ID, parameterized LIMIT rejected (auto-inlined integer literals), DEFAULT on TEXT forbidden in strict mode._
404
+
405
+ MySQL 8+ is the second-most-shipped dialect because of procurement: enterprises that standardized on MySQL want to stay there. The framework provides full feature parity with postgres — including **binlog CDC for cross-instance reactivity** (mysql and mariadb share one binlog reader) — plus a handful of driver quirks worked around transparently.
406
+
407
+ ## Target version
408
+
409
+ **MySQL 8.0.18 or later.** Two reasons:
410
+
411
+ 1. `LIMIT ?` as a prepared-statement parameter was rejected with `ER_WRONG_ARGUMENTS` in 8.0.0–8.0.17. The framework inlines integer literals (validated as non-negative) via `sql.unsafe(String(n))` to work around this, but other tooling you connect (BI tools, ORMs) may break.
412
+ 2. JSON_OBJECT + JSON_ARRAYAGG semantics stabilized in 8.0.14. The framework's JSON-agg eager-load compiler depends on these.
413
+
414
+ MySQL 5.7 is **not** supported — the JSON eager loads would fail.
415
+
416
+ ## Configuration
417
+
418
+ ```sh
419
+ DB_DIALECT=mysql
420
+ DB_URL=mysql://app:app@db.internal:3306/app
421
+ # …or discrete fields — the same DB_* vars every dialect uses
422
+ # (there are no MYSQL_* vars):
423
+ DB_HOST=… DB_PORT=… DB_USER=… DB_PASSWORD=… DB_DATABASE=…
424
+
425
+ # Strict mode is REQUIRED. Most modern MySQL deploys have it on by
426
+ # default; verify with `SELECT @@sql_mode;` — should contain
427
+ # `STRICT_TRANS_TABLES,STRICT_ALL_TABLES`. The framework's DDL
428
+ # emitter relies on strict-mode validation to surface schema bugs
429
+ # at migration time.
430
+ ```
431
+
432
+ ## Driver: `@effect/sql-mysql2`
433
+
434
+ Wraps the `mysql2` driver (Node.js mysql client). The framework's `MysqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
435
+
436
+ The driver returns BOOLEAN as `0|1` (TINYINT(1) is MySQL's underlying type). The framework's `decodeRowsFromSchema` post-processor converts back to `true|false` for any column the schema registry declares as `boolean()`. Without this, every reactive subscription opening on a BOOLEAN column would see `1` instead of `true` and fail framework Schema validators.
437
+
438
+ ## No `RETURNING` — INSERT then SELECT
439
+
440
+ MySQL 8.x does not support `RETURNING *` on any DML statement. The framework's mysql store handles this with deterministic IDs + a follow-up SELECT:
441
+
442
+ ```typescript
443
+ // What you write:
444
+ await ctx.store.insert('todos', { title: 'hello', done: false })
445
+
446
+ // What the framework does on mysql (internally — `generateId` takes the
447
+ // table's RESOLVED scheme, not a bare table name):
448
+ const id = generateId(resolveIdScheme(undefined, 'todos'), 'todos') // TypeID auto-injection
449
+ await sql`INSERT INTO todos (id, title, done) VALUES (${id}, ${title}, ${done})`
450
+ const [row] = await sql`SELECT * FROM todos WHERE id = ${id} LIMIT 1`
451
+ return row
452
+ ```
453
+
454
+ This works because the framework's mutation middleware generates IDs client-side BEFORE the insert via the schema registry's TypeID / ULID / Snowflake generator. The follow-up SELECT is `O(1)` against the PK. Performance impact: one extra round-trip per write — meaningful for very high-throughput workloads but invisible for typical app traffic.
455
+
456
+ For UPDATE: UPDATE then SELECT by the known PK. For DELETE: SELECT before delete (to capture the row for the ChangeEvent) then DELETE.
457
+
458
+ ### AUTO_INCREMENT ids — `LAST_INSERT_ID()` recovery
459
+
460
+ The write-then-SELECT above relies on knowing the PK before the insert — which the framework's client-side TypeID / ULID / Snowflake auto-injection guarantees on every framework path. The escape hatch is a **hand-rolled numeric `AUTO_INCREMENT` primary key**: a table you created yourself with `id INT PRIMARY KEY AUTO_INCREMENT`, inserted WITHOUT a client-side `id`. The store recovers the DB-generated id via `LAST_INSERT_ID()`:
461
+
462
+ ```typescript
463
+ // A table with a numeric AUTO_INCREMENT PK (no framework id() column):
464
+ // CREATE TABLE counters (id INT PRIMARY KEY AUTO_INCREMENT, label VARCHAR(64))
465
+
466
+ const row = await ctx.store.insert('counters', { label: 'hits' })
467
+ row.id // ← the DB-generated id, recovered and returned
468
+
469
+ const rows = await ctx.store.insertMany('counters', [
470
+ { label: 'a' }, { label: 'b' }, { label: 'c' },
471
+ ])
472
+ rows.map((r) => r.id) // ← the full generated range, in input order
473
+ ```
474
+
475
+ `LAST_INSERT_ID()` is connection-scoped, so the INSERT, the id read, and the post-image SELECT are pinned to one connection (a short transaction when you're not already inside one). For `insertMany`, `LAST_INSERT_ID()` returns the FIRST generated id and the engine allocates the rest consecutively, so the store re-selects the `[first, first+n-1]` range. This works identically on mysql and mariadb. (If the PK is not actually `AUTO_INCREMENT`, the insert throws a clear error rather than returning a bogus id.)
476
+
477
+ ## CDC — binlog (ROW format)
478
+
479
+ MySQL has no `LISTEN/NOTIFY`, but MySQL 8's ROW-format binary log is a real out-of-band CDC source — the SAME binlog the framework already tails for mariadb. The framework tails the primary's binlog via the `@vlasky/zongji` replication client, so **every replica tails the binlog itself and a write on any instance surfaces on every instance's `onChange`** (true cross-instance reactivity in a multi-replica deploy). This is at parity with mariadb: binlog CDC is **one path** with per-variant detection, not two implementations.
480
+
481
+ ### How it works
482
+
483
+ 1. With `CDC=1` (the default for any SQL dialect) on `mysql`, the store starts a binlog reader on a **separate replication connection** — distinct from the SQL pool.
484
+ 2. The reader subscribes to `WriteRows` / `UpdateRows` / `DeleteRows` events and turns each into a `ChangeEvent { table, op, old, new }`. UPDATE events carry both the BEFORE and AFTER row images (needs `binlog_row_image=FULL`), so `old` is richer than inline mode can produce.
485
+ 3. In CDC mode the binlog reader is the **sole** emitter — the write path stays silent, so each write surfaces exactly once per instance, delivered by that instance's own reader.
486
+ 4. Framework-internal `_voltro_`-prefixed tables (and any table not in the reactive set) are skipped by the reader.
487
+
488
+ ### Requirements
489
+
490
+ The reader fails fast at boot if these aren't met:
491
+
492
+ - **`binlog_format=ROW`** — statement/mixed formats don't carry per-row images.
493
+ - **`binlog_row_image=FULL`** — needed for complete UPDATE/DELETE before-images (a non-FULL image logs a warning; before-images may be partial).
494
+ - **`log_bin=ON`.** MySQL 8.0+ enables this **by default** (unlike mariadb, which needs an explicit `--log-bin`), so most MySQL 8 deploys already have a binlog.
495
+ - A DB user with **`REPLICATION SLAVE, REPLICATION CLIENT`**.
496
+ - **A UNIQUE `server_id` per reader.** Duplicate `server_id`s silently break binlog streams. The framework derives one per pod from `POD_NAME` / `HOSTNAME` (falling back to the PID in dev).
497
+ - The **`@vlasky/zongji`** package. It ships as an `optionalDependency` of `@voltro/sql-mysql`; if it's absent, a `cdc` request throws a clear "install `@vlasky/zongji`" error rather than silently degrading.
498
+
499
+ ### mysql-vs-mariadb difference: binlog-end query
500
+
501
+ The one engine divergence the store handles for you: **MySQL 8.4 removed `SHOW MASTER STATUS`** in favour of `SHOW BINARY LOG STATUS`. The framework resolves the current binlog end with the right statement per variant (`SHOW BINARY LOG STATUS` on mysql, `SHOW MASTER STATUS` on mariadb), falling back to the other spelling for mysql < 8.4. You don't wire anything for this — it's picked from the `variant` flag.
502
+
503
+ ### Resume offsets
504
+
505
+ Each replica persists its progress in the **`_voltro_cdc_offsets`** table — one row per replica (`PK = replicaId`), holding the last binlog **`(file, position)`** it fully processed. On boot the reader resumes from that point; events between crash and resume replay and self-heal via the dispatcher's per-subscribe re-query. (Resume is by binlog file + position, not by GTID.)
506
+
507
+ If the persisted offset has been purged (`err 1236`) or rejected after a failover, the reader jumps to the current binlog end and signals a resync so dependent subscriptions re-query rather than missing the gap.
508
+
509
+ ### Boot summary
510
+
511
+ ```
512
+ [voltro:dev] sql dialect resolved: mysql — CDC: binlog CDC (ROW), RETURNING: INSERT/UPDATE/DELETE then SELECT (no RETURNING)
513
+ [voltro:dev] mysql binlog CDC enabled { replicaId: 'pod-0', serverId: 1234567, reactiveTables: 42 }
514
+ [voltro:dev] cdc: binlog reader attached { serverId: 1234567, from: 'current-end' }
515
+ ```
516
+
517
+ Set `CDC=0` to fall back to inline-emit (single-process only, no binlog dependency) — useful for tests and single-binary deploys.
518
+
519
+ ## Parameterized LIMIT — auto-inlined
520
+
521
+ `mysql2`'s prepared-statement layer rejects `LIMIT ?` with the cryptic error `ER_WRONG_ARGUMENTS`. The framework's `compileSelect` inlines integer literals across every dialect to dodge this:
522
+
523
+ ```sql
524
+ -- What the framework emits:
525
+ SELECT * FROM users WHERE tenant_id = ? ORDER BY created_at DESC LIMIT 100
526
+
527
+ -- Not what would fail:
528
+ SELECT * FROM users WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?
529
+ ```
530
+
531
+ The `100` is a literal `sql.unsafe(String(100))`. The framework validates non-negative integer before emitting → zero injection surface.
532
+
533
+ User-written `unsafe()` SQL on mysql should follow the same pattern: inline integer literals via `sql.literal(String(n))` rather than `${n}` parameter binding.
534
+
535
+ ## DEFAULT on TEXT columns — VARCHAR(255) fallback
536
+
537
+ MySQL (strict mode) rejects DEFAULT values on TEXT/BLOB columns with `BLOB, TEXT, GEOMETRY or JSON column 'col' can't have a default value`. MariaDB allows this; MySQL does not.
538
+
539
+ The framework's DDL emitter detects the case and switches to `VARCHAR(255)`:
540
+
541
+ ```typescript
542
+ text().default('json') // → VARCHAR(255) DEFAULT 'json'
543
+ text().oneOf(['a', 'b', 'c']).default('a') // → VARCHAR(255) DEFAULT 'a' CHECK (col IN ('a','b','c'))
544
+ text().nullable() // → TEXT (unchanged — no default to trip up)
545
+ ```
546
+
547
+ VARCHAR(255) is the framework's heuristic — enough for typical enum-like values, short status strings, format identifiers. If you need longer defaulted text, declare the column as `text().nullable()` + handle the missing-default case in application code, OR drop down to `unsafe()`.
548
+
549
+ ## JSON columns
550
+
551
+ `json()` columns emit `JSON` (mysql's native binary JSON type since 5.7+). The driver auto-parses on read; same shape as postgres. No coercion overhead.
552
+
553
+ JSON_VALID is enforced by mysql — you can't insert non-JSON strings into a JSON column. Test fixtures that bypass the framework's validation will hit this.
554
+
555
+ ## Identifier quoting
556
+
557
+ `` `users` `` — backticks. Standard MySQL. The framework's compiler emits these for every identifier; user-written `unsafe()` SQL should match.
558
+
559
+ ## Workflow cluster
560
+
561
+ `@effect/cluster`'s `SqlRunnerStorage` mysql branch uses `GET_LOCK(name, timeout)` for shard claims and `ON DUPLICATE KEY UPDATE` for runner upserts. Same end-to-end behaviour as postgres; takeover on runner death takes ~5–15s.
562
+
563
+ ## Read replicas
564
+
565
+ The framework's mysql replication adapter reads GTID positions on both sides (`@@global.gtid_executed` on mysql) — but the catch-up **comparison is a stub today**: `compare()` always answers `'behind'`. Under the default `RYW_POLICY=fallback` that's fine (fallback pins on RYW-position presence and never calls `compare()`); under `RYW_POLICY=wait`, a session with a pending RYW position always falls back to the primary instead of ever seeing a caught-up replica. A real `GTID_SUBSET()` round-trip is a tracked follow-up.
566
+
567
+ ```sh
568
+ DB_REPLICA_URLS=mysql://app:app@replica-1:3306/app
569
+ RYW_POLICY=fallback
570
+ ```
571
+
572
+ Set `gtid_mode=ON` + `enforce_gtid_consistency=ON` on every node in the topology — without GTIDs, the replication adapter can't track positions and the RYW guarantee silently degrades.
573
+
574
+ ## Known caveats
575
+
576
+ - **Mysql 8.4.x raised the default `auth_plugin` to `caching_sha2_password`**. The `mysql2` driver supports it but some older Node.js builds need the `RSA-OAEP` opt-in. If you hit `ERR_OSSL_UNSUPPORTED`, create the app's database user with `mysql_native_password` on the server (there is no framework env var for the auth plugin).
577
+ - **DATETIME(6) for sub-second precision**. The framework emits this for `timestamp()` columns; lower-precision DATETIME drops microseconds and your audit logs lose ordering on fast inserts.
578
+ - **`utf8mb4_unicode_ci` collation is required** for the framework's text columns to compare correctly. `utf8` (legacy 3-byte) breaks emoji + non-BMP chars; charset checks fire at first insert.
579
+
580
+ ## Where it lives
581
+
582
+ - `voltro/packages/sql-mysql/src/store.ts` — `MysqlStore` with `variant: 'mysql'`
583
+ - `voltro/packages/sql-mysql/src/retry.ts` — `isRetryableMysqlFailure` (1213 / 1205 deadlock + lock wait timeout)
584
+ - `voltro/packages/sql-mysql/src/replicationAdapter.ts` — GTID capture/probe for RYW (catch-up `compare()` is a stub)
585
+ - `voltro/packages/database/src/migrate.ts` — mysql DDL branch (line 59)
586
+ - `voltro/packages/database/src/sqlCompiler.ts` — integer-literal LIMIT inlining
587
+
588
+
589
+
590
+ ---
591
+
592
+ <!-- source: en/database/dialects/mariadb.md -->
593
+ ## MariaDB 10.6+
594
+
595
+ _Wire-compatible with MySQL but diverges on UPDATE-RETURNING (doesn't exist), correlated derived tables (rejected), and JSON_ARRAYAGG ORDER BY (MariaDB-only extension). Framework dispatches all three at the dialect-tag level._
596
+
597
+ MariaDB shares MySQL's wire protocol and the `@effect/sql-mysql2` driver — but the SQL surface diverges enough that the framework keeps it as a separate dialect tag (`DB_DIALECT=mariadb`). This page enumerates the differences the framework handles for you, plus the ones that bite when you reach for hand-written SQL.
598
+
599
+ ## Target version
600
+
601
+ **MariaDB 10.6 or later.** Two reasons:
602
+
603
+ 1. JSON_OBJECT + JSON_ARRAYAGG were added in 10.5, but `JSON_ARRAYAGG(... ORDER BY ...)` (a MariaDB-only extension the framework's eager-load compiler uses) is stable from 10.5+. ROW_NUMBER + window functions stabilized earlier (10.2+).
604
+ 2. Native `INSERT … RETURNING` is in 10.5+, `DELETE … RETURNING` is in 10.0+. The framework relies on both.
605
+
606
+ 10.5.x works in principle; 10.6+ gives you the long-term-support windowing semantics the framework tests against.
607
+
608
+ ## Configuration
609
+
610
+ ```sh
611
+ DB_DIALECT=mariadb
612
+ DB_URL=mysql://app:app@db.internal:3306/app
613
+ # …or discrete fields — the same DB_* vars every dialect uses
614
+ # (there are no MARIADB_* vars):
615
+ DB_HOST=… DB_PORT=… DB_USER=… DB_PASSWORD=… DB_DATABASE=…
616
+
617
+ # Strict mode is recommended. MariaDB defaults to ON in modern
618
+ # versions but is less aggressive about it than mysql. Verify:
619
+ # `SELECT @@sql_mode;` should contain STRICT_TRANS_TABLES.
620
+ ```
621
+
622
+ ## Driver: `@effect/sql-mysql2` (variant='mariadb')
623
+
624
+ Same driver as MySQL — MariaDB is wire-compatible. The framework keys behaviour on the `variant` flag passed at store construction:
625
+
626
+ ```typescript
627
+ makeMysqlStore({ sqlLayer, variant: 'mariadb', changeStrategy: 'cdc', cdcConfig })
628
+ ```
629
+
630
+ The variant flows through to per-operation getters (`supportsInsertReturning`, `supportsDeleteReturning`, `supportsUpdateReturning`) and to the JSON-agg compiler's dialect branch.
631
+
632
+ ## `UPDATE … RETURNING` does NOT exist — anywhere
633
+
634
+ Despite MariaDB's broad RETURNING support — `INSERT … RETURNING *` since 10.5, `DELETE … RETURNING *` since 10.0 — there is **no UPDATE … RETURNING in any MariaDB version**. The framework's first attempt at supportsReturning treated the whole RETURNING family as one flag and emitted `UPDATE … RETURNING *` on mariadb, which fails with a parse error.
635
+
636
+ The split:
637
+
638
+ | Operation | MariaDB native? | Framework path |
639
+ |-----------|------------------|----------------|
640
+ | INSERT | yes (10.5+) | `INSERT … RETURNING *` |
641
+ | DELETE | yes (10.0+) | `DELETE … RETURNING *` |
642
+ | UPDATE | **no, never** | UPDATE then SELECT by PK (same as mysql) |
643
+
644
+ The mysql store's class internally exposes three getters (`supportsInsertReturning`, `supportsDeleteReturning`, `supportsUpdateReturning`). MariaDB gets `true` on the first two and `false` on the third. User code that calls `ctx.store.update(...)` returns the row from the follow-up SELECT — same interface, same return shape, just one extra round-trip.
645
+
646
+ ## Correlated subqueries in non-LATERAL derived tables — rejected
647
+
648
+ MariaDB refuses correlated outer references inside non-LATERAL derived tables. The classic MySQL eager-load pattern:
649
+
650
+ ```sql
651
+ -- Works in MySQL 8.0.14+ (auto-promoted to LATERAL).
652
+ -- Works in postgres.
653
+ -- FAILS in MariaDB: Unknown column 't1.id' in 'WHERE'.
654
+ SELECT t1.id,
655
+ (SELECT JSON_ARRAYAGG(JSON_OBJECT('title', x.title))
656
+ FROM (SELECT * FROM posts WHERE author_id = t1.id LIMIT 5) AS x
657
+ ) AS posts
658
+ FROM users t1;
659
+ ```
660
+
661
+ And MariaDB does NOT accept the `LATERAL` keyword as a workaround either.
662
+
663
+ ### The framework's workaround: window function pattern
664
+
665
+ For `many()` and `manyToMany()` branches with `limit` / `offset` / `orderBy`, the framework emits a `ROW_NUMBER() OVER (PARTITION BY fk ORDER BY …)` pattern. The ranking happens in a derived table with NO outer reference, and the correlation lives in the wrapping subquery's WHERE clause where MariaDB accepts it:
666
+
667
+ ```sql
668
+ -- What the framework emits on MariaDB for users.with({ posts: { limit: 5 } }):
669
+ SELECT JSON_OBJECT(
670
+ 'id', t1.id,
671
+ 'posts', COALESCE((
672
+ SELECT JSON_ARRAYAGG(JSON_OBJECT(
673
+ 'id', ranked.id, 'title', ranked.title
674
+ ) ORDER BY ranked.rn)
675
+ FROM (
676
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY created_at DESC) AS rn
677
+ FROM posts
678
+ ) ranked
679
+ WHERE ranked.author_id = t1.id AND ranked.rn <= 5
680
+ ), JSON_ARRAY())
681
+ ) AS __row
682
+ FROM users t1;
683
+ ```
684
+
685
+ For the no-pagination case (no `limit`, no `offset`), the framework emits a direct correlated subquery with `JSON_ARRAYAGG(… ORDER BY col)` — the MariaDB-only extension — for cleaner SQL.
686
+
687
+ Performance cost: `ROW_NUMBER` materializes ranking across the whole table on every invocation. For tables in the millions this hurts; MariaDB users running into the cliff should add FK indices and consider limit-less queries against the walker path. For typical app-tier sizes (10k–100k rows) the optimizer prunes by partition + the cost stays linear with per-parent row count.
688
+
689
+ ## `JSON_ARRAYAGG(… ORDER BY …)` — MariaDB-only
690
+
691
+ This extension lets you order rows inside a JSON aggregate without a derived-table wrap:
692
+
693
+ ```sql
694
+ SELECT JSON_ARRAYAGG(x.title ORDER BY x.title) -- MariaDB ✓ / MySQL ✗
695
+ FROM posts x WHERE author_id = ?;
696
+ ```
697
+
698
+ MySQL 8 rejects `ORDER BY` inside `JSON_ARRAYAGG` with a parse error. MariaDB has supported it since 10.5.
699
+
700
+ The framework uses it for the no-pagination case of `many()` / `manyToMany()` on MariaDB — cleaner SQL than the ROW_NUMBER pattern when you don't need per-parent limits.
701
+
702
+ ## CDC — binlog (ROW format)
703
+
704
+ MariaDB has no `LISTEN/NOTIFY`, but it gets a real out-of-band CDC source: the framework tails the primary's **ROW-format binary log** via the `@vlasky/zongji` replication client. This is the MariaDB equivalent of postgres's `LISTEN/NOTIFY` — **every replica tails the binlog itself, so a write on any instance surfaces on every instance's `onChange`** (true cross-instance reactivity in a multi-replica deploy).
705
+
706
+ This is **one binlog-CDC path shared with `mysql`** — mysql-8 and mariadb speak the same ROW binlog to the reader, so both get cross-instance CDC. The only engine difference is the binlog-end query (mariadb `SHOW MASTER STATUS`; mysql 8.4 `SHOW BINARY LOG STATUS`), which the store picks per variant.
707
+
708
+ ### How it works
709
+
710
+ 1. With `CDC=1` (the default for any SQL dialect) on `mariadb`, the store starts a binlog reader on a **separate replication connection** — distinct from the SQL pool.
711
+ 2. The reader subscribes to `WriteRows` / `UpdateRows` / `DeleteRows` events and turns each into a `ChangeEvent { table, op, old, new }`. UPDATE events carry both the BEFORE and AFTER row images (needs `binlog_row_image=FULL`), so `old` is richer than inline mode can produce.
712
+ 3. In CDC mode the binlog reader is the **sole** emitter — the write path stays silent, so each write surfaces exactly once per instance, delivered by that instance's own reader.
713
+ 4. Framework-internal `_voltro_`-prefixed tables (and any table not in the reactive set) are skipped by the reader.
714
+
715
+ ### Requirements
716
+
717
+ The reader fails fast at boot if these aren't met:
718
+
719
+ - **`binlog_format=ROW`** — statement/mixed formats don't carry per-row images.
720
+ - **`binlog_row_image=FULL`** — needed for complete UPDATE/DELETE before-images (a non-FULL image logs a warning; before-images may be partial).
721
+ - **`log_bin=ON`.**
722
+ - A DB user with **`REPLICATION SLAVE, REPLICATION CLIENT`**.
723
+ - **A UNIQUE `server_id` per reader.** Duplicate `server_id`s silently break binlog streams — two readers with the same id collide. The framework derives one per pod from `POD_NAME` / `HOSTNAME` (falling back to the PID in dev).
724
+ - The **`@vlasky/zongji`** package. It ships as an `optionalDependency` of `@voltro/sql-mysql`; if it's absent, a `cdc` request throws a clear "install `@vlasky/zongji`" error rather than silently degrading.
725
+
726
+ ### Resume offsets
727
+
728
+ Each replica persists its progress in the **`_voltro_cdc_offsets`** table — one row per replica (`PK = replicaId`), holding the last binlog **`(file, position)`** it fully processed. On boot the reader resumes from that point; events between crash and resume replay and self-heal via the dispatcher's per-subscribe re-query. (Resume is by binlog file + position, not by GTID — failover relies on the re-query self-heal, not GTID portability.)
729
+
730
+ If the persisted offset has been purged (`err 1236`) or rejected after a failover, the reader jumps to the current binlog end and signals a resync so dependent subscriptions re-query rather than missing the gap.
731
+
732
+ ### Boot summary
733
+
734
+ ```
735
+ [voltro:dev] sql dialect resolved: mariadb — CDC: binlog CDC (ROW), RETURNING: native (INSERT/DELETE); UPDATE then SELECT
736
+ [voltro:dev] mariadb binlog CDC enabled { replicaId: 'pod-0', serverId: 1234567, reactiveTables: 42 }
737
+ [voltro:dev] cdc: binlog reader attached { serverId: 1234567, from: 'current-end' }
738
+ ```
739
+
740
+ Set `CDC=0` to fall back to inline-emit (single-process only, no binlog dependency) — useful for tests and single-binary deploys.
741
+
742
+ ## Workflow cluster
743
+
744
+ `@effect/cluster`'s mysql branch (`GET_LOCK` + `ON DUPLICATE KEY UPDATE`) works on MariaDB. The framework dispatches it via the same `variant: 'mariadb'` flag the store carries.
745
+
746
+ ## Read replicas
747
+
748
+ MariaDB's GTID format differs from MySQL's: `0-1-100` (domain-server-sequence) vs `aaaaaaaa-...:1-100` (UUID-based). The framework's replication adapter probes the right variable per variant — `@@global.gtid_current_pos` on mariadb, `@@global.gtid_executed` on mysql. As on mysql, the catch-up **comparison is a stub today** (`compare()` always answers `'behind'`): the default `RYW_POLICY=fallback` never calls it, but `RYW_POLICY=wait` always routes RYW reads to the primary. A real GTID-subset round-trip is a tracked follow-up.
749
+
750
+ ## Identifier quoting
751
+
752
+ `` `name` `` — backticks. Same as MySQL.
753
+
754
+ ## Known caveats
755
+
756
+ - **`mariadb` schema package is wire-compatible with `mysql`**. If you migrate from MySQL → MariaDB, the framework re-emits DDL cleanly via `applySchema(..., 'mariadb')`. Production data round-trips through `mysqldump` without translation.
757
+ - **`sql_mode=NO_BACKSLASH_ESCAPES`** is sometimes set on MariaDB deploys. The framework's identifier escaping handles it, but user-written `unsafe()` strings that hand-escape backslashes may produce wrong output. Leave that mode off if you can.
758
+ - **Sequence-based ID columns**. MariaDB has true CREATE SEQUENCE; the framework doesn't use it (TypeID / ULID / Snowflake are client-side). If you reach for sequences for legacy reasons, they're outside the framework's auto-injection path.
759
+ - **Hand-rolled `AUTO_INCREMENT` primary keys** work the same as on mysql: an `insert` / `insertMany` with no client-side `id` recovers the DB-generated id via `LAST_INSERT_ID()` (connection-pinned; `insertMany` recovers the whole consecutive range). See the [mysql page](/docs/en/database/dialects/mysql#auto_increment-ids--last_insert_id-recovery) for the worked example — the recovery path is identical on both engines.
760
+
761
+ ## Where it lives
762
+
763
+ - `voltro/packages/sql-mysql/src/index.ts` — exports `mariadbDialect`
764
+ - `voltro/packages/sql-mysql/src/store.ts` — `supportsInsertReturning` / `supportsDeleteReturning` / `supportsUpdateReturning` getters branch on `variant`
765
+ - `voltro/packages/database/src/jsonEagerCompiler.ts` — `mariadbManySubquery` / `mariadbManyToManySubquery` ROW_NUMBER window-function pattern
766
+ - `voltro/packages/database/src/migrate.ts` — mariadb shares the mysql DDL branch (text-DEFAULT stays as TEXT — mariadb allows it)
767
+ - `voltro/packages/sql-mysql/src/binlogCdc.ts` — ROW-format binlog reader (`@vlasky/zongji`), per-pod `server_id`, file/position resume + self-heal on purge/failover
768
+ - `voltro/packages/sql-mysql/src/cdcOffsetsTable.ts` — `_voltro_cdc_offsets` per-replica binlog `(file, position)` checkpoint
769
+
770
+
771
+
772
+ ---
773
+
774
+ <!-- source: en/database/dialects/mssql.md -->
775
+ ## MSSQL 2019+
776
+
777
+ _TOP / OFFSET-FETCH instead of LIMIT, single-statement OUTPUT INSERTED/DELETED instead of RETURNING, IDENTITY id recovery, native MERGE upserts, NVARCHAR(MAX) for JSON columns, and Change Tracking CDC for cross-instance reactivity. Four upstream cluster patches the framework carries._
778
+
779
+ MSSQL is the most divergent dialect the framework supports. T-SQL deviates from ANSI SQL in places the schema DSL hides — but when you reach for hand-written SQL the differences surface. This page documents what the framework does for you + what to know when you bypass it.
780
+
781
+ ## Target version
782
+
783
+ **SQL Server 2019 or later.** Three reasons:
784
+
785
+ 1. JSON support is mature: `FOR JSON PATH`, `JSON_QUERY`, `JSON_VALUE`, `ISJSON` all stable. 2016/2017 had FOR JSON PATH but `JSON_QUERY` semantics were narrower.
786
+ 2. `OFFSET … FETCH NEXT` optimizer plans stabilized around 2019 — earlier versions could plan-thrash on paginated queries with complex predicates.
787
+ 3. Always-On Availability Groups ship the `sys.dm_hadr_database_replica_states` DMV columns (`end_of_log_lsn`, `last_hardened_lsn`) the framework's RYW adapter reads. Earlier versions exposed equivalent data through other DMVs but the framework doesn't fallback to them.
788
+
789
+ SQL Server 2016 / 2017 work for the basic workflow path; some performance characteristics will be different.
790
+
791
+ ## Configuration
792
+
793
+ ```sh
794
+ DB_DIALECT=mssql
795
+ DB_URL=mssql://sa:<password>@localhost:11433/voltro_test
796
+ # or discrete fields: DB_DIALECT + DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME
797
+
798
+ # Local Docker dev fixture defaults:
799
+ # container: mcr.microsoft.com/mssql/server:2022-latest
800
+ # port: 11433 (mapped from 1433 to avoid the system default)
801
+ # db: voltro_test (created by an init sidecar)
802
+ # user: sa / Voltro_test_99
803
+ ```
804
+
805
+ The framework's docker-compose at `voltro/test/docker-compose.yml` brings up MSSQL on `:11433` with an `mssql-init` sidecar that runs `CREATE DATABASE voltro_test` once the server is healthy. mssql doesn't have a `docker-entrypoint-initdb.d` equivalent, so the framework synthesizes one.
806
+
807
+ ## Driver: `@effect/sql-mssql`
808
+
809
+ Wraps `tedious` (the Node.js TDS driver). The framework's `MssqlStore` opens a connection pool sized via `DB_MAX_CONNECTIONS` (default 10).
810
+
811
+ Tedious returns BIT as proper boolean (✓) and NVARCHAR as string. JSON columns are NVARCHAR(MAX) under the hood — driver returns the raw string. The framework's `decodeRowsFromSchema` JSON.parses any column declared as `json()` in the schema registry; without this, every reactive subscription reading a JSON column would see strings and fail framework Schema validators.
812
+
813
+ ## No `RETURNING` — single-statement `OUTPUT INSERTED.*` / `OUTPUT DELETED.*`
814
+
815
+ MSSQL has the OUTPUT clause for DML, and the framework's `MssqlStore` uses it for **every** write — insert, update, patchJson, delete, `updateMany`, `deleteMany`, and the MERGE upsert — so each returns its post-image (or pre-image, for delete) in ONE round-trip, matching postgres's `RETURNING` behaviour with no follow-up SELECT:
816
+
817
+ ```sql
818
+ -- What MSSQL uses (all single-statement):
819
+ INSERT INTO todos (id, title, done) OUTPUT INSERTED.* VALUES (?, ?, ?)
820
+ UPDATE todos SET done = ? OUTPUT INSERTED.* WHERE id = ?
821
+ DELETE FROM todos OUTPUT DELETED.* WHERE id = ?
822
+ ```
823
+
824
+ The `OUTPUT` clause sits in the T-SQL-required position — between the column list and `VALUES` for INSERT, between `SET` and `WHERE` for UPDATE, between the table and `WHERE` for DELETE. `@effect/sql`'s insert/update helpers carry a `.returning('*')`, and the mssql compiler lowers it to `OUTPUT INSERTED.*` in exactly that spot, so there is no write-then-SELECT fallback anywhere. An empty OUTPUT result is also the existence check: an UPDATE/DELETE by an id that matches no row returns `null` / `false`.
825
+
826
+ ### IDENTITY id recovery
827
+
828
+ Because the write path returns `INSERTED.*`, a raw `IDENTITY(1,1)` primary-key table inserts **without a client-side id** and the server-generated id comes back in the returned row — no `SCOPE_IDENTITY()` round-trip needed. (Framework entities use client-side TypeIDs by default, so this matters only for hand-declared IDENTITY tables.)
829
+
830
+ ```ts
831
+ // ident table: id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(200)
832
+ const row = await store.insert('ident', { title: 'auto' })
833
+ row.id // → the generated INT, e.g. 1
834
+ ```
835
+
836
+ ### Native MERGE upsert
837
+
838
+ `upsert` compiles to a single-statement `MERGE … WITH (HOLDLOCK) … OUTPUT $action, INSERTED.*`:
839
+
840
+ ```sql
841
+ MERGE todos WITH (HOLDLOCK) AS tgt
842
+ USING (VALUES (?, ?, ?)) AS src (id, title, done)
843
+ ON tgt.id = src.id
844
+ WHEN MATCHED THEN UPDATE SET tgt.title = src.title, tgt.done = src.done
845
+ WHEN NOT MATCHED THEN INSERT (id, title, done) VALUES (src.id, src.title, src.done)
846
+ OUTPUT $action AS __action, INSERTED.*;
847
+ ```
848
+
849
+ `WITH (HOLDLOCK)` takes a range lock on the match key so a concurrent upsert on the same conflict key serialises behind it (closing the MERGE insert/update race + the Halloween window). `$action` distinguishes the insert vs update branch, so the emitted `ChangeEvent` carries the right `op`. The **function-form** `update` (compute the patch from the conflicting row) can't be expressed in a MERGE `WHEN MATCHED` clause, so it alone keeps the read-then-compute path.
850
+
851
+ Caveat: MSSQL's OUTPUT clause forbids subqueries (Msg 10705). `MERGE … OUTPUT (SELECT … FROM …) AS X` fails to parse — the framework's MERGE uses only `$action` + `INSERTED.*`, never a subquery. `@effect/cluster`'s upsert path hits the subquery limit → see "Upstream cluster patches" below.
852
+
853
+ ## No `LIMIT / OFFSET` — TOP N / OFFSET … FETCH NEXT
854
+
855
+ MSSQL has two pagination idioms:
856
+
857
+ ```sql
858
+ -- Take-only: SELECT TOP N goes before the projection list.
859
+ SELECT TOP 10 id, name FROM users ORDER BY created_at DESC
860
+
861
+ -- Skip + take: OFFSET … FETCH NEXT goes after the ORDER BY.
862
+ -- Requires ORDER BY (mssql refuses FETCH NEXT without one).
863
+ SELECT id, name FROM users
864
+ ORDER BY created_at DESC
865
+ OFFSET 100 ROWS FETCH NEXT 20 ROWS ONLY
866
+ ```
867
+
868
+ The framework's `compileSelect` emits whichever fits the descriptor:
869
+
870
+ - `take=N, skip=undefined` → `TOP N` prefix.
871
+ - `take=N, skip=M` (or `take=undefined, skip=M`) → `OFFSET M ROWS FETCH NEXT N ROWS ONLY` suffix.
872
+
873
+ When the caller didn't supply an `orderBy` but did set `skip` (uncommon but legal), the framework adds `ORDER BY (SELECT NULL)` as a parser-pacifier — the planner treats it as "any order" with no actual sorting cost.
874
+
875
+ ## Integer-literal LIMIT (same as MySQL)
876
+
877
+ The compiler inlines integer literals for TOP/OFFSET/FETCH NEXT values rather than parameter binding. Same rationale as MySQL — tedious has bind-as-INT issues with large or unexpected-typed numeric params.
878
+
879
+ ## JSON columns — NVARCHAR(MAX) + auto-parse
880
+
881
+ `json()` columns emit `NVARCHAR(MAX)` in DDL — mssql has no native JSON type pre-2025. Validation goes through `ISJSON(col) = 1` CHECK constraints; serialization is application-side.
882
+
883
+ The framework's `decodeRowsFromSchema` JSON.parses these on read so application code always sees objects. Writes go through `JSON.stringify` before bind — the mutation middleware handles this transparently for any `json()` column.
884
+
885
+ If you write hand-rolled queries that read JSON columns through `unsafe()`, you'll get strings. `JSON.parse` them yourself or pre-shape them through `JSON_VALUE(col, '$.field')` in SQL.
886
+
887
+ ## Identifier quoting
888
+
889
+ `[users]` — square brackets. Standard MSSQL. The framework's compiler emits these for every identifier; double quotes work too if `QUOTED_IDENTIFIER ON` is set (which it is by default in modern MSSQL).
890
+
891
+ ## Workflow cluster
892
+
893
+ > **If you run durable cluster workflows on mssql, run `voltro add mssql`.** A pnpm
894
+ > patch lives in your workspace config, NOT in a published npm tarball — so a plain
895
+ > `pnpm install` of the framework can't carry it. `voltro add mssql` ships the
896
+ > `.patch` file (it's bundled in the CLI) into your `patches/` and adds the
897
+ > `patchedDependencies` entry to your `pnpm-workspace.yaml`; the next `pnpm install`
898
+ > then applies it. Idempotent, and a no-op on every other dialect. Without it,
899
+ > workflow message/runner storage misbehaves on SQL Server.
900
+
901
+ `@effect/cluster`'s mssql branch uses `sp_getapplock` for shard claims and `MERGE … WHEN NOT MATCHED THEN INSERT … OUTPUT INSERTED` for runner upserts. Four bugs in `@effect/cluster@0.59.0` mssql code paths fail under the framework's workflow stack; the framework carries a `pnpm patch` (shipped in the CLI at `packages/cli/templates/patches/@effect__cluster@0.59.0.patch`, written into your project by `voltro add mssql`):
902
+
903
+ ### Patch 1 — SqlRunnerStorage shard-lock MERGE alias
904
+
905
+ The shard-lock acquireShards SQL wraps its VALUES list in an extra SELECT:
906
+
907
+ ```sql
908
+ -- Original (illegal in MSSQL):
909
+ USING (SELECT * FROM (VALUES (...)) ) AS source (shard_id, address, acquired_at)
910
+
911
+ -- Patched (drops the wrap):
912
+ USING (VALUES (...)) AS source (shard_id, address, acquired_at)
913
+ ```
914
+
915
+ The inner `(VALUES ...)` derived table doesn't get its own alias, and the outer SELECT can't introduce a column-name list. Flattening fixes it.
916
+
917
+ ### Patch 2 — SqlMessageStorage `FOR UPDATE` in non-cursor SELECT
918
+
919
+ `SELECT … ORDER BY … FOR UPDATE` is illegal in MSSQL outside a `DECLARE CURSOR`. Postgres/MySQL use it for row-locking; MSSQL needs `WITH (UPDLOCK, ROWLOCK)` table hints. The patch dispatches `mssql` to an empty literal (same path SQLite takes), accepting the race-window tradeoff the upstream cluster already accepts for SQLite.
920
+
921
+ ### Patch 3 — SqlMessageStorage `insertEnvelope` MERGE-with-OUTPUT subqueries
922
+
923
+ MSSQL forbids subqueries inside OUTPUT clauses (Msg 10705). The original `insertEnvelope` MERGE used `CASE WHEN inserted.id IS NULL THEN (SELECT …) END` in the OUTPUT list. The patch restructures: `MERGE … OUTPUT inserted.id;` then a conditional follow-up SELECT joining the replies table. Same control flow as the mysql branch.
924
+
925
+ ### Patch 4 — `envelopeToRow` BigInt for `deliver_at`
926
+
927
+ The cluster's `deliver_at` column is BIGINT (storing millisecond epoch). Tedious binds JS `number` parameters as INT — which overflows for any post-2001 timestamp. The patch casts `deliver_at` to `BigInt` once at the top of `envelopeToRow` so all three message-kind switch arms emit `bigint | null`; other dialects accept bigint fine.
928
+
929
+ All four patches are dialect-keyed (touch only the `mssql:` branch of `sql.onDialectOrElse`) so postgres / mysql / sqlite paths are bit-identical to upstream. The framework carries them and ships them to your project via `voltro add mssql` (see the note at the top of this section) — there is no newer `@effect/cluster` to bump to (0.59.0 is the latest), so the patch is how the fix reaches you.
930
+
931
+ ## Read replicas — Always-On Availability Groups
932
+
933
+ The framework's mssql replication adapter reads `end_of_log_lsn` from `sys.dm_hadr_database_replica_states` to measure replica freshness against the primary's `last_hardened_lsn`. The LSN triplet is parsed and compared lexicographically.
934
+
935
+ ```sh
936
+ DB_REPLICA_URLS=mssql://app:app@replica-1:1433/voltro_app
937
+ RYW_POLICY=fallback
938
+ ```
939
+
940
+ Requires the deployment to use Always-On AGs (the modern HA story since SQL Server 2012). The framework does NOT support Log Shipping or Database Mirroring — they have different position tracking. If you're on those, stick with primary-only routing.
941
+
942
+ ## CDC — Change Tracking (cross-instance reactivity)
943
+
944
+ SQL Server ships **Change Tracking** (CT) — a lightweight, built-in change source available on every edition (unlike the heavier Change Data Capture feature). The framework's mssql store uses it as an out-of-band CDC reader so a write on ANY instance surfaces on EVERY instance's `onChange` — the mssql equivalent of postgres `LISTEN/NOTIFY` or mariadb binlog CDC.
945
+
946
+ Enable it and set `changeStrategy: 'cdc'` (the default when `CDC` is not `0`):
947
+
948
+ ```sh
949
+ DB_DIALECT=mssql
950
+ CDC=1 # default; set CDC=0 for single-instance inline emit
951
+ ```
952
+
953
+ ```sql
954
+ -- One-time, at the database level (the store enables per-table CT itself):
955
+ ALTER DATABASE voltro_test
956
+ SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON);
957
+ ```
958
+
959
+ ```
960
+ [voltro:dev] sql dialect resolved: mssql — CDC: Change Tracking (polled), RETURNING: OUTPUT INSERTED/DELETED
961
+ ```
962
+
963
+ How it works: a polling reader tails each reactive table via `CHANGETABLE(CHANGES <t>, @sinceVersion)`, advancing a database-wide `CHANGE_TRACKING_CURRENT_VERSION()` cursor that it checkpoints per-replica into `_voltro_cdc_offsets` (so a restart resumes where it left off). Rows are joined back to the base table for the current image; the CT op (`I`/`U`/`D`) maps to `insert`/`update`/`delete`. If auto-cleanup outruns a replica's cursor (`CHANGE_TRACKING_MIN_VALID_VERSION` passes it), the reader jumps to the current version and the dispatcher's per-subscribe re-query self-heals.
964
+
965
+ Two honest limitations of Change Tracking, both documented so subscribers don't assume more than CT gives:
966
+
967
+ - **Net-change, not a change log.** Between two polls, an insert→update→delete of one row collapses to a single delete, and an insert→update to a single insert carrying the latest image. The framework surfaces the net effect per poll cycle. Reactive queries re-read on any change, so a collapsed intermediate never produces a wrong result — but an exactly-once consumer must not assume it sees every intermediate op.
968
+ - **No before-image.** CT reports the primary key + op, not the prior row. So an update's `ChangeEvent.old` is `null` (the `new` image carries the current row), and a delete's `old` carries only the id. Mariadb binlog CDC (`binlog_row_image=FULL`) gives a full before-image; CT does not.
969
+
970
+ For a full before-image or per-op fidelity, use **mariadb** (binlog CDC). For zero-infra single-instance reactivity, set `CDC=0` (inline emit — the writing instance emits its own deltas, never cross-instance).
971
+
972
+ ## Known caveats
973
+
974
+ - **`tedious` connections close abruptly on AAD-only auth refresh**. Use SQL auth (sa / app role) for app-tier deploys; reserve AAD for admin access.
975
+ - **`NVARCHAR(MAX)` columns can't be in indexes** with `INCLUDE` clauses pre-2017. The framework's index emitter doesn't try; you get an error at migration time if you compose `.index([jsonCol])`.
976
+ - **`DATETIME2(6)` is the framework default for timestamps**; older DATETIME drops sub-second precision and serialization on the wire differs.
977
+ - **`MERGE` has subtle race conditions** documented by Microsoft. The framework's `upsert` uses `MERGE … WITH (HOLDLOCK)` to dodge them; if you write hand-rolled MERGE, copy the HOLDLOCK hint.
978
+
979
+ ## Where it lives
980
+
981
+ - `voltro/packages/sql-mssql/src/store.ts` — `MssqlStore` with single-statement OUTPUT INSERTED/DELETED, native MERGE upsert, IDENTITY id recovery, Change Tracking CDC consumer
982
+ - `voltro/packages/sql-mssql/src/changeTrackingCdc.ts` — the polled Change Tracking reader (`CHANGETABLE` / `CHANGE_TRACKING_CURRENT_VERSION`)
983
+ - `voltro/packages/sql-mssql/src/cdcOffsetsTable.ts` — `_voltro_cdc_offsets` per-replica CT resume checkpoint
984
+ - `voltro/packages/sql-mssql/src/retry.ts` — `isRetryableMssqlFailure` (1205 deadlock)
985
+ - `voltro/packages/sql-mssql/src/replicationAdapter.ts` — `end_of_log_lsn` / `last_hardened_lsn` compare for RYW
986
+ - `voltro/packages/database/src/sqlCompiler.ts` — TOP / FETCH NEXT dispatch
987
+ - `voltro/packages/database/src/migrate.ts` — mssql DDL branch (line 72)
988
+ - `packages/cli/templates/patches/@effect__cluster@0.59.0.patch` — the four upstream patches (shipped in the CLI; `voltro add mssql` writes it into your project)
989
+
990
+
991
+
992
+ ---
993
+
994
+ <!-- source: en/database/dialects/sqlite.md -->
995
+ ## SQLite 3.38+
996
+
997
+ _Single-process by design. Workflows run with in-memory runner storage. Read replicas n/a. In-process EventEmitter CDC bus. TEXT storage for JSON/timestamps/dates with framework auto-coercion on read._
998
+
999
+ SQLite is the framework's single-process dialect — Dev environments, embedded apps, CLIs, edge-deployed single-tenant tools. The architectural trade-offs are different from the server dialects: no cluster scale-out, no read replicas, sub-millisecond CDC. The framework documents these explicitly because users frequently underestimate the inherent single-process constraint.
1000
+
1001
+ ## Target version
1002
+
1003
+ **SQLite 3.38 or later.** Two reasons:
1004
+
1005
+ 1. `json_object` + `json_group_array` (the framework's eager-load idiom on sqlite) stabilized in 3.38. 3.37.x supports them but with subtle ordering quirks.
1006
+ 2. `RETURNING *` on DML was added in 3.35. The framework relies on it.
1007
+
1008
+ `better-sqlite3` (the bundled driver) ships modern SQLite versions — version targeting is more about your build environment than the runtime.
1009
+
1010
+ ## Configuration
1011
+
1012
+ ```sh
1013
+ DB_DIALECT=sqlite
1014
+
1015
+ # File backend:
1016
+ DB_URL=file:./db.sqlite # relative path (resolved against cwd)
1017
+ DB_URL=file:/abs/path/db.sqlite
1018
+
1019
+ # Ephemeral in-memory backend:
1020
+ DB_URL=:memory:
1021
+ ```
1022
+
1023
+ No host, no port, no credentials. SQLite is process-local.
1024
+
1025
+ ## Driver: `@effect/sql-sqlite-node` (better-sqlite3)
1026
+
1027
+ Synchronous driver under the hood — the framework wraps every call in `Effect.tryPromise` so the API surface stays Promise-based. Performance is excellent for single-process workloads (no inter-process IPC, no network round-trips).
1028
+
1029
+ The driver returns INTEGER for BOOLEAN columns (sqlite stores them as 0/1) and TEXT for everything else (no native DATE / TIMESTAMP / JSON types). The framework's `decodeRowsFromSchema` post-processor handles all three:
1030
+
1031
+ - `boolean()` columns: 0/1 → false/true
1032
+ - `json()` columns: TEXT → object (JSON.parse)
1033
+ - `timestamp()` / `date()` columns: ISO string → Date
1034
+
1035
+ On write, `coerceForSqlite` converts Date → ISO string and boolean → 0/1 before binding.
1036
+
1037
+ ## No cluster — `runnerStorage: 'memory'`
1038
+
1039
+ SQLite is single-process by definition. `@effect/cluster`'s `SqlRunnerStorage` requires a coordinatable lock primitive (advisory locks, GET_LOCK, sp_getapplock) — SQLite has none.
1040
+
1041
+ The framework boots the workflow engine with `runnerStorage: 'memory'` when the dialect resolves to sqlite. What this means:
1042
+
1043
+ - Workflows run durably **within ONE process**. Crash + restart resumes from the persisted journal.
1044
+ - Multi-replica deployments are **not possible**. The cluster's `getRunners()` returns only the local instance; there's no shard re-assignment because there's no second runner to assign to.
1045
+ - Boot log surfaces this:
1046
+
1047
+ ```
1048
+ [voltro:dev] workflow engine: cluster-memory, dialect=sqlite — single-process durable replay, no horizontal scaling
1049
+ ```
1050
+
1051
+ For SQLite use cases this constraint is usually intentional — a CLI tool that runs workflows during a single command execution, a desktop app where the whole framework lives in the same process. If you're considering multi-replica scale-out you've outgrown SQLite; switch to postgres / mysql / mariadb / mssql.
1052
+
1053
+ ## No read replicas
1054
+
1055
+ SQLite is single-writer by definition. Setting `DB_REPLICA_URLS=…` is a no-op with a warning:
1056
+
1057
+ ```
1058
+ [voltro:dev] read replicas: not applicable (sqlite is single-process); ignoring DB_REPLICA_URLS
1059
+ ```
1060
+
1061
+ ## CDC — in-process EventEmitter
1062
+
1063
+ Sqlite has no LISTEN/NOTIFY equivalent and no trigger-based fan-out is needed (everything runs in one process). The framework's `SqliteStore` uses an in-process Node `EventEmitter` (composed as a private field, not subclassed):
1064
+
1065
+ - Insert/update/delete emit `'change'` events synchronously to the dispatcher.
1066
+ - The dispatcher's `onChange` callback is registered against the emitter — no polling, no triggers, no log table.
1067
+
1068
+ Latency: sub-millisecond. Bounded by Node's event-loop tick.
1069
+
1070
+ This is the FASTEST CDC path the framework offers. The tradeoff is the inherent single-process constraint — there's no cross-process or cross-machine fan-out to worry about.
1071
+
1072
+ ## JSON columns — TEXT with auto-coerce
1073
+
1074
+ `json()` columns map to TEXT in DDL. SQLite's optional `json` extension validates content via the `JSON1` functions but enforces no type — TEXT is what you get on the wire.
1075
+
1076
+ The framework's `decodeRowsFromSchema` JSON.parses any column declared as `json()` in the schema registry. Writes go through `JSON.stringify` in the mutation middleware. Application code sees objects on both sides.
1077
+
1078
+ ## Identifier quoting
1079
+
1080
+ `"name"` — double quotes. Same as postgres.
1081
+
1082
+ ## Migration emitter
1083
+
1084
+ `voltro migrate` against sqlite emits:
1085
+
1086
+ - `CREATE TABLE IF NOT EXISTS …`
1087
+ - `CREATE INDEX IF NOT EXISTS …`
1088
+ - FK constraints via `REFERENCES … ON DELETE CASCADE/RESTRICT/SET NULL` (sqlite supports these since `PRAGMA foreign_keys = ON`, which the framework sets at connect)
1089
+ - `INTEGER PRIMARY KEY AUTOINCREMENT` for numeric ids
1090
+ - `TEXT` for ids, text, timestamp, date, json, references
1091
+ - `INTEGER` for booleans
1092
+ - `BLOB` for vectors
1093
+
1094
+ The `_voltro_migrations` ledger is a regular table keyed by migration id — re-running the same migration is a no-op. WAL mode is enabled at connect (`PRAGMA journal_mode = WAL`) for better concurrency and crash safety.
1095
+
1096
+ ## File vs `:memory:`
1097
+
1098
+ - `:memory:` — ephemeral, lives in the process's address space, dies on exit. Use for tests + smoke fixtures.
1099
+ - `file:./db.sqlite` — durable, lives at the filesystem path. The framework auto-creates the file on first write. WAL mode means you'll see `db.sqlite-wal` + `db.sqlite-shm` sidecars; that's expected.
1100
+
1101
+ When you copy or back up a sqlite file, capture all THREE files together (the WAL contains uncommitted-to-main writes). Or run `PRAGMA wal_checkpoint(FULL)` first to fold the WAL back into the main file.
1102
+
1103
+ ## Known caveats
1104
+
1105
+ - **`PRAGMA foreign_keys` is OFF by default**. The framework turns it on at every connect; if you open the database via another tool (sqlite3 CLI, DBeaver) and run mutations, you bypass FK enforcement.
1106
+ - **Single-writer**. SQLite serializes writes — a long-running write blocks every other write on the same database file. WAL mode helps readers (they don't block) but doesn't help writers.
1107
+ - **Date arithmetic is string-based**. `timestamp()` columns store ISO-8601 text; comparing two timestamps is lexical (which works because ISO-8601 sorts correctly) but date math requires the framework's higher-level API, not raw SQL.
1108
+ - **No native DECIMAL**. The framework doesn't ship a decimal type yet — `integer()` and `number()` (floating-point) are it. Money values: integer cents.
1109
+
1110
+ ## Where it lives
1111
+
1112
+ - `voltro/packages/sql-sqlite/src/store.ts` — `SqliteStore` with EventEmitter CDC + `coerceForSqlite` on write
1113
+ - `voltro/packages/sql-sqlite/src/retry.ts` — `isRetryableSqliteFailure` (SQLITE_BUSY / SQLITE_LOCKED)
1114
+ - `voltro/packages/database/src/migrate.ts` — sqlite DDL branch (line 46)
1115
+ - `voltro/packages/database/src/jsonEagerCompiler.ts` — `compileSqliteEntry` using `json_object` / `json_group_array`
1116
+ - `voltro/packages/database/src/rowDecoder.ts` — read-path JSON / boolean / Date coercion against schema
1117
+ - `voltro/packages/workflow/src/clusterLayer.ts` — `runnerStorage: 'memory'` branch for sqlite
1118
+
1119
+
1120
+
1121
+ ---
1122
+
1123
+ <!-- source: en/database/dialects/turso.md -->
1124
+ ## Turso (beta)
1125
+
1126
+ _The Rust rewrite of SQLite (@tursodatabase/database) with MVCC concurrent writes via BEGIN CONCURRENT — OR remote Turso Cloud (libsql://) with embedded-replica sync via @libsql/client. Routed by URL scheme. SQLite-compatible SQL. Single-node local, beta. No generated columns, no FTS; numeric ids drop AUTOINCREMENT; no Alpine/musl or Intel-mac prebuilts on the local engine._
1127
+
1128
+ Turso is the **Rust rewrite of SQLite** (`@tursodatabase/database`, formerly Limbo). It speaks SQLite's SQL dialect, file format, and a better-sqlite3-shaped driver — so the framework reuses the entire SQLite-family store and SQL compiler. The ONE thing it adds over the `sqlite` dialect is the reason to pick it: **MVCC concurrent writes** via `BEGIN CONCURRENT`. Where `sqlite` serializes every write through a single connection, `turso` runs a connection POOL where multiple transactions commit concurrently; a write-write conflict is detected and the framework retries it transparently.
1129
+
1130
+ The `turso` dialect has **two live backends, routed by the connection URL scheme** — not two versions of the same thing, but a real config choice:
1131
+
1132
+ - **Local Rust engine** (`file:` / `:memory:`) — the embedded `@tursodatabase/database` engine described above: pooled, single-process, MVCC concurrent writes. **beta** (`0.x`).
1133
+ - **Remote Turso Cloud** (`libsql://` / `https://` / `wss://`) — a connection to a hosted Turso Cloud database over the official `@libsql/client`, with an auth token. Also supports an **embedded replica**: a local `file:` copy that periodically syncs from a remote primary (local-latency reads, writes forwarded to the primary).
1134
+
1135
+ Treat the local engine as "SQLite for a single node that needs real write concurrency", not a production-hardened engine; treat the remote/replica backend as "your app runs against managed Turso Cloud". For multi-instance scale-out with sub-second local reactivity use postgres / mysql / mariadb / mssql; for a rock-stable single-process embed use `sqlite`.
1136
+
1137
+ ## Configuration
1138
+
1139
+ The URL scheme picks the backend — local engine vs remote Turso Cloud vs embedded replica:
1140
+
1141
+ ```sh
1142
+ DB_DIALECT=turso
1143
+
1144
+ # ── Local Rust engine ─────────────────────────────────────────
1145
+ DB_URL=file:./db.turso # relative path (resolved against cwd)
1146
+ DB_URL=file:/abs/path/db.turso
1147
+ DB_URL=:memory: # ephemeral, pool forced to size 1 (see below)
1148
+ DB_MAX_CONNECTIONS=8 # pool size = the MVCC write-concurrency knob. Default 4.
1149
+
1150
+ # ── Remote Turso Cloud ────────────────────────────────────────
1151
+ DB_URL=libsql://your-db.turso.io # or https:// / wss://
1152
+ DB_AUTH_TOKEN=<token> # from `turso db tokens create <db>` — never logged/baked
1153
+
1154
+ # ── Embedded replica (local file that syncs from a remote primary) ─
1155
+ DB_URL=file:./replica.db # the LOCAL replica file
1156
+ DB_SYNC_URL=libsql://your-db.turso.io # the remote primary it syncs FROM
1157
+ DB_AUTH_TOKEN=<token>
1158
+ DB_SYNC_INTERVAL=30 # optional: pull-sync every N seconds (else sync on boot only)
1159
+ ```
1160
+
1161
+ The scheme routes the connection: `file:` / `:memory:` → the **local** Rust engine; `libsql://` / `https://` / `wss://` → **remote** Turso Cloud (needs `DB_AUTH_TOKEN`); a `file:` URL **paired with `DB_SYNC_URL`** → an **embedded replica**. So the same `file:` scheme means "local engine" without a sync URL and "libsql embedded replica" with one. A remote URL without an auth token, or a `DB_SYNC_URL` without one, **fails loud at boot** with an actionable message. The token comes from `DB_AUTH_TOKEN` (alias `TURSO_AUTH_TOKEN`) — it is never logged or baked into the build.
1162
+
1163
+ ## Remote Turso Cloud + embedded replicas
1164
+
1165
+ The remote backend connects through the official `@libsql/client`:
1166
+
1167
+ - **Remote Turso Cloud** (`libsql://` / `https://` / `wss://`): every statement runs against your hosted Turso database; `ctx.store.transactional(...)` opens an interactive libsql `transaction('write')`. Turso Cloud serialises writes server-side (there's no client-side `BEGIN CONCURRENT` here — that's the local engine's mechanism), and a transient busy/conflict is retried by the same transaction-retry the local engine uses.
1168
+ - **Embedded replica** (`file:` + `DB_SYNC_URL`): the client keeps a **local copy** of the database that reads with local latency and syncs from the remote primary. The framework does an initial `client.sync()` at boot so the first reads see a warm replica; if `DB_SYNC_INTERVAL` is set, the driver keeps pulling on that cadence. Writes are forwarded to the primary. Optional `DB_READ_YOUR_WRITES` (default on) makes a local read after a write wait until the replica has caught up; `DB_ENCRYPTION_KEY` encrypts the local replica file at rest.
1169
+
1170
+ Everything above this seam is identical to the local engine — the same reused SQLite-family `DataStore`, the same schema/query/mutation code. Only the transport differs.
1171
+
1172
+ > The local Rust engine and the remote/replica backend are a deliberate config choice, not a versioned split. Pick `file:`/`:memory:` for an embedded single-process DB with MVCC write concurrency; pick `libsql://` (± an embedded replica) to run against managed Turso Cloud.
1173
+
1174
+ ## MVCC is mandatory — `journal_mode=experimental_mvcc`
1175
+
1176
+ `BEGIN CONCURRENT` is only accepted when MVCC is enabled, so every pooled connection runs `PRAGMA journal_mode=experimental_mvcc` at open. This is not optional for the dialect — it's what the whole thing is for. The store's DML transactions run as `BEGIN CONCURRENT`, so every `ctx.store.transactional(...)` (i.e. every mutation) opens a concurrent transaction. DDL keeps plain `BEGIN` (Turso rejects DDL inside a concurrent transaction), so the `@effect/sql` client defaults to `BEGIN` and the store upgrades its own transactions per-fiber — migrations (including the workflow engine's) still work.
1177
+
1178
+ ### Concurrent writes + automatic retry
1179
+
1180
+ Two mutations that touch the same row run on two different pooled connections, each with its own MVCC snapshot. The loser of the commit race gets a `Write-write conflict`; the framework's transaction retry (exponential backoff, a few attempts) replays the whole transaction body on a fresh snapshot that now sees the winner's commit. Your handler code is unchanged — write a normal transactional mutation and the concurrency + retry happen underneath.
1181
+
1182
+ Size the pool (`DB_MAX_CONNECTIONS`) to the concurrent-mutation count you expect. `:memory:` is per-connection (each connection is a private database), so the pool is **forced to size 1** there — `:memory:` is for tests, not for exercising concurrency.
1183
+
1184
+ ## Driver: `@tursodatabase/database` (native NAPI)
1185
+
1186
+ Async driver (`connect()` → `prepare()` → `run`/`get`/`all`), wrapped in `Effect`. The framework's `decodeRowsFromSchema` post-processor handles the same SQLite read quirks as the `sqlite` dialect:
1187
+
1188
+ - `boolean()` columns: 0/1 → false/true
1189
+ - `json()` columns: TEXT → object (JSON.parse)
1190
+ - `timestamp()` / `date()` columns: ISO string → Date
1191
+
1192
+ On write, the same coercion converts Date → ISO string and boolean → 0/1 before binding.
1193
+
1194
+ **Prebuilt binaries ship for `linux-x64-gnu`, `linux-arm64-gnu`, `win32-x64-msvc`, `darwin-arm64` only — there is NO Alpine/musl or Intel-mac binary.** A Node Docker image on Alpine will fail to install the driver; use a glibc base image (`node:22-bookworm-slim`, etc.). This applies to the **local** engine only — the remote / embedded-replica backend goes through `@libsql/client` (pure JS + its own bindings) and is not bound by the local engine's prebuilt matrix.
1195
+
1196
+ ## No cluster — `runnerStorage: 'memory'`
1197
+
1198
+ Like `sqlite`, Turso is single-node here. The workflow engine boots with `runnerStorage: 'memory'`: workflows run durably within ONE process (crash + restart resumes from the journal), multi-replica scale-out is not possible, and `DB_REPLICA_URLS` is a no-op with a warning. (The pool gives concurrent *writes* on one node — it does NOT give horizontal scale-out.)
1199
+
1200
+ ## CDC — in-process EventEmitter
1201
+
1202
+ Same as `sqlite`: an in-process Node `EventEmitter` fans insert/update/delete events to the dispatcher. Sub-millisecond, single-process. No `LISTEN/NOTIFY`, no triggers.
1203
+
1204
+ ## Not supported on Turso (beta gaps)
1205
+
1206
+ These are SQLite features the `sqlite` dialect has but Turso's MVCC mode does not. The framework fails LOUD at migrate rather than emit DDL the engine rejects:
1207
+
1208
+ - **Generated columns** (`.generatedAs(...)`). Turso MVCC rejects STORED/VIRTUAL generated columns — migrate throws a clear error. Drop the column or use `sqlite` / `postgres`.
1209
+ - **Full-text search** (`.fullTextIndex(...)`). MVCC has no virtual tables (FTS5) — migrate throws. Use `sqlite` / `postgres` for FTS.
1210
+ - **`AUTOINCREMENT`.** Numeric ids (`id({ scheme: 'numeric' })`) emit plain `INTEGER PRIMARY KEY` (still auto-allocating, just without the no-reuse-of-deleted-ids guarantee). The default TypeID/ULID schemes are unaffected.
1211
+
1212
+ Everything else — `RETURNING`, `ON CONFLICT` upserts, `json_object`/`json_group_array` eager-loads, PRAGMA introspection, partial + expression indexes, CHECK constraints, namespace ATTACH (multi-tenant physical isolation) — works identically to `sqlite`.
1213
+
1214
+ ## DDL runs in autocommit
1215
+
1216
+ Turso rejects DDL inside a `BEGIN CONCURRENT` transaction ("DDL statements require an exclusive transaction"). So `voltro migrate` / auto-migrate runs each DDL statement in **autocommit** on turso (sequential, idempotent `CREATE … IF NOT EXISTS`). A partial failure simply recovers on the next boot; DML transactions still use `BEGIN CONCURRENT`.
1217
+
1218
+ ## Identifier quoting
1219
+
1220
+ `"name"` — double quotes. Same as postgres + sqlite.
1221
+
1222
+ ## File vs `:memory:`
1223
+
1224
+ - `:memory:` — ephemeral, per-connection, pool clamped to 1. Tests + smoke fixtures.
1225
+ - `file:./db.turso` — durable. Pool of N connections share the one file; MVCC coordinates concurrent writers.
1226
+
1227
+ ## Where it lives
1228
+
1229
+ - `voltro/packages/sql-turso/src/sqlLayer.ts` — the URL-scheme ROUTER: `connectionFromConfig` parses a `ConnectionConfig` (+ token/sync env) into a `local` (Rust engine) or `remote` (libsql) connection and builds the matching layer
1230
+ - `voltro/packages/sql-turso/src/sqlClient.ts` — the LOCAL pooled `@effect/sql` client: async connection, per-connection prepare cache, `Pool` acquirers, `beginTransaction: 'BEGIN'` (DDL-safe default; the store upgrades its own DML transactions to `BEGIN CONCURRENT` per-fiber via `ConcurrentTransaction`), MVCC pragma, `:memory:` size-1 clamp
1231
+ - `voltro/packages/sql-turso/src/libsqlClient.ts` — the REMOTE `@effect/sql` client over `@libsql/client`: autocommit `client.execute`, an interactive `client.transaction('write')` for `transactional()` (with BEGIN as a no-op and COMMIT/ROLLBACK mapped onto the tx object), and the embedded-replica sync (`syncUrl` + initial `client.sync()`). The auth token is config/env-sourced and never logged
1232
+ - `voltro/packages/sql-turso/src/retry.ts` — `isTursoRetryableFailure` (matches the `"Write-write conflict"` / busy MESSAGE; the local engine tags every error with a generic code, so the retry identity is the message)
1233
+ - `voltro/packages/sql-turso/src/index.ts` — `tursoDialect` (`id: 'turso'`); reuses `makeSqliteStore` from `@voltro/sql-sqlite` with the routed client + retry predicate
1234
+ - `voltro/packages/sql-sqlite/src/store.ts` — the shared SQLite-family store (driver + retry predicate + span name are injected)
1235
+ - `voltro/packages/database/src/migrate.ts` — turso DDL branch (numeric-id without AUTOINCREMENT; generated-column + FTS guards; autocommit DDL)