@voltro/cli 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/CHANGELOG.md +655 -0
  2. package/THIRD-PARTY-NOTICES.md +90 -4
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BceikpQf.js} +3 -3
  4. package/dist/apiBuild-CxvR1Dwt.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BMr-X48w.js +2427 -0
  7. package/dist/checkCommand-BOk9y264.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-CSkrUI1h.js → commands-D4Y1-Nlu.js} +8575 -9366
  10. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-C7hbl5pk.js} +345 -266
  11. package/dist/dbCommand-D8nhyppR.js +2 -0
  12. package/dist/{dev-DlBWWnJQ.js → dev-Cr-teYsz.js} +1 -1
  13. package/dist/{dev-CEZwJhmb.js → dev-DmnkH-wr.js} +2939 -1832
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-BzAajuUv.js} +115 -99
  16. package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1 -1
  19. package/dist/{inspect-CUCCzw2I.js → inspect-BUUjt773.js} +349 -192
  20. package/dist/inspect-mmBuRXmy.js +2 -0
  21. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-BeJdP_3b.js} +1068 -978
  22. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-BLrVuSlM.js} +1 -1
  23. package/dist/manifestBuild-Dj8Jjoto.js +2 -0
  24. package/dist/sdkgen-CqpOq_fy.js +582 -0
  25. package/dist/serveCommand-Ciq_wIY8.js +1577 -0
  26. package/dist/serveEntry.js +2 -2
  27. package/dist/{start-BGXIf6zT.js → start-ocqB1seX.js} +283 -270
  28. package/dist/startEntry.js +2 -2
  29. package/package.json +17 -17
  30. package/templates/AGENTS.core.md +50 -0
  31. package/templates/AGENTS.md +54 -2
  32. package/templates/agent-docs/_index.md +4 -2
  33. package/templates/agent-docs/_manifest.json +26 -5
  34. package/templates/agent-docs/ai.md +191 -0
  35. package/templates/agent-docs/cli.md +143 -6
  36. package/templates/agent-docs/configuration.md +32 -0
  37. package/templates/agent-docs/data.md +282 -0
  38. package/templates/agent-docs/database/migrations.md +47 -0
  39. package/templates/agent-docs/database/misc.md +57 -0
  40. package/templates/agent-docs/database/schema.md +3 -1
  41. package/templates/agent-docs/internationalization.md +169 -8
  42. package/templates/agent-docs/local-first-mobile.md +426 -0
  43. package/templates/agent-docs/routing.md +94 -3
  44. package/templates/agent-docs/schema-driven-ui.md +12 -0
  45. package/templates/agent-docs/security.md +17 -0
  46. package/templates/agent-docs/templates/apibackends.md +210 -1
  47. package/templates/agent-docs/templates/appshells.md +144 -1
  48. package/templates/agent-docs/templates/mobile.md +60 -0
  49. package/templates/agent-docs/templates/overview.md +17 -7
  50. package/templates/agent-docs/whats-new.md +238 -158
  51. package/templates/agent-docs/workflows.md +376 -0
  52. package/templates/apps/api-ai/package.json +7 -7
  53. package/templates/apps/api-auth/package.json +8 -8
  54. package/templates/apps/api-backend/package.json +7 -7
  55. package/templates/apps/api-backend-deactivation/package.json +7 -7
  56. package/templates/apps/api-backend-mail/package.json +8 -8
  57. package/templates/apps/api-backend-mariadb/package.json +9 -9
  58. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  59. package/templates/apps/api-backend-sqlite/README.md +38 -0
  60. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  61. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  62. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  63. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  64. package/templates/apps/api-backend-sqlite/package.json +30 -0
  65. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  66. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  67. package/templates/apps/api-backend-sqlite/template.json +6 -0
  68. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  69. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  70. package/templates/apps/api-backend-storage/package.json +8 -8
  71. package/templates/apps/api-cms/README.md +104 -0
  72. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  73. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  74. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  75. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  76. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  77. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  78. package/templates/apps/api-cms/app.config.ts +61 -0
  79. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  80. package/templates/apps/api-cms/content/index.ts +18 -0
  81. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  82. package/templates/apps/api-cms/database/schema.ts +64 -0
  83. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  84. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  85. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  86. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  87. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  88. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  89. package/templates/apps/api-cms/package.json +32 -0
  90. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  91. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  92. package/templates/apps/api-cms/template.json +6 -0
  93. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  94. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  95. package/templates/apps/api-cms/tsconfig.json +5 -0
  96. package/templates/apps/api-collab/README.md +70 -0
  97. package/templates/apps/api-collab/app.config.ts +28 -0
  98. package/templates/apps/api-collab/database/schema.ts +63 -0
  99. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
  100. package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
  101. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
  102. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
  103. package/templates/apps/api-collab/package.json +30 -0
  104. package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
  105. package/templates/apps/api-collab/queries/documents.query.ts +23 -0
  106. package/templates/apps/api-collab/template.json +6 -0
  107. package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
  108. package/templates/apps/api-collab/tsconfig.json +5 -0
  109. package/templates/apps/api-data-advanced/package.json +8 -8
  110. package/templates/apps/api-durable/package.json +8 -8
  111. package/templates/apps/api-feature-flags/package.json +9 -9
  112. package/templates/apps/api-governance/package.json +8 -8
  113. package/templates/apps/api-kv/package.json +8 -8
  114. package/templates/apps/api-moderation/package.json +8 -8
  115. package/templates/apps/api-observability/package.json +8 -8
  116. package/templates/apps/api-ratelimit/package.json +8 -8
  117. package/templates/apps/api-rbac/package.json +8 -8
  118. package/templates/apps/api-rest/package.json +7 -7
  119. package/templates/apps/api-saas/package.json +11 -11
  120. package/templates/apps/api-saas-starter/README.md +103 -0
  121. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  122. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  123. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  124. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  125. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  126. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  127. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  128. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  129. package/templates/apps/api-saas-starter/package.json +32 -0
  130. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  131. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  132. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  133. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  134. package/templates/apps/api-saas-starter/template.json +6 -0
  135. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  136. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  137. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  138. package/templates/apps/api-search/package.json +8 -8
  139. package/templates/apps/api-status/README.md +88 -0
  140. package/templates/apps/api-status/app.config.ts +36 -0
  141. package/templates/apps/api-status/authz.ts +33 -0
  142. package/templates/apps/api-status/database/schema.ts +70 -0
  143. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  144. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  145. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  146. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  147. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  148. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  149. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  150. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  151. package/templates/apps/api-status/package.json +30 -0
  152. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  153. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  154. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  155. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  156. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  157. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  158. package/templates/apps/api-status/template.json +6 -0
  159. package/templates/apps/api-status/tests/status.test.ts +70 -0
  160. package/templates/apps/api-status/tsconfig.json +5 -0
  161. package/templates/apps/api-versioning/package.json +8 -8
  162. package/templates/apps/api-webhooks/package.json +9 -9
  163. package/templates/apps/changelog/package.json +6 -6
  164. package/templates/apps/edge-functions/package.json +2 -2
  165. package/templates/apps/frontend-admin/app.config.ts +1 -1
  166. package/templates/apps/frontend-admin/package.json +8 -8
  167. package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
  168. package/templates/apps/frontend-app/app.config.ts +1 -1
  169. package/templates/apps/frontend-app/package.json +8 -8
  170. package/templates/apps/frontend-app/src/locales/index.ts +1 -1
  171. package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
  172. package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
  173. package/templates/apps/frontend-auth/README.md +78 -0
  174. package/templates/apps/frontend-auth/app.config.ts +34 -0
  175. package/templates/apps/frontend-auth/package.json +32 -0
  176. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  177. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  178. package/templates/apps/frontend-auth/src/config.ts +11 -0
  179. package/templates/apps/frontend-auth/src/globals.css +105 -0
  180. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  181. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  182. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  183. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  184. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  185. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  186. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  187. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  188. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  189. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  190. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  191. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  192. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  193. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  194. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  195. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  196. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  197. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  198. package/templates/apps/frontend-auth/template.json +6 -0
  199. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  200. package/templates/apps/frontend-blank/app.config.ts +1 -1
  201. package/templates/apps/frontend-blank/package.json +7 -7
  202. package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
  203. package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
  204. package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
  205. package/templates/apps/frontend-cms/README.md +47 -0
  206. package/templates/apps/frontend-cms/app.config.ts +37 -0
  207. package/templates/apps/frontend-cms/package.json +33 -0
  208. package/templates/apps/frontend-cms/src/config.ts +8 -0
  209. package/templates/apps/frontend-cms/src/globals.css +105 -0
  210. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  211. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  212. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  213. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  214. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  215. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  216. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  217. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  218. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  219. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  220. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  221. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  222. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  223. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  224. package/templates/apps/frontend-cms/template.json +6 -0
  225. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  226. package/templates/apps/frontend-collab/README.md +80 -0
  227. package/templates/apps/frontend-collab/app.config.ts +38 -0
  228. package/templates/apps/frontend-collab/package.json +33 -0
  229. package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
  230. package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
  231. package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
  232. package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
  233. package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
  234. package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
  235. package/templates/apps/frontend-collab/template.json +6 -0
  236. package/templates/apps/frontend-collab/tsconfig.json +11 -0
  237. package/templates/apps/frontend-contact/package.json +7 -7
  238. package/templates/apps/frontend-dashboard/app.config.ts +1 -1
  239. package/templates/apps/frontend-dashboard/package.json +7 -7
  240. package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
  241. package/templates/apps/frontend-docs/package.json +7 -7
  242. package/templates/apps/frontend-i18n/package.json +6 -6
  243. package/templates/apps/frontend-landing/package.json +7 -7
  244. package/templates/apps/frontend-portal/README.md +71 -0
  245. package/templates/apps/frontend-portal/app.config.ts +37 -0
  246. package/templates/apps/frontend-portal/package.json +32 -0
  247. package/templates/apps/frontend-portal/src/config.ts +8 -0
  248. package/templates/apps/frontend-portal/src/globals.css +93 -0
  249. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  250. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  251. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  252. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  253. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  254. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  255. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  256. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  257. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  258. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  259. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  260. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  261. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  262. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  263. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  264. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  265. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  266. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  267. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  268. package/templates/apps/frontend-portal/template.json +6 -0
  269. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  270. package/templates/apps/frontend-saas/README.md +73 -0
  271. package/templates/apps/frontend-saas/app.config.ts +46 -0
  272. package/templates/apps/frontend-saas/package.json +32 -0
  273. package/templates/apps/frontend-saas/src/config.ts +8 -0
  274. package/templates/apps/frontend-saas/src/globals.css +85 -0
  275. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  276. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  277. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  278. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  279. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  280. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  281. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  282. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  283. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  284. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  285. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  286. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  287. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  288. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  289. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  290. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  291. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  292. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  293. package/templates/apps/frontend-saas/template.json +6 -0
  294. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  295. package/templates/apps/frontend-spa/app.config.ts +1 -1
  296. package/templates/apps/frontend-spa/package.json +7 -7
  297. package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
  298. package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
  299. package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
  300. package/templates/apps/frontend-ssr/app.config.ts +1 -1
  301. package/templates/apps/frontend-ssr/package.json +7 -7
  302. package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
  303. package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
  304. package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
  305. package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
  306. package/templates/apps/frontend-ssr-api/package.json +8 -8
  307. package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
  308. package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
  309. package/templates/apps/frontend-static-blog/package.json +6 -6
  310. package/templates/apps/frontend-status/README.md +51 -0
  311. package/templates/apps/frontend-status/app.config.ts +39 -0
  312. package/templates/apps/frontend-status/package.json +32 -0
  313. package/templates/apps/frontend-status/src/config.ts +7 -0
  314. package/templates/apps/frontend-status/src/globals.css +70 -0
  315. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  316. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  317. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  318. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  319. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  320. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  321. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  322. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  323. package/templates/apps/frontend-status/template.json +6 -0
  324. package/templates/apps/frontend-status/tsconfig.json +11 -0
  325. package/templates/apps/mobile-app/README.md +58 -0
  326. package/templates/apps/mobile-app/app.config.ts +33 -0
  327. package/templates/apps/mobile-app/babel.config.js +5 -0
  328. package/templates/apps/mobile-app/metro.config.js +20 -0
  329. package/templates/apps/mobile-app/package.json +33 -0
  330. package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
  331. package/templates/apps/mobile-app/src/app/index.tsx +62 -0
  332. package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
  333. package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
  334. package/templates/apps/mobile-app/src/client.ts +74 -0
  335. package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
  336. package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
  337. package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
  338. package/templates/apps/mobile-app/src/persistence.ts +38 -0
  339. package/templates/apps/mobile-app/template.json +6 -0
  340. package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
  341. package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
  342. package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
  343. package/templates/apps/mobile-app/tsconfig.json +10 -0
  344. package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
  345. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  346. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  347. package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
  348. package/dist/apiBuild-DgBS9ayv.js +0 -2
  349. package/dist/dbCommand-DvguqlzF.js +0 -2
  350. package/dist/fileConventions-3bffWssN.js +0 -30
  351. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  352. package/dist/inspect-gt8bq-Tz.js +0 -2
  353. package/dist/manifestBuild-ifczArzr.js +0 -2
  354. package/dist/serveCommand-ZTn-dPFa.js +0 -1425
@@ -13,20 +13,20 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.28.0",
17
- "@voltro/database": "0.28.0",
18
- "@voltro/plugin-analytics-postgres": "0.28.0",
19
- "@voltro/plugin-billing": "0.28.0",
20
- "@voltro/plugin-multitenancy": "0.28.0",
21
- "@voltro/plugin-notifications": "0.28.0",
22
- "@voltro/plugin-presence": "0.28.0",
23
- "@voltro/protocol": "0.28.0",
24
- "@voltro/runtime": "0.28.0",
25
- "@voltro/sql-postgres": "0.28.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/plugin-analytics-postgres": "0.30.0",
19
+ "@voltro/plugin-billing": "0.30.0",
20
+ "@voltro/plugin-multitenancy": "0.30.0",
21
+ "@voltro/plugin-notifications": "0.30.0",
22
+ "@voltro/plugin-presence": "0.30.0",
23
+ "@voltro/protocol": "0.30.0",
24
+ "@voltro/runtime": "0.30.0",
25
+ "@voltro/sql-postgres": "0.30.0",
26
26
  "effect": "^3.22.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@voltro/testing": "0.28.0",
29
+ "@voltro/testing": "0.30.0",
30
30
  "typescript": "^6.0.3",
31
31
  "vitest": "^4.1.10"
32
32
  }
@@ -0,0 +1,103 @@
1
+ # {{capProjectName}} {{capAppName}} — the full SaaS backend
2
+
3
+ The whole SaaS loop wired end to end in ONE api — **sign up → hit a paywall →
4
+ upgrade → invite a teammate** — composing two shipped plugins that are normally
5
+ wired separately, plus a small tenant-scoped domain. Boots **zero-infra**
6
+ (`store: 'memory'`, `mock` billing provider, in-process users).
7
+
8
+ Pair it with the **frontend-saas** web template for the dashboard UI:
9
+
10
+ ```bash
11
+ voltro init my-saas --api=api-saas-starter --web=frontend-saas
12
+ ```
13
+
14
+ ## What it composes
15
+
16
+ - **Auth** ([`@voltro/plugin-auth`](https://voltro.cloud/docs/plugins/auth)) —
17
+ password **sign-up / sign-in / sign-out** over an HttpOnly **session cookie**,
18
+ and `voltroPasswordStrategy` resolving that cookie into a typed `Subject` on
19
+ every rpc/ws call. `session.me` returns it (the frontend's SSR gate reads it).
20
+ - **Billing** ([`@voltro/plugin-billing`](https://voltro.cloud/docs/plugins/overview)) —
21
+ subscriptions + **entitlements** over the in-memory `mock` provider. `plans`
22
+ is the single source of tier→quota truth: `free` caps `projects` at 3 and
23
+ `seats` at 1; `pro` lifts both.
24
+ - **Domain** — tenant-scoped `projects` + `invites`. `projects.create` spends
25
+ the `projects` quota; `invites.create` spends the `seats` quota. Over quota →
26
+ typed **`EntitlementExceeded`** (the paywall signal), the row never written.
27
+
28
+ ## The loop
29
+
30
+ | Step | Call | Result |
31
+ |---|---|---|
32
+ | Sign up | `POST /auth/sign-up` | session cookie; lands in tenant `acme` on the `free` plan |
33
+ | Work | `projects.create` ×3 | ok |
34
+ | **Paywall** | `projects.create` (4th) | typed `EntitlementExceeded { entitlement: 'projects', limit: 3 }` |
35
+ | Upgrade | `billing.startCheckout` → `pro` | `projects` becomes `unlimited`, `seats` → 10 |
36
+ | Invite | `invites.create` | spends a `seats` entitlement |
37
+ | Buy seats | `billing.changeSeats` | raises the seat quantity |
38
+
39
+ `billing.*` (`subscription`, `startCheckout`, `changePlan`, `changeSeats`,
40
+ `invoices`) and `/auth/*` are contributed by the plugins — you don't author them.
41
+
42
+ ## Run it
43
+
44
+ ```bash
45
+ voltro dev .
46
+ # → http://localhost:4000 (rpc + /auth/* HTTP routes)
47
+ ```
48
+
49
+ `voltro dev` mints a unique `VOLTRO_SESSION_SECRET` into a gitignored
50
+ `.env.local` on first boot — no secret ships with the template.
51
+
52
+ ### Try the loop (curl)
53
+
54
+ ```bash
55
+ # Sign up (CSRF-protected — grab a token first, keep the cookie jar)
56
+ curl -s -c jar.txt http://localhost:4000/auth/csrf # → { "csrfToken": "<csrf>" }
57
+ curl -s -b jar.txt -c jar.txt -X POST http://localhost:4000/auth/sign-up \
58
+ -H 'content-type: application/json' -H 'x-csrf-token: <csrf>' \
59
+ -d '{"email":"ada@example.com","password":"hunter2hunter2"}'
60
+
61
+ # Create projects — the free plan allows 3, the 4th trips the paywall
62
+ for i in 1 2 3 4; do
63
+ curl -s -b jar.txt -X POST http://localhost:4000/_voltro/inspect/invoke \
64
+ -H 'content-type: application/json' \
65
+ -d "{\"tag\":\"projects.create\",\"input\":{\"name\":\"Project $i\"}}"
66
+ done
67
+ # → the 4th: { "ok": false, "error": { "_tag": "EntitlementExceeded", "entitlement": "projects", "limit": 3, … } }
68
+ ```
69
+
70
+ ## Files
71
+
72
+ ```
73
+ app.config.ts authRoutesPlugin + voltroPasswordStrategy + billingPlugin (free/pro plans)
74
+ database/schema.ts core actors/tenants + tenant-scoped projects + invites
75
+ actions/me.action.ts(.server) session.me — the resolved Subject (SSR gate reads it)
76
+ queries/
77
+ projects.list.query.* live, tenant-scoped subscription
78
+ invites.list.query.* live, tenant-scoped subscription
79
+ mutations/
80
+ projects.create.* entitlement-gated (projects quota → paywall)
81
+ invites.create.* entitlement-gated (seats quota)
82
+ tests/ descriptor pins + tenant isolation + session.me
83
+ ```
84
+
85
+ ## Going to production
86
+
87
+ | Want… | Do |
88
+ |---|---|
89
+ | Durable accounts | swap `memoryUserStore()` for `postgresUserStore({ sql })` + `store: 'postgres'` |
90
+ | Real Stripe checkout | `billingPlugin({ provider: 'stripe', apiKey, webhookSecret, plans })` with real `priceId`s |
91
+ | Email flows (magic-link, reset) | pass `sendEmail: mailSender(mailService)` from `@voltro/plugin-mail` |
92
+ | Cookie hardening | `cookieSecure: true` (+ `cookieDomain`) — already ON in production here |
93
+
94
+ ## Anti-patterns
95
+
96
+ - **Authoring your own `billing.*` / `/auth/*` procedures.** The plugins own
97
+ those namespaces — boot fails on a collision. Use the plugin routes + services.
98
+ - **Skipping the entitlement gate on a paid action.** A scope says "may you call
99
+ this"; an entitlement says "do you have quota left". `requireEntitlement` is
100
+ one line and fails closed — that's what makes the paywall real.
101
+ - **Reading `VOLTRO_SESSION_SECRET` at config time for a fallback.** The plugin
102
+ reads it per request, after the env gate mints it. A hardcoded fallback is a
103
+ published signing key.
@@ -0,0 +1,18 @@
1
+ // Executor for `session.me`. Reads the Subject the AuthMiddleware resolved (via
2
+ // voltroPasswordStrategy reading the session cookie) and returns it.
3
+ import type { AppContext } from '@voltro/runtime'
4
+
5
+ const execute = async (_input: Record<string, never>, ctx: AppContext) => {
6
+ const subject = ctx.request.subject as {
7
+ readonly type: string
8
+ readonly id: string | null
9
+ readonly tenantId: string | null
10
+ }
11
+ return {
12
+ type: subject.type,
13
+ id: subject.id ?? null,
14
+ tenantId: subject.tenantId ?? null,
15
+ }
16
+ }
17
+
18
+ export default execute
@@ -0,0 +1,20 @@
1
+ // `session.me` — returns the caller's resolved identity. This is the proof the
2
+ // auth loop works end-to-end, and the frontend's SSR auth gate reads it: call
3
+ // it with no session cookie and you get an anonymous Subject; sign in via POST
4
+ // /auth/sign-in (which sets the cookie), call it again, and
5
+ // `voltroPasswordStrategy` has resolved that cookie into a `user` Subject.
6
+ // Browser-safe descriptor — no server imports here.
7
+ import { defineAction } from '@voltro/protocol'
8
+ import { Schema } from 'effect'
9
+
10
+ export const me = defineAction({
11
+ name: 'session.me',
12
+ input: Schema.Struct({}),
13
+ output: Schema.Struct({
14
+ // 'anonymous' | 'user' | 'apiKey' | 'serviceAccount'
15
+ type: Schema.String,
16
+ // null while anonymous; the user id once signed in.
17
+ id: Schema.NullOr(Schema.String),
18
+ tenantId: Schema.NullOr(Schema.String),
19
+ }),
20
+ })
@@ -0,0 +1,87 @@
1
+ // The SaaS backend for the {{projectName}} project — the WHOLE loop in one api.
2
+ // Read by `voltro dev`.
3
+ //
4
+ // It composes two shipped plugins that are normally wired separately, plus a
5
+ // small tenant-scoped domain (projects + invites), so that signup → paywall →
6
+ // app works out of the box:
7
+ //
8
+ // • @voltro/plugin-auth — password sign-up/in/out over an HttpOnly session
9
+ // cookie, and a strategy that resolves that cookie into a typed `Subject`
10
+ // on every rpc/ws call (so handlers read `ctx.request.subject`).
11
+ // • @voltro/plugin-billing — subscriptions + ENTITLEMENTS over a pluggable
12
+ // provider. `plans` is the single source of tier→quota truth. The handlers
13
+ // call `requireEntitlement(...)`, which consumes the tenant's metered
14
+ // quota and fails typed `EntitlementExceeded` when it's exhausted — that
15
+ // typed failure IS the paywall signal the frontend upgrades on.
16
+ //
17
+ // The domain then ties them together: `projects.create` spends a `projects`
18
+ // quota (free = 3), and `invites.create` spends a `seats` quota (free = 1) — so
19
+ // the free plan runs out and upgrading to `pro` (via `billing.startCheckout`)
20
+ // lifts the caps. Boots ZERO-infra (`store: 'memory'`, `mock` billing provider,
21
+ // `memoryUserStore`); the README shows the production swaps.
22
+ import { defineEnv, envVar } from '@voltro/env'
23
+ import { authRoutesPlugin, memoryUserStore, voltroPasswordStrategy } from '@voltro/plugin-auth'
24
+ import { billingPlugin } from '@voltro/plugin-billing'
25
+
26
+ export const env = defineEnv({
27
+ LOG_LEVEL: envVar.enum(['debug', 'info', 'warn', 'error'], { access: 'public', default: 'info' }),
28
+ // HMAC key that signs + verifies the session cookie.
29
+ //
30
+ // `generate` means this project mints its OWN key: `voltro dev` writes a
31
+ // unique value into a gitignored `.env.local` on first boot. No value ships
32
+ // with the template — a shipped placeholder would be a signing key published
33
+ // to everyone who downloads it, and every session in every deployment built
34
+ // from it would be forgeable. Your DEPLOYMENT mints its own the same way (a
35
+ // missing secret is a hard boot failure in `serve`/`build`/`start`).
36
+ VOLTRO_SESSION_SECRET: envVar.secret({
37
+ generate: 'base64url',
38
+ description: 'HMAC key that signs and verifies session cookies.',
39
+ }),
40
+ })
41
+
42
+ // In-process user store — zero infra. `postgresUserStore({ sql })` for durable
43
+ // accounts (it manages its own `_voltro_auth_*` tables, auto-migrated).
44
+ const userStore = memoryUserStore()
45
+
46
+ export default {
47
+ type: 'api' as const,
48
+ name: '{{capProjectName}}{{capAppName}}',
49
+ store: 'memory' as const,
50
+ env,
51
+ plugins: [
52
+ // ── Auth ─────────────────────────────────────────────────────────────
53
+ authRoutesPlugin({
54
+ store: userStore,
55
+ // New sign-ups land in this tenant. Every table below carries `tenant()`,
56
+ // so the runtime AND-merges `tenantId = subject.tenantId` into reads —
57
+ // one tenant never sees another's projects or invites.
58
+ defaultTenantId: 'acme',
59
+ successRedirect: '/',
60
+ // `Secure` cookies are HTTPS-only, so OFF in dev (a localhost http page
61
+ // would silently drop the cookie) and ON in production — this is what
62
+ // makes the sign-in → session → authenticated-call loop work locally.
63
+ cookieSecure: process.env.NODE_ENV === 'production',
64
+ }),
65
+ // ── Billing ──────────────────────────────────────────────────────────
66
+ // `mock` is the in-memory provider (no Stripe key needed) — great for dev.
67
+ // `plans` is the single source of tier→quota truth: `free` caps projects at
68
+ // 3 and seats at 1; `pro` lifts both. Swap `provider: 'stripe'` (+
69
+ // STRIPE_SECRET_KEY) and real `priceId`s for production checkout.
70
+ billingPlugin({
71
+ provider: 'mock',
72
+ plans: {
73
+ free: { entitlements: { projects: 3, seats: 1 } },
74
+ pro: {
75
+ priceId: 'price_demo_pro',
76
+ entitlements: { projects: 'unlimited', seats: 10 },
77
+ },
78
+ },
79
+ }),
80
+ ],
81
+ auth: {
82
+ // Resolves the session cookie the /auth routes mint into the request's
83
+ // Subject. Reads the SAME VOLTRO_SESSION_SECRET the routes sign with, so
84
+ // the signing and verifying sides cannot drift.
85
+ strategies: [voltroPasswordStrategy()],
86
+ },
87
+ }
@@ -0,0 +1,57 @@
1
+ // Schema for the {{projectName}} SaaS backend.
2
+ //
3
+ // Two tenant-scoped domain tables — `projects` and `invites`. The plugins
4
+ // contribute their OWN tables automatically (auth uses the in-process
5
+ // `memoryUserStore`; billing adds `_voltro_billing_*`), so you don't declare
6
+ // those here. `actors` + `tenants` are the framework's core tables the audit /
7
+ // tenant mixins reference.
8
+ import {
9
+ databaseHandle,
10
+ id,
11
+ table,
12
+ text,
13
+ timestamp,
14
+ type InferRow,
15
+ } from '@voltro/database'
16
+ import { tenant } from '@voltro/plugin-multitenancy'
17
+
18
+ // ---------- Core tables (required by the audit / tenant mixins) ----------
19
+
20
+ export const actors = table('actors', {
21
+ id: id(),
22
+ kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
23
+ displayName: text().nullable(),
24
+ createdAt: timestamp().default('now'),
25
+ })
26
+
27
+ export const tenants = table('tenants', {
28
+ id: id(),
29
+ name: text(),
30
+ createdAt: timestamp().default('now'),
31
+ })
32
+
33
+ // ---------- Application tables ----------
34
+
35
+ // Creating a project spends the billing `projects` entitlement (free = 3) —
36
+ // see mutations/projects.create.
37
+ export const projects = table('projects', {
38
+ id: id({ prefix: 'proj' }),
39
+ name: text(),
40
+ })
41
+ // tenant() pulls audit() transitively → tenantId + createdAt / updatedAt /
42
+ // createdBy / updatedBy (auto-stamped by the runtime).
43
+ .with(tenant())
44
+
45
+ // Inviting a teammate spends the billing `seats` entitlement (free = 1) — see
46
+ // mutations/invites.create.
47
+ export const invites = table('invites', {
48
+ id: id({ prefix: 'inv' }),
49
+ email: text(),
50
+ status: text().oneOf(['pending', 'accepted', 'revoked']),
51
+ })
52
+ .with(tenant())
53
+
54
+ export type Project = InferRow<typeof projects>
55
+ export type Invite = InferRow<typeof invites>
56
+
57
+ export const database = databaseHandle({ actors, tenants, projects, invites })
@@ -0,0 +1,26 @@
1
+ // Effect-mode: gate on the `seats` entitlement (free = 1), then write the
2
+ // pending invite. Because the free plan grants a single seat, the FIRST invite
3
+ // after sign-up already trips `EntitlementExceeded` unless the tenant has
4
+ // upgraded / bought seats — which is exactly the seat-management moment the
5
+ // frontend's team page surfaces.
6
+ import { Effect } from 'effect'
7
+ import type { AppContext } from '@voltro/runtime'
8
+ import { EffectStore } from '@voltro/runtime'
9
+ import { requireEntitlement } from '@voltro/plugin-billing'
10
+
11
+ const execute = (input: { email: string }, ctx: AppContext) =>
12
+ Effect.gen(function* () {
13
+ yield* requireEntitlement(ctx, 'seats', 1)
14
+
15
+ const store = yield* EffectStore
16
+ const row = yield* store.insert('invites', { email: input.email, status: 'pending' })
17
+
18
+ return {
19
+ id: row['id'] as string,
20
+ email: row['email'] as string,
21
+ status: row['status'] as string,
22
+ tenantId: row['tenantId'] as string,
23
+ }
24
+ })
25
+
26
+ export default execute
@@ -0,0 +1,18 @@
1
+ // Invite a teammate — spends a `seats` entitlement. Same shape as
2
+ // projects.create: the billing plugin merges `EntitlementExceeded` into the
3
+ // wire-error union, so running out of seats surfaces typed and the frontend
4
+ // shows "buy more seats" (billing.changeSeats) instead of a generic error.
5
+ import { defineMutation } from '@voltro/protocol'
6
+ import { Schema } from 'effect'
7
+
8
+ export const createInvite = defineMutation({
9
+ name: 'invites.create',
10
+ target: { table: 'invites', op: 'insert' },
11
+ input: Schema.Struct({ email: Schema.NonEmptyString }),
12
+ output: Schema.Struct({
13
+ id: Schema.String,
14
+ email: Schema.String,
15
+ status: Schema.String,
16
+ tenantId: Schema.String,
17
+ }),
18
+ })
@@ -0,0 +1,29 @@
1
+ // Effect-mode so it can `yield*` the billing service the framework provides in
2
+ // the per-request stack. `requireEntitlement` consumes the tenant's metered
3
+ // `projects` quota (free = 3) and fails typed `EntitlementExceeded` when it's
4
+ // exhausted — BEFORE the row is written, so an over-quota create never leaves a
5
+ // partial write. Upgrading to `pro` (via `billing.startCheckout`) lifts the cap
6
+ // to `unlimited` and the same handler starts succeeding again.
7
+ import { Effect } from 'effect'
8
+ import type { AppContext } from '@voltro/runtime'
9
+ import { EffectStore } from '@voltro/runtime'
10
+ import { requireEntitlement } from '@voltro/plugin-billing'
11
+
12
+ const execute = (input: { name: string }, ctx: AppContext) =>
13
+ Effect.gen(function* () {
14
+ // 1. Billing gate — fails closed (typed EntitlementExceeded) when the
15
+ // tenant has no `projects` quota left. Runs BEFORE the write.
16
+ yield* requireEntitlement(ctx, 'projects', 1)
17
+
18
+ // 2. Write the row. `tenant()` auto-stamps tenantId + audit columns.
19
+ const store = yield* EffectStore
20
+ const row = yield* store.insert('projects', { name: input.name })
21
+
22
+ return {
23
+ id: row['id'] as string,
24
+ name: row['name'] as string,
25
+ tenantId: row['tenantId'] as string,
26
+ }
27
+ })
28
+
29
+ export default execute
@@ -0,0 +1,18 @@
1
+ // Create a project — the paywall in ONE handler. The billing plugin auto-merges
2
+ // its typed `EntitlementExceeded` (+ `BillingError`) into this procedure's
3
+ // wire-error union, so the client decodes the over-quota failure typed WITHOUT
4
+ // a manual `error:` declaration here. That typed failure is the paywall signal:
5
+ // the frontend catches `_tag === 'EntitlementExceeded'` and shows the upgrade CTA.
6
+ import { defineMutation } from '@voltro/protocol'
7
+ import { Schema } from 'effect'
8
+
9
+ export const createProject = defineMutation({
10
+ name: 'projects.create',
11
+ target: { table: 'projects', op: 'insert' },
12
+ input: Schema.Struct({ name: Schema.NonEmptyString }),
13
+ output: Schema.Struct({
14
+ id: Schema.String,
15
+ name: Schema.String,
16
+ tenantId: Schema.String,
17
+ }),
18
+ })
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@{{projectName}}/{{appName}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
9
+ "migrate": "voltro migrate",
10
+ "test": "voltro test",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@effect/platform": "^0.97.0",
15
+ "@effect/rpc": "^0.76.0",
16
+ "@voltro/cli": "0.30.0",
17
+ "@voltro/database": "0.30.0",
18
+ "@voltro/env": "0.30.0",
19
+ "@voltro/plugin-auth": "0.30.0",
20
+ "@voltro/plugin-billing": "0.30.0",
21
+ "@voltro/plugin-multitenancy": "0.30.0",
22
+ "@voltro/protocol": "0.30.0",
23
+ "@voltro/runtime": "0.30.0",
24
+ "@voltro/sql-postgres": "0.30.0",
25
+ "effect": "^3.22.0"
26
+ },
27
+ "devDependencies": {
28
+ "@voltro/testing": "0.30.0",
29
+ "typescript": "^6.0.3",
30
+ "vitest": "^4.1.10"
31
+ }
32
+ }
@@ -0,0 +1,14 @@
1
+ import type { AppContext } from '@voltro/runtime'
2
+
3
+ const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
4
+ descriptor: {
5
+ table: 'invites' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'createdAt' as const, direction: 'desc' as const }],
8
+ take: 100,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,17 @@
1
+ // Streaming subscription of the tenant's teammate invites. Tenant-scoped like
2
+ // projects.list — read live from the frontend's team page with
3
+ // `useSubscription('app', 'invites.list')`.
4
+ import { defineQuery } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const listInvites = defineQuery({
8
+ name: 'invites.list',
9
+ input: Schema.Struct({}),
10
+ output: Schema.Struct({
11
+ id: Schema.String,
12
+ email: Schema.String,
13
+ status: Schema.String,
14
+ tenantId: Schema.String,
15
+ createdAt: Schema.Date,
16
+ }),
17
+ })
@@ -0,0 +1,14 @@
1
+ import type { AppContext } from '@voltro/runtime'
2
+
3
+ const execute = (_input: Record<string, never>, _ctx: AppContext) => ({
4
+ descriptor: {
5
+ table: 'projects' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'createdAt' as const, direction: 'desc' as const }],
8
+ take: 100,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,18 @@
1
+ // Streaming subscription of the tenant's projects. Every change to `projects`
2
+ // for the caller's tenant lands as a delta — the frontend upgrades its first
3
+ // paint to live with `useSubscription('app', 'projects.list')`. The runtime
4
+ // AND-merges the tenant predicate (the table carries `tenant()`), so this never
5
+ // leaks across tenants — no manual `eq('tenantId', ...)` needed.
6
+ import { defineQuery } from '@voltro/protocol'
7
+ import { Schema } from 'effect'
8
+
9
+ export const listProjects = defineQuery({
10
+ name: 'projects.list',
11
+ input: Schema.Struct({}),
12
+ output: Schema.Struct({
13
+ id: Schema.String,
14
+ name: Schema.String,
15
+ tenantId: Schema.String,
16
+ createdAt: Schema.Date,
17
+ }),
18
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "api-saas-starter",
3
+ "kind": "api",
4
+ "summary": "The whole SaaS loop wired end to end: @voltro/plugin-auth (sign-up/in → session cookie → typed Subject) + @voltro/plugin-billing (free/pro plans, entitlement paywall, seats, checkout) + a tenant-scoped projects + invites backend. projects.create is gated by the billing `projects` quota (4th trips EntitlementExceeded); invites.create consumes a `seats` entitlement. Pair with the frontend-saas web template for signup → paywall → app. Boots zero-infra.",
5
+ "tags": ["api", "saas", "auth", "billing", "entitlements", "paywall", "seats", "invites", "starter"]
6
+ }
@@ -0,0 +1,62 @@
1
+ // Unit tests with `@voltro/testing` — no database, no running server, no
2
+ // billing backend. The create executors run in Effect mode and `yield*` the
3
+ // billing service (`requireEntitlement`), which lives in the per-request plugin
4
+ // layer the unit harness does NOT wire — so we do not fake it. Instead we PIN
5
+ // each descriptor's wire contract (the shape the client codegen + the paywall
6
+ // branch depend on) and prove the `tenant()` scoping that keeps one SaaS
7
+ // tenant's rows invisible to another — the headline isolation guarantee.
8
+ // Run with `voltro test` (vitest).
9
+
10
+ import { describe, it, expect } from 'vitest'
11
+ import { Schema } from 'effect'
12
+ import { makeTestContext, mockStore } from '@voltro/testing'
13
+ import { database } from '../database/schema' // registers projects / invites / actors / tenants
14
+ import { createProject } from '../mutations/projects.create.mutation'
15
+ import { createInvite } from '../mutations/invites.create.mutation'
16
+
17
+ describe('projects.create descriptor', () => {
18
+ it('decodes a valid input and rejects a blank name', () => {
19
+ const decode = Schema.decodeUnknownSync(createProject.input)
20
+ expect(decode({ name: 'Acme App' })).toEqual({ name: 'Acme App' })
21
+ // `name: Schema.NonEmptyString` — an empty string fails the wire decode.
22
+ expect(() => decode({ name: '' })).toThrow()
23
+ })
24
+
25
+ it('encodes the output shape the client expects', () => {
26
+ const encode = Schema.encodeUnknownSync(createProject.output)
27
+ const wire = encode({ id: 'proj_1', name: 'Acme App', tenantId: 'acme' })
28
+ expect(wire).toEqual({ id: 'proj_1', name: 'Acme App', tenantId: 'acme' })
29
+ })
30
+ })
31
+
32
+ describe('invites.create descriptor', () => {
33
+ it('decodes an email and rejects a blank one', () => {
34
+ const decode = Schema.decodeUnknownSync(createInvite.input)
35
+ expect(decode({ email: 'ada@example.com' })).toEqual({ email: 'ada@example.com' })
36
+ expect(() => decode({ email: '' })).toThrow()
37
+ })
38
+
39
+ it('encodes the invite output shape', () => {
40
+ const encode = Schema.encodeUnknownSync(createInvite.output)
41
+ const wire = encode({ id: 'inv_1', email: 'ada@example.com', status: 'pending', tenantId: 'acme' })
42
+ expect(wire).toEqual({ id: 'inv_1', email: 'ada@example.com', status: 'pending', tenantId: 'acme' })
43
+ })
44
+ })
45
+
46
+ describe('tenant isolation (REAL tenant scoping)', () => {
47
+ it('t2 never sees t1 projects', async () => {
48
+ const ctx = makeTestContext({
49
+ subject: { type: 'user', id: 'u1', tenantId: 't1' },
50
+ store: mockStore({ projects: [], invites: [] }),
51
+ })
52
+ // Raw insert through the mixin store — tenant() auto-stamps tenantId=t1.
53
+ const row = await ctx.store.insert('projects', { name: 'secret' })
54
+ expect(row['tenantId']).toBe('t1')
55
+
56
+ const seenByT2 = await ctx.withTenant('t2', (c) => c.store.query(database.projects.descriptor))
57
+ expect(seenByT2).toHaveLength(0)
58
+ // …and t1 still sees its own row.
59
+ const seenByT1 = await ctx.store.query(database.projects.descriptor)
60
+ expect(seenByT1).toHaveLength(1)
61
+ })
62
+ })
@@ -0,0 +1,33 @@
1
+ // `session.me` returns the caller's resolved identity — the proof the auth loop
2
+ // threads the Subject through to handlers, and the value the frontend's SSR gate
3
+ // reads to decide redirect-to-login vs render. `makeTestContext({ subject })`
4
+ // sets `ctx.request.subject` to exactly what the AuthMiddleware would resolve at
5
+ // runtime, so the executor is fully runnable with no cookie, no session store,
6
+ // no running server. Run with `voltro test` (vitest).
7
+
8
+ import { describe, it, expect } from 'vitest'
9
+ import { makeTestContext } from '@voltro/testing'
10
+ import '../database/schema' // registers actors / tenants
11
+ import me from '../actions/me.action.server'
12
+
13
+ describe('session.me', () => {
14
+ it('reflects a signed-in user Subject', async () => {
15
+ const ctx = makeTestContext({
16
+ subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
17
+ })
18
+ const identity = await me({}, ctx)
19
+
20
+ expect(identity.type).toBe('user')
21
+ expect(identity.id).toBe('user_1')
22
+ expect(identity.tenantId).toBe('acme')
23
+ })
24
+
25
+ it('returns an anonymous identity with null id/tenant when no session is present', async () => {
26
+ const ctx = makeTestContext()
27
+ const identity = await me({}, ctx)
28
+
29
+ expect(identity.type).toBe('anonymous')
30
+ expect(identity.id).toBeNull()
31
+ expect(identity.tenantId).toBeNull()
32
+ })
33
+ })
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }