@voltro/cli 0.27.0 → 0.29.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 (284) hide show
  1. package/CHANGELOG.md +513 -0
  2. package/THIRD-PARTY-NOTICES.md +88 -2
  3. package/dist/{apiBuild-D22_EpoR.js → apiBuild-BESZbTjA.js} +4 -4
  4. package/dist/apiBuild-C8d74T6g.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/checkCommand-BPAtyWs8.js +2420 -0
  7. package/dist/checkCommand-DBvZvCh1.js +2 -0
  8. package/dist/cliRuntime-Oh517vCV.js +96 -0
  9. package/dist/{commands-jBX8no1I.js → commands-82BDEktg.js} +6476 -7672
  10. package/dist/dbCommand-CaIVTp-a.js +2 -0
  11. package/dist/{dbCommand-uuNCrFAb.js → dbCommand-bCBbk6tz.js} +3 -3
  12. package/dist/{dev-DcbIJrWg.js → dev-Cg4BKLmi.js} +3034 -2619
  13. package/dist/dev-DeEJj5T4.js +3 -0
  14. package/dist/fileConventions-Cof68_BL.js +33 -0
  15. package/dist/{frameworkTableAssembly-BwHU9Euq.js → frameworkTableAssembly-4Db40V23.js} +2 -2
  16. package/dist/frameworkTableAssembly-BTNirAAk.js +2 -0
  17. package/dist/index.js +1 -1
  18. package/dist/{inspect-CUCCzw2I.js → inspect-BMvQpwqy.js} +211 -190
  19. package/dist/inspect-DHeaMZak.js +2 -0
  20. package/dist/{inspectMetrics-BU90mvJN.js → inspectMetrics-DHYygE4s.js} +999 -961
  21. package/dist/{manifestBuild-BnzAxp2O.js → manifestBuild-C4R1EdHn.js} +1 -1
  22. package/dist/manifestBuild-Ck_90gZy.js +2 -0
  23. package/dist/sdkgen-CW5NbYF0.js +582 -0
  24. package/dist/{seedRunner-D6eu-u5U.js → seedRunner-Bqxgp7HZ.js} +60 -59
  25. package/dist/serveCommand-BXl8mhZD.js +1489 -0
  26. package/dist/serveEntry.js +3 -3
  27. package/dist/{start-BGXIf6zT.js → start-CsCIaW4-.js} +282 -269
  28. package/dist/startEntry.js +2 -2
  29. package/package.json +17 -17
  30. package/templates/AGENTS.md +2 -1
  31. package/templates/agent-docs/_index.md +2 -1
  32. package/templates/agent-docs/_manifest.json +10 -1
  33. package/templates/agent-docs/ai.md +135 -0
  34. package/templates/agent-docs/cli.md +124 -6
  35. package/templates/agent-docs/configuration.md +32 -0
  36. package/templates/agent-docs/data.md +282 -0
  37. package/templates/agent-docs/database/migrations.md +47 -0
  38. package/templates/agent-docs/database/misc.md +57 -0
  39. package/templates/agent-docs/database/schema.md +3 -1
  40. package/templates/agent-docs/deployment.md +56 -0
  41. package/templates/agent-docs/internationalization.md +161 -1
  42. package/templates/agent-docs/local-first-mobile.md +414 -0
  43. package/templates/agent-docs/routing.md +93 -2
  44. package/templates/agent-docs/schema-driven-ui.md +12 -0
  45. package/templates/agent-docs/templates/apibackends.md +1 -1
  46. package/templates/agent-docs/testing.md +42 -0
  47. package/templates/agent-docs/whats-new.md +165 -131
  48. package/templates/agent-docs/workflows.md +11 -0
  49. package/templates/apps/api-ai/package.json +7 -7
  50. package/templates/apps/api-auth/package.json +8 -8
  51. package/templates/apps/api-backend/package.json +7 -7
  52. package/templates/apps/api-backend-deactivation/package.json +7 -7
  53. package/templates/apps/api-backend-mail/package.json +8 -8
  54. package/templates/apps/api-backend-mariadb/package.json +9 -9
  55. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  56. package/templates/apps/api-backend-sqlite/README.md +38 -0
  57. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  58. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  59. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  60. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  61. package/templates/apps/api-backend-sqlite/package.json +30 -0
  62. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  63. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  64. package/templates/apps/api-backend-sqlite/template.json +6 -0
  65. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  66. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  67. package/templates/apps/api-backend-storage/package.json +8 -8
  68. package/templates/apps/api-cms/README.md +104 -0
  69. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  70. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  71. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  72. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  73. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  74. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  75. package/templates/apps/api-cms/app.config.ts +61 -0
  76. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  77. package/templates/apps/api-cms/content/index.ts +18 -0
  78. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  79. package/templates/apps/api-cms/database/schema.ts +64 -0
  80. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  81. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  82. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  83. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  84. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  85. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  86. package/templates/apps/api-cms/package.json +32 -0
  87. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  88. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  89. package/templates/apps/api-cms/template.json +6 -0
  90. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  91. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  92. package/templates/apps/api-cms/tsconfig.json +5 -0
  93. package/templates/apps/api-data-advanced/package.json +8 -8
  94. package/templates/apps/api-durable/package.json +8 -8
  95. package/templates/apps/api-feature-flags/package.json +9 -9
  96. package/templates/apps/api-governance/package.json +8 -8
  97. package/templates/apps/api-kv/package.json +8 -8
  98. package/templates/apps/api-moderation/package.json +8 -8
  99. package/templates/apps/api-observability/package.json +8 -8
  100. package/templates/apps/api-ratelimit/package.json +8 -8
  101. package/templates/apps/api-rbac/package.json +8 -8
  102. package/templates/apps/api-rest/package.json +7 -7
  103. package/templates/apps/api-saas/package.json +11 -11
  104. package/templates/apps/api-saas-starter/README.md +103 -0
  105. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  106. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  107. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  108. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  109. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  110. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  111. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  112. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  113. package/templates/apps/api-saas-starter/package.json +32 -0
  114. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  115. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  116. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  117. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  118. package/templates/apps/api-saas-starter/template.json +6 -0
  119. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  120. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  121. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  122. package/templates/apps/api-search/package.json +8 -8
  123. package/templates/apps/api-status/README.md +88 -0
  124. package/templates/apps/api-status/app.config.ts +36 -0
  125. package/templates/apps/api-status/authz.ts +33 -0
  126. package/templates/apps/api-status/database/schema.ts +70 -0
  127. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  128. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  129. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  130. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  131. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  132. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  133. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  134. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  135. package/templates/apps/api-status/package.json +30 -0
  136. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  137. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  138. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  139. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  140. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  141. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  142. package/templates/apps/api-status/template.json +6 -0
  143. package/templates/apps/api-status/tests/status.test.ts +70 -0
  144. package/templates/apps/api-status/tsconfig.json +5 -0
  145. package/templates/apps/api-versioning/package.json +8 -8
  146. package/templates/apps/api-webhooks/package.json +9 -9
  147. package/templates/apps/changelog/package.json +6 -6
  148. package/templates/apps/edge-functions/package.json +2 -2
  149. package/templates/apps/frontend-admin/package.json +8 -8
  150. package/templates/apps/frontend-app/package.json +8 -8
  151. package/templates/apps/frontend-auth/README.md +78 -0
  152. package/templates/apps/frontend-auth/app.config.ts +34 -0
  153. package/templates/apps/frontend-auth/package.json +32 -0
  154. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  155. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  156. package/templates/apps/frontend-auth/src/config.ts +11 -0
  157. package/templates/apps/frontend-auth/src/globals.css +105 -0
  158. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  159. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  160. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  161. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  162. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  163. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  164. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  165. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  166. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  167. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  168. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  169. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  170. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  171. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  172. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  173. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  174. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  175. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  176. package/templates/apps/frontend-auth/template.json +6 -0
  177. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  178. package/templates/apps/frontend-blank/package.json +7 -7
  179. package/templates/apps/frontend-cms/README.md +47 -0
  180. package/templates/apps/frontend-cms/app.config.ts +37 -0
  181. package/templates/apps/frontend-cms/package.json +33 -0
  182. package/templates/apps/frontend-cms/src/config.ts +8 -0
  183. package/templates/apps/frontend-cms/src/globals.css +105 -0
  184. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  185. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  186. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  187. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  188. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  189. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  190. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  191. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  192. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  193. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  194. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  195. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  196. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  197. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  198. package/templates/apps/frontend-cms/template.json +6 -0
  199. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  200. package/templates/apps/frontend-contact/package.json +7 -7
  201. package/templates/apps/frontend-dashboard/package.json +7 -7
  202. package/templates/apps/frontend-docs/package.json +7 -7
  203. package/templates/apps/frontend-i18n/package.json +6 -6
  204. package/templates/apps/frontend-landing/package.json +7 -7
  205. package/templates/apps/frontend-portal/README.md +71 -0
  206. package/templates/apps/frontend-portal/app.config.ts +37 -0
  207. package/templates/apps/frontend-portal/package.json +32 -0
  208. package/templates/apps/frontend-portal/src/config.ts +8 -0
  209. package/templates/apps/frontend-portal/src/globals.css +93 -0
  210. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  211. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  212. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  213. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  214. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  215. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  216. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  217. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  218. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  219. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  220. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  221. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  222. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  223. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  224. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  225. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  226. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  227. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  228. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  229. package/templates/apps/frontend-portal/template.json +6 -0
  230. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  231. package/templates/apps/frontend-saas/README.md +73 -0
  232. package/templates/apps/frontend-saas/app.config.ts +46 -0
  233. package/templates/apps/frontend-saas/package.json +32 -0
  234. package/templates/apps/frontend-saas/src/config.ts +8 -0
  235. package/templates/apps/frontend-saas/src/globals.css +85 -0
  236. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  237. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  238. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  239. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  240. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  241. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  242. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  243. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  244. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  245. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  246. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  247. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  248. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  249. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  250. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  251. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  252. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  253. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  254. package/templates/apps/frontend-saas/template.json +6 -0
  255. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  256. package/templates/apps/frontend-spa/package.json +7 -7
  257. package/templates/apps/frontend-ssr/package.json +7 -7
  258. package/templates/apps/frontend-ssr-api/package.json +8 -8
  259. package/templates/apps/frontend-static-blog/package.json +6 -6
  260. package/templates/apps/frontend-status/README.md +51 -0
  261. package/templates/apps/frontend-status/app.config.ts +39 -0
  262. package/templates/apps/frontend-status/package.json +32 -0
  263. package/templates/apps/frontend-status/src/config.ts +7 -0
  264. package/templates/apps/frontend-status/src/globals.css +70 -0
  265. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  266. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  267. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  268. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  269. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  270. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  271. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  272. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  273. package/templates/apps/frontend-status/template.json +6 -0
  274. package/templates/apps/frontend-status/tsconfig.json +11 -0
  275. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  276. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  277. package/dist/apiBuild-BrjrVJJh.js +0 -2
  278. package/dist/dbCommand-DrzXimKf.js +0 -2
  279. package/dist/dev-DNkso403.js +0 -3
  280. package/dist/fileConventions-3bffWssN.js +0 -30
  281. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  282. package/dist/inspect-gt8bq-Tz.js +0 -2
  283. package/dist/manifestBuild-ifczArzr.js +0 -2
  284. package/dist/serveCommand-DfkisVWP.js +0 -1310
@@ -0,0 +1,70 @@
1
+ // Schema for the {{projectName}} status page.
2
+ //
3
+ // These tables are PUBLIC and NOT tenant-scoped on purpose: a status page is
4
+ // read by anyone, so the live queries must return rows for an anonymous caller.
5
+ // (Adding `tenant()` here would AND-merge `tenantId = subject.tenantId` and an
6
+ // anonymous visitor — tenantId null — would see nothing.) Write access is
7
+ // controlled by the rbac `status:write` guard on the mutations instead.
8
+ //
9
+ // `actors` + `tenants` are the framework core tables.
10
+ import {
11
+ databaseHandle,
12
+ id,
13
+ table,
14
+ text,
15
+ timestamp,
16
+ type InferRow,
17
+ } from '@voltro/database'
18
+
19
+ export const actors = table('actors', {
20
+ id: id(),
21
+ kind: text().oneOf(['user', 'serviceAccount', 'apiKey', 'system']),
22
+ displayName: text().nullable(),
23
+ createdAt: timestamp().default('now'),
24
+ })
25
+
26
+ export const tenants = table('tenants', {
27
+ id: id(),
28
+ name: text(),
29
+ createdAt: timestamp().default('now'),
30
+ })
31
+
32
+ // A monitored service component (API, Dashboard, Webhooks, …).
33
+ export const components = table('components', {
34
+ id: id({ prefix: 'cmp' }),
35
+ name: text(),
36
+ status: text().oneOf(['operational', 'degraded', 'partial_outage', 'major_outage']),
37
+ })
38
+
39
+ // An incident. `impact` drives the public banner colour; `status` walks the
40
+ // lifecycle investigating → identified → monitoring → resolved.
41
+ export const incidents = table('incidents', {
42
+ id: id({ prefix: 'inc' }),
43
+ title: text(),
44
+ impact: text().oneOf(['minor', 'major', 'critical']),
45
+ status: text().oneOf(['investigating', 'identified', 'monitoring', 'resolved']),
46
+ startedAt: timestamp().default('now'),
47
+ resolvedAt: timestamp().nullable(),
48
+ })
49
+
50
+ // A timeline entry posted against an incident. The public page renders these in
51
+ // order under each incident.
52
+ export const incidentUpdates = table('incident_updates', {
53
+ id: id({ prefix: 'iu' }),
54
+ incidentId: text(),
55
+ body: text(),
56
+ status: text().oneOf(['investigating', 'identified', 'monitoring', 'resolved']),
57
+ createdAt: timestamp().default('now'),
58
+ })
59
+
60
+ export type Component = InferRow<typeof components>
61
+ export type Incident = InferRow<typeof incidents>
62
+ export type IncidentUpdate = InferRow<typeof incidentUpdates>
63
+
64
+ export const database = databaseHandle({
65
+ actors,
66
+ tenants,
67
+ components,
68
+ incidents,
69
+ incidentUpdates,
70
+ })
@@ -0,0 +1,19 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (input: { name: string }, _ctx: AppContext) =>
6
+ Effect.gen(function* () {
7
+ const store = yield* EffectStore
8
+ const row = yield* store.insert('components', {
9
+ name: input.name,
10
+ status: 'operational',
11
+ })
12
+ return {
13
+ id: row['id'] as string,
14
+ name: row['name'] as string,
15
+ status: row['status'] as string,
16
+ }
17
+ })
18
+
19
+ export default execute
@@ -0,0 +1,16 @@
1
+ // Add a monitored component — OPERATOR ONLY. Seed your components once
2
+ // (API, Dashboard, Webhooks, …); the public page lists them with their status.
3
+ import { defineMutation } from '@voltro/protocol'
4
+ import { Schema } from 'effect'
5
+
6
+ export const createComponent = defineMutation({
7
+ name: 'components.create',
8
+ target: { table: 'components', op: 'insert' },
9
+ guards: [{ scope: 'status:write' }],
10
+ input: Schema.Struct({ name: Schema.NonEmptyString }),
11
+ output: Schema.Struct({
12
+ id: Schema.String,
13
+ name: Schema.String,
14
+ status: Schema.String,
15
+ }),
16
+ })
@@ -0,0 +1,25 @@
1
+ // Authz-free here: by the time this runs the framework has already enforced the
2
+ // `status:write` guard from the descriptor. A new incident opens in the
3
+ // `investigating` state; `startedAt` is stamped by the column default.
4
+ import { Effect } from 'effect'
5
+ import type { AppContext } from '@voltro/runtime'
6
+ import { EffectStore } from '@voltro/runtime'
7
+
8
+ const execute = (input: { title: string; impact: string }, _ctx: AppContext) =>
9
+ Effect.gen(function* () {
10
+ const store = yield* EffectStore
11
+ const row = yield* store.insert('incidents', {
12
+ title: input.title,
13
+ impact: input.impact,
14
+ status: 'investigating',
15
+ resolvedAt: null,
16
+ })
17
+ return {
18
+ id: row['id'] as string,
19
+ title: row['title'] as string,
20
+ impact: row['impact'] as string,
21
+ status: row['status'] as string,
22
+ }
23
+ })
24
+
25
+ export default execute
@@ -0,0 +1,22 @@
1
+ // Declare an incident — OPERATOR ONLY. The `guards: [{ scope: 'status:write' }]`
2
+ // is enforced by the framework before the executor runs; `voltro check` sees it
3
+ // statically. Declaring `guards:` merges `ScopeError` into the wire-error union
4
+ // automatically (no `error:` needed).
5
+ import { defineMutation } from '@voltro/protocol'
6
+ import { Schema } from 'effect'
7
+
8
+ export const createIncident = defineMutation({
9
+ name: 'incidents.create',
10
+ target: { table: 'incidents', op: 'insert' },
11
+ guards: [{ scope: 'status:write' }],
12
+ input: Schema.Struct({
13
+ title: Schema.NonEmptyString,
14
+ impact: Schema.Literal('minor', 'major', 'critical'),
15
+ }),
16
+ output: Schema.Struct({
17
+ id: Schema.String,
18
+ title: Schema.String,
19
+ impact: Schema.String,
20
+ status: Schema.String,
21
+ }),
22
+ })
@@ -0,0 +1,24 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (input: { incidentId: string; body: string }, _ctx: AppContext) =>
6
+ Effect.gen(function* () {
7
+ const store = yield* EffectStore
8
+ const row = yield* store.update('incidents', input.incidentId, {
9
+ status: 'resolved',
10
+ resolvedAt: new Date(),
11
+ })
12
+ // A closing timeline entry (default body when the operator left it blank).
13
+ yield* store.insert('incident_updates', {
14
+ incidentId: input.incidentId,
15
+ body: input.body === '' ? 'Resolved.' : input.body,
16
+ status: 'resolved',
17
+ })
18
+ return {
19
+ id: input.incidentId,
20
+ status: (row?.['status'] as string | undefined) ?? 'resolved',
21
+ }
22
+ })
23
+
24
+ export default execute
@@ -0,0 +1,19 @@
1
+ // Resolve an incident — OPERATOR ONLY. Sets `status: 'resolved'` + stamps
2
+ // `resolvedAt`, and appends a final `resolved` timeline entry. The public page's
3
+ // banner goes green and the incident archives into the history.
4
+ import { defineMutation } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const resolveIncident = defineMutation({
8
+ name: 'incidents.resolve',
9
+ target: { table: 'incidents', op: 'update' },
10
+ guards: [{ scope: 'status:write' }],
11
+ input: Schema.Struct({
12
+ incidentId: Schema.NonEmptyString,
13
+ body: Schema.String,
14
+ }),
15
+ output: Schema.Struct({
16
+ id: Schema.String,
17
+ status: Schema.String,
18
+ }),
19
+ })
@@ -0,0 +1,26 @@
1
+ import { Effect } from 'effect'
2
+ import type { AppContext } from '@voltro/runtime'
3
+ import { EffectStore } from '@voltro/runtime'
4
+
5
+ const execute = (
6
+ input: { incidentId: string; body: string; status: string },
7
+ _ctx: AppContext,
8
+ ) =>
9
+ Effect.gen(function* () {
10
+ const store = yield* EffectStore
11
+ // Advance the incident's own status so the banner reflects the latest note.
12
+ yield* store.update('incidents', input.incidentId, { status: input.status })
13
+ // Append the timeline entry.
14
+ const row = yield* store.insert('incident_updates', {
15
+ incidentId: input.incidentId,
16
+ body: input.body,
17
+ status: input.status,
18
+ })
19
+ return {
20
+ id: row['id'] as string,
21
+ incidentId: row['incidentId'] as string,
22
+ status: row['status'] as string,
23
+ }
24
+ })
25
+
26
+ export default execute
@@ -0,0 +1,21 @@
1
+ // Post a timeline update to an open incident — OPERATOR ONLY. Writes an
2
+ // `incident_updates` row AND advances the incident's own `status` so the public
3
+ // banner + timeline move together.
4
+ import { defineMutation } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const updateIncident = defineMutation({
8
+ name: 'incidents.update',
9
+ target: { table: 'incident_updates', op: 'insert' },
10
+ guards: [{ scope: 'status:write' }],
11
+ input: Schema.Struct({
12
+ incidentId: Schema.NonEmptyString,
13
+ body: Schema.NonEmptyString,
14
+ status: Schema.Literal('investigating', 'identified', 'monitoring'),
15
+ }),
16
+ output: Schema.Struct({
17
+ id: Schema.String,
18
+ incidentId: Schema.String,
19
+ status: Schema.String,
20
+ }),
21
+ })
@@ -0,0 +1,30 @@
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.29.0",
17
+ "@voltro/database": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/plugin-rbac": "0.29.0",
20
+ "@voltro/protocol": "0.29.0",
21
+ "@voltro/runtime": "0.29.0",
22
+ "@voltro/sql-postgres": "0.29.0",
23
+ "effect": "^3.22.0"
24
+ },
25
+ "devDependencies": {
26
+ "@voltro/testing": "0.29.0",
27
+ "typescript": "^6.0.3",
28
+ "vitest": "^4.1.10"
29
+ }
30
+ }
@@ -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: 'components' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'name' as const, direction: 'asc' as const }],
8
+ take: 100,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,13 @@
1
+ // Live list of service components + their current status. Public (no guard).
2
+ import { defineQuery } from '@voltro/protocol'
3
+ import { Schema } from 'effect'
4
+
5
+ export const componentsList = defineQuery({
6
+ name: 'components.list',
7
+ input: Schema.Struct({}),
8
+ output: Schema.Struct({
9
+ id: Schema.String,
10
+ name: Schema.String,
11
+ status: Schema.String,
12
+ }),
13
+ })
@@ -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: 'incidents' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'startedAt' 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,20 @@
1
+ // The live incident feed — the heart of the status page. NO `guards:`, so an
2
+ // anonymous visitor's browser can subscribe: `useSubscription('app',
3
+ // 'incidents.live')`. Every insert/update to `incidents` lands as a delta, so a
4
+ // newly-declared incident (or a resolve) shows up on every open status page
5
+ // within seconds — no polling.
6
+ import { defineQuery } from '@voltro/protocol'
7
+ import { Schema } from 'effect'
8
+
9
+ export const incidentsLive = defineQuery({
10
+ name: 'incidents.live',
11
+ input: Schema.Struct({}),
12
+ output: Schema.Struct({
13
+ id: Schema.String,
14
+ title: Schema.String,
15
+ impact: Schema.String,
16
+ status: Schema.String,
17
+ startedAt: Schema.Date,
18
+ resolvedAt: Schema.NullOr(Schema.Date),
19
+ }),
20
+ })
@@ -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: 'incident_updates' as const,
6
+ predicate: undefined,
7
+ order: [{ column: 'createdAt' as const, direction: 'desc' as const }],
8
+ take: 200,
9
+ skip: undefined,
10
+ projection: undefined,
11
+ },
12
+ })
13
+
14
+ export default execute
@@ -0,0 +1,17 @@
1
+ // Live feed of incident timeline entries. Public (no guard). The page joins
2
+ // these to incidents by `incidentId` client-side to render each incident's
3
+ // update history.
4
+ import { defineQuery } from '@voltro/protocol'
5
+ import { Schema } from 'effect'
6
+
7
+ export const updatesList = defineQuery({
8
+ name: 'updates.list',
9
+ input: Schema.Struct({}),
10
+ output: Schema.Struct({
11
+ id: Schema.String,
12
+ incidentId: Schema.String,
13
+ body: Schema.String,
14
+ status: Schema.String,
15
+ createdAt: Schema.Date,
16
+ }),
17
+ })
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "api-status",
3
+ "kind": "api",
4
+ "summary": "The backend for a public status page: ungated live queries (incidents.live, updates.list, components.list) any visitor can subscribe to, plus operator-only mutations (create/update/resolve incidents, manage components) gated by a @voltro/plugin-rbac `status:write` scope. Reads are public so an anonymous status page works; writes need an operator. Pairs with the frontend-status web template. Boots zero-infra.",
5
+ "tags": ["api", "status-page", "incidents", "rbac", "live", "useSubscription", "operator"]
6
+ }
@@ -0,0 +1,70 @@
1
+ // Authorization + descriptor tests, driven through the REAL rbac plugin.
2
+ //
3
+ // `makeTestContext({ plugins: [...] })` + `invoke` composes this app's actual
4
+ // interceptor chain and enforces each descriptor's `guards:` with the same
5
+ // `checkGuardsEffect` the serve pipeline calls — so we exercise the whole path
6
+ // (tenant → role → compiled scope → guard), not a hand-stamped `subject.scopes`.
7
+ // The asymmetry that defines a status page is the thing under test: reads are
8
+ // public, writes are operator-only.
9
+ //
10
+ // Run with `voltro test` (vitest).
11
+
12
+ import { describe, it, expect } from 'vitest'
13
+ import { Schema } from 'effect'
14
+ import { makeTestContext, mockStore, invoke } from '@voltro/testing'
15
+ import { rbacPlugin } from '@voltro/plugin-rbac'
16
+ import type { Subject } from '@voltro/protocol'
17
+
18
+ import { roles, demoRolesForTenant } from '../authz'
19
+ import { database } from '../database/schema' // registers incidents / components / updates / core
20
+ import { createIncident } from '../mutations/incidents.create.mutation'
21
+ import createIncidentHandler from '../mutations/incidents.create.mutation.server'
22
+ import { incidentsLive } from '../queries/incidents.live.query'
23
+ import incidentsLiveHandler from '../queries/incidents.live.query.server'
24
+
25
+ /** The app's real plugin, built from the app's real role map. */
26
+ const plugin = () => rbacPlugin({ roles, resolveRoles: (subject) => demoRolesForTenant(subject.tenantId) })
27
+
28
+ const subjectFor = (tenantId: string | null, id: string | null = 'u_1', type = 'user'): Subject =>
29
+ ({ type, id, tenantId, scopes: [] }) as Subject
30
+
31
+ const ctxFor = (subject: Subject) =>
32
+ makeTestContext({ subject, store: mockStore({ incidents: [], components: [], incident_updates: [] }), plugins: [plugin()] })
33
+
34
+ describe('incidents.create — operator-gated', () => {
35
+ it('an operator (tenant `ops`) may declare an incident', async () => {
36
+ const ctx = ctxFor(subjectFor('ops'))
37
+ const inc = await invoke(createIncident, createIncidentHandler, { title: 'API degraded', impact: 'major' }, ctx)
38
+ expect(inc.title).toBe('API degraded')
39
+ expect(inc.status).toBe('investigating')
40
+ })
41
+
42
+ it('an anonymous visitor is refused with a typed ScopeError, and nothing is written', async () => {
43
+ const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
44
+ await expect(invoke(createIncident, createIncidentHandler, { title: 'nope', impact: 'minor' }, ctx))
45
+ .rejects.toMatchObject({ _tag: 'ScopeError', required: 'status:write' })
46
+ expect(await ctx.store.query(database.incidents.descriptor)).toHaveLength(0)
47
+ })
48
+
49
+ it('a signed-in NON-operator is also refused', async () => {
50
+ const ctx = ctxFor(subjectFor('marketing'))
51
+ await expect(invoke(createIncident, createIncidentHandler, { title: 'nope', impact: 'minor' }, ctx))
52
+ .rejects.toMatchObject({ _tag: 'ScopeError', required: 'status:write' })
53
+ })
54
+ })
55
+
56
+ describe('incidents.live — reads are PUBLIC', () => {
57
+ it('an anonymous visitor may subscribe (no guard on the query)', async () => {
58
+ const ctx = ctxFor(subjectFor(null, null, 'anonymous'))
59
+ await expect(invoke(incidentsLive, incidentsLiveHandler, {}, ctx)).resolves.toBeDefined()
60
+ })
61
+ })
62
+
63
+ describe('incidents.create — descriptor', () => {
64
+ it('accepts a valid impact and rejects an unknown one', () => {
65
+ const decode = Schema.decodeUnknownSync(createIncident.input)
66
+ expect(decode({ title: 'x', impact: 'critical' })).toEqual({ title: 'x', impact: 'critical' })
67
+ expect(() => decode({ title: 'x', impact: 'catastrophic' })).toThrow()
68
+ expect(() => decode({ title: '', impact: 'minor' })).toThrow()
69
+ })
70
+ })
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
@@ -13,17 +13,17 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.27.0",
17
- "@voltro/database": "0.27.0",
18
- "@voltro/env": "0.27.0",
19
- "@voltro/plugin-multitenancy": "0.27.0",
20
- "@voltro/plugin-versioning": "0.27.0",
21
- "@voltro/protocol": "0.27.0",
22
- "@voltro/runtime": "0.27.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/database": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/plugin-multitenancy": "0.29.0",
20
+ "@voltro/plugin-versioning": "0.29.0",
21
+ "@voltro/protocol": "0.29.0",
22
+ "@voltro/runtime": "0.29.0",
23
23
  "effect": "^3.22.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.27.0",
26
+ "@voltro/testing": "0.29.0",
27
27
  "typescript": "^6.0.3",
28
28
  "vitest": "^4.1.10"
29
29
  }
@@ -13,18 +13,18 @@
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
15
  "@effect/rpc": "^0.76.0",
16
- "@voltro/cli": "0.27.0",
17
- "@voltro/database": "0.27.0",
18
- "@voltro/env": "0.27.0",
19
- "@voltro/plugin-multitenancy": "0.27.0",
20
- "@voltro/plugin-webhooks": "0.27.0",
21
- "@voltro/protocol": "0.27.0",
22
- "@voltro/runtime": "0.27.0",
23
- "@voltro/sql-postgres": "0.27.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/database": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/plugin-multitenancy": "0.29.0",
20
+ "@voltro/plugin-webhooks": "0.29.0",
21
+ "@voltro/protocol": "0.29.0",
22
+ "@voltro/runtime": "0.29.0",
23
+ "@voltro/sql-postgres": "0.29.0",
24
24
  "effect": "^3.22.0"
25
25
  },
26
26
  "devDependencies": {
27
- "@voltro/testing": "0.27.0",
27
+ "@voltro/testing": "0.29.0",
28
28
  "typescript": "^6.0.3",
29
29
  "vitest": "^4.1.10"
30
30
  }
@@ -12,12 +12,12 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/changelog": "0.27.0",
16
- "@voltro/cli": "0.27.0",
17
- "@voltro/client": "0.27.0",
18
- "@voltro/env": "0.27.0",
19
- "@voltro/i18n": "0.27.0",
20
- "@voltro/web": "0.27.0",
15
+ "@voltro/changelog": "0.29.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/client": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/i18n": "0.29.0",
20
+ "@voltro/web": "0.29.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@effect/platform": "^0.97.0",
15
- "@voltro/cli": "0.27.0",
16
- "@voltro/serverless": "0.27.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/serverless": "0.29.0",
17
17
  "effect": "^3.22.0"
18
18
  },
19
19
  "devDependencies": {
@@ -12,18 +12,18 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.27.0",
16
- "@voltro/cli": "0.27.0",
17
- "@voltro/client": "0.27.0",
18
- "@voltro/env": "0.27.0",
19
- "@voltro/i18n": "0.27.0",
20
- "@voltro/ui-shadcn": "0.27.0",
21
- "@voltro/web": "0.27.0",
15
+ "@{{projectName}}/api": "0.29.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/client": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/i18n": "0.29.0",
20
+ "@voltro/ui-shadcn": "0.29.0",
21
+ "@voltro/web": "0.29.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.27.0",
26
+ "@voltro/devtools": "0.29.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "typescript": "^6.0.3",
@@ -12,18 +12,18 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@{{projectName}}/api": "0.27.0",
16
- "@voltro/cli": "0.27.0",
17
- "@voltro/client": "0.27.0",
18
- "@voltro/env": "0.27.0",
19
- "@voltro/i18n": "0.27.0",
20
- "@voltro/ui-shadcn": "0.27.0",
21
- "@voltro/web": "0.27.0",
15
+ "@{{projectName}}/api": "0.29.0",
16
+ "@voltro/cli": "0.29.0",
17
+ "@voltro/client": "0.29.0",
18
+ "@voltro/env": "0.29.0",
19
+ "@voltro/i18n": "0.29.0",
20
+ "@voltro/ui-shadcn": "0.29.0",
21
+ "@voltro/web": "0.29.0",
22
22
  "react": "^19.0.0",
23
23
  "react-dom": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/devtools": "0.27.0",
26
+ "@voltro/devtools": "0.29.0",
27
27
  "@types/react": "^19.0.0",
28
28
  "@types/react-dom": "^19.0.0",
29
29
  "typescript": "^6.0.3",