@voltro/cli 0.28.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 (280) hide show
  1. package/CHANGELOG.md +278 -0
  2. package/THIRD-PARTY-NOTICES.md +88 -2
  3. package/dist/{apiBuild-eUM32r1u.js → apiBuild-BESZbTjA.js} +3 -3
  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-CSkrUI1h.js → commands-82BDEktg.js} +6803 -8191
  10. package/dist/dbCommand-CaIVTp-a.js +2 -0
  11. package/dist/{dbCommand-CpYgmSw4.js → dbCommand-bCBbk6tz.js} +2 -2
  12. package/dist/{dev-CEZwJhmb.js → dev-Cg4BKLmi.js} +1801 -1612
  13. package/dist/{dev-DlBWWnJQ.js → dev-DeEJj5T4.js} +1 -1
  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/serveCommand-BXl8mhZD.js +1489 -0
  25. package/dist/serveEntry.js +2 -2
  26. package/dist/{start-BGXIf6zT.js → start-CsCIaW4-.js} +282 -269
  27. package/dist/startEntry.js +2 -2
  28. package/package.json +17 -17
  29. package/templates/AGENTS.md +2 -1
  30. package/templates/agent-docs/_index.md +2 -1
  31. package/templates/agent-docs/_manifest.json +10 -1
  32. package/templates/agent-docs/ai.md +135 -0
  33. package/templates/agent-docs/cli.md +124 -6
  34. package/templates/agent-docs/configuration.md +32 -0
  35. package/templates/agent-docs/data.md +282 -0
  36. package/templates/agent-docs/database/migrations.md +47 -0
  37. package/templates/agent-docs/database/misc.md +57 -0
  38. package/templates/agent-docs/database/schema.md +3 -1
  39. package/templates/agent-docs/internationalization.md +161 -1
  40. package/templates/agent-docs/local-first-mobile.md +414 -0
  41. package/templates/agent-docs/routing.md +93 -2
  42. package/templates/agent-docs/schema-driven-ui.md +12 -0
  43. package/templates/agent-docs/templates/apibackends.md +1 -1
  44. package/templates/agent-docs/whats-new.md +162 -181
  45. package/templates/agent-docs/workflows.md +11 -0
  46. package/templates/apps/api-ai/package.json +7 -7
  47. package/templates/apps/api-auth/package.json +8 -8
  48. package/templates/apps/api-backend/package.json +7 -7
  49. package/templates/apps/api-backend-deactivation/package.json +7 -7
  50. package/templates/apps/api-backend-mail/package.json +8 -8
  51. package/templates/apps/api-backend-mariadb/package.json +9 -9
  52. package/templates/apps/api-backend-sqlite/.env.example +19 -0
  53. package/templates/apps/api-backend-sqlite/README.md +38 -0
  54. package/templates/apps/api-backend-sqlite/app.config.ts +35 -0
  55. package/templates/apps/api-backend-sqlite/database/schema.ts +54 -0
  56. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.server.ts +19 -0
  57. package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +37 -0
  58. package/templates/apps/api-backend-sqlite/package.json +30 -0
  59. package/templates/apps/api-backend-sqlite/queries/notes.query.server.ts +14 -0
  60. package/templates/apps/api-backend-sqlite/queries/notes.query.ts +20 -0
  61. package/templates/apps/api-backend-sqlite/template.json +6 -0
  62. package/templates/apps/api-backend-sqlite/tests/notes.create.test.ts +50 -0
  63. package/templates/apps/api-backend-sqlite/tsconfig.json +5 -0
  64. package/templates/apps/api-backend-storage/package.json +8 -8
  65. package/templates/apps/api-cms/README.md +104 -0
  66. package/templates/apps/api-cms/actions/content.get.action.server.ts +27 -0
  67. package/templates/apps/api-cms/actions/content.get.action.ts +19 -0
  68. package/templates/apps/api-cms/actions/content.types.action.server.ts +26 -0
  69. package/templates/apps/api-cms/actions/content.types.action.ts +40 -0
  70. package/templates/apps/api-cms/actions/me.action.server.ts +18 -0
  71. package/templates/apps/api-cms/actions/me.action.ts +16 -0
  72. package/templates/apps/api-cms/app.config.ts +61 -0
  73. package/templates/apps/api-cms/content/blogPost.contentType.ts +39 -0
  74. package/templates/apps/api-cms/content/index.ts +18 -0
  75. package/templates/apps/api-cms/content/page.contentType.ts +24 -0
  76. package/templates/apps/api-cms/database/schema.ts +64 -0
  77. package/templates/apps/api-cms/mutations/content.publish.mutation.server.ts +19 -0
  78. package/templates/apps/api-cms/mutations/content.publish.mutation.ts +15 -0
  79. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.server.ts +36 -0
  80. package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +32 -0
  81. package/templates/apps/api-cms/mutations/content.unpublish.mutation.server.ts +19 -0
  82. package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +11 -0
  83. package/templates/apps/api-cms/package.json +32 -0
  84. package/templates/apps/api-cms/queries/content.list.query.server.ts +0 -0
  85. package/templates/apps/api-cms/queries/content.list.query.ts +27 -0
  86. package/templates/apps/api-cms/template.json +6 -0
  87. package/templates/apps/api-cms/tests/content.descriptors.test.ts +64 -0
  88. package/templates/apps/api-cms/tests/content.write.test.ts +85 -0
  89. package/templates/apps/api-cms/tsconfig.json +5 -0
  90. package/templates/apps/api-data-advanced/package.json +8 -8
  91. package/templates/apps/api-durable/package.json +8 -8
  92. package/templates/apps/api-feature-flags/package.json +9 -9
  93. package/templates/apps/api-governance/package.json +8 -8
  94. package/templates/apps/api-kv/package.json +8 -8
  95. package/templates/apps/api-moderation/package.json +8 -8
  96. package/templates/apps/api-observability/package.json +8 -8
  97. package/templates/apps/api-ratelimit/package.json +8 -8
  98. package/templates/apps/api-rbac/package.json +8 -8
  99. package/templates/apps/api-rest/package.json +7 -7
  100. package/templates/apps/api-saas/package.json +11 -11
  101. package/templates/apps/api-saas-starter/README.md +103 -0
  102. package/templates/apps/api-saas-starter/actions/me.action.server.ts +18 -0
  103. package/templates/apps/api-saas-starter/actions/me.action.ts +20 -0
  104. package/templates/apps/api-saas-starter/app.config.ts +87 -0
  105. package/templates/apps/api-saas-starter/database/schema.ts +57 -0
  106. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.server.ts +26 -0
  107. package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +18 -0
  108. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.server.ts +29 -0
  109. package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +18 -0
  110. package/templates/apps/api-saas-starter/package.json +32 -0
  111. package/templates/apps/api-saas-starter/queries/invites.list.query.server.ts +14 -0
  112. package/templates/apps/api-saas-starter/queries/invites.list.query.ts +17 -0
  113. package/templates/apps/api-saas-starter/queries/projects.list.query.server.ts +14 -0
  114. package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -0
  115. package/templates/apps/api-saas-starter/template.json +6 -0
  116. package/templates/apps/api-saas-starter/tests/projects.create.test.ts +62 -0
  117. package/templates/apps/api-saas-starter/tests/session.test.ts +33 -0
  118. package/templates/apps/api-saas-starter/tsconfig.json +5 -0
  119. package/templates/apps/api-search/package.json +8 -8
  120. package/templates/apps/api-status/README.md +88 -0
  121. package/templates/apps/api-status/app.config.ts +36 -0
  122. package/templates/apps/api-status/authz.ts +33 -0
  123. package/templates/apps/api-status/database/schema.ts +70 -0
  124. package/templates/apps/api-status/mutations/components.create.mutation.server.ts +19 -0
  125. package/templates/apps/api-status/mutations/components.create.mutation.ts +16 -0
  126. package/templates/apps/api-status/mutations/incidents.create.mutation.server.ts +25 -0
  127. package/templates/apps/api-status/mutations/incidents.create.mutation.ts +22 -0
  128. package/templates/apps/api-status/mutations/incidents.resolve.mutation.server.ts +24 -0
  129. package/templates/apps/api-status/mutations/incidents.resolve.mutation.ts +19 -0
  130. package/templates/apps/api-status/mutations/incidents.update.mutation.server.ts +26 -0
  131. package/templates/apps/api-status/mutations/incidents.update.mutation.ts +21 -0
  132. package/templates/apps/api-status/package.json +30 -0
  133. package/templates/apps/api-status/queries/components.list.query.server.ts +14 -0
  134. package/templates/apps/api-status/queries/components.list.query.ts +13 -0
  135. package/templates/apps/api-status/queries/incidents.live.query.server.ts +14 -0
  136. package/templates/apps/api-status/queries/incidents.live.query.ts +20 -0
  137. package/templates/apps/api-status/queries/updates.list.query.server.ts +14 -0
  138. package/templates/apps/api-status/queries/updates.list.query.ts +17 -0
  139. package/templates/apps/api-status/template.json +6 -0
  140. package/templates/apps/api-status/tests/status.test.ts +70 -0
  141. package/templates/apps/api-status/tsconfig.json +5 -0
  142. package/templates/apps/api-versioning/package.json +8 -8
  143. package/templates/apps/api-webhooks/package.json +9 -9
  144. package/templates/apps/changelog/package.json +6 -6
  145. package/templates/apps/edge-functions/package.json +2 -2
  146. package/templates/apps/frontend-admin/package.json +8 -8
  147. package/templates/apps/frontend-app/package.json +8 -8
  148. package/templates/apps/frontend-auth/README.md +78 -0
  149. package/templates/apps/frontend-auth/app.config.ts +34 -0
  150. package/templates/apps/frontend-auth/package.json +32 -0
  151. package/templates/apps/frontend-auth/src/components/AuthShell.tsx +35 -0
  152. package/templates/apps/frontend-auth/src/components/PasswordStrength.tsx +33 -0
  153. package/templates/apps/frontend-auth/src/config.ts +11 -0
  154. package/templates/apps/frontend-auth/src/globals.css +105 -0
  155. package/templates/apps/frontend-auth/src/globals.d.ts +6 -0
  156. package/templates/apps/frontend-auth/src/lib/auth.ts +34 -0
  157. package/templates/apps/frontend-auth/src/lib/redirect.test.ts +24 -0
  158. package/templates/apps/frontend-auth/src/lib/redirect.ts +29 -0
  159. package/templates/apps/frontend-auth/src/locales/de.ts +66 -0
  160. package/templates/apps/frontend-auth/src/locales/en.ts +76 -0
  161. package/templates/apps/frontend-auth/src/locales/index.ts +14 -0
  162. package/templates/apps/frontend-auth/src/pages/forgot/page.tsx +51 -0
  163. package/templates/apps/frontend-auth/src/pages/layout.tsx +12 -0
  164. package/templates/apps/frontend-auth/src/pages/login/page.test.tsx +53 -0
  165. package/templates/apps/frontend-auth/src/pages/login/page.tsx +67 -0
  166. package/templates/apps/frontend-auth/src/pages/logout/page.tsx +28 -0
  167. package/templates/apps/frontend-auth/src/pages/magic/page.tsx +51 -0
  168. package/templates/apps/frontend-auth/src/pages/page.tsx +26 -0
  169. package/templates/apps/frontend-auth/src/pages/reset/page.test.tsx +51 -0
  170. package/templates/apps/frontend-auth/src/pages/reset/page.tsx +63 -0
  171. package/templates/apps/frontend-auth/src/pages/signup/page.tsx +60 -0
  172. package/templates/apps/frontend-auth/src/pages/verify/page.tsx +50 -0
  173. package/templates/apps/frontend-auth/template.json +6 -0
  174. package/templates/apps/frontend-auth/tsconfig.json +5 -0
  175. package/templates/apps/frontend-blank/package.json +7 -7
  176. package/templates/apps/frontend-cms/README.md +47 -0
  177. package/templates/apps/frontend-cms/app.config.ts +37 -0
  178. package/templates/apps/frontend-cms/package.json +33 -0
  179. package/templates/apps/frontend-cms/src/config.ts +8 -0
  180. package/templates/apps/frontend-cms/src/globals.css +105 -0
  181. package/templates/apps/frontend-cms/src/globals.d.ts +6 -0
  182. package/templates/apps/frontend-cms/src/lib/api.ts +58 -0
  183. package/templates/apps/frontend-cms/src/locales/de.ts +50 -0
  184. package/templates/apps/frontend-cms/src/locales/en.ts +55 -0
  185. package/templates/apps/frontend-cms/src/locales/index.ts +14 -0
  186. package/templates/apps/frontend-cms/src/pages/(app)/error.tsx +18 -0
  187. package/templates/apps/frontend-cms/src/pages/(app)/layout.test.tsx +31 -0
  188. package/templates/apps/frontend-cms/src/pages/(app)/layout.tsx +62 -0
  189. package/templates/apps/frontend-cms/src/pages/(app)/not-found.tsx +14 -0
  190. package/templates/apps/frontend-cms/src/pages/(app)/page.test.tsx +110 -0
  191. package/templates/apps/frontend-cms/src/pages/(app)/page.tsx +159 -0
  192. package/templates/apps/frontend-cms/src/pages/layout.tsx +12 -0
  193. package/templates/apps/frontend-cms/src/pages/login/page.test.tsx +58 -0
  194. package/templates/apps/frontend-cms/src/pages/login/page.tsx +93 -0
  195. package/templates/apps/frontend-cms/template.json +6 -0
  196. package/templates/apps/frontend-cms/tsconfig.json +5 -0
  197. package/templates/apps/frontend-contact/package.json +7 -7
  198. package/templates/apps/frontend-dashboard/package.json +7 -7
  199. package/templates/apps/frontend-docs/package.json +7 -7
  200. package/templates/apps/frontend-i18n/package.json +6 -6
  201. package/templates/apps/frontend-landing/package.json +7 -7
  202. package/templates/apps/frontend-portal/README.md +71 -0
  203. package/templates/apps/frontend-portal/app.config.ts +37 -0
  204. package/templates/apps/frontend-portal/package.json +32 -0
  205. package/templates/apps/frontend-portal/src/config.ts +8 -0
  206. package/templates/apps/frontend-portal/src/globals.css +93 -0
  207. package/templates/apps/frontend-portal/src/globals.d.ts +6 -0
  208. package/templates/apps/frontend-portal/src/lib/api.ts +62 -0
  209. package/templates/apps/frontend-portal/src/locales/de.ts +86 -0
  210. package/templates/apps/frontend-portal/src/locales/en.ts +93 -0
  211. package/templates/apps/frontend-portal/src/locales/index.ts +14 -0
  212. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.test.tsx +71 -0
  213. package/templates/apps/frontend-portal/src/pages/(portal)/api-keys/page.tsx +124 -0
  214. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.test.tsx +86 -0
  215. package/templates/apps/frontend-portal/src/pages/(portal)/billing/page.tsx +97 -0
  216. package/templates/apps/frontend-portal/src/pages/(portal)/error.tsx +19 -0
  217. package/templates/apps/frontend-portal/src/pages/(portal)/invoices/page.tsx +45 -0
  218. package/templates/apps/frontend-portal/src/pages/(portal)/layout.test.tsx +31 -0
  219. package/templates/apps/frontend-portal/src/pages/(portal)/layout.tsx +63 -0
  220. package/templates/apps/frontend-portal/src/pages/(portal)/not-found.tsx +15 -0
  221. package/templates/apps/frontend-portal/src/pages/(portal)/page.tsx +39 -0
  222. package/templates/apps/frontend-portal/src/pages/(portal)/profile/page.tsx +79 -0
  223. package/templates/apps/frontend-portal/src/pages/layout.tsx +12 -0
  224. package/templates/apps/frontend-portal/src/pages/login/page.test.tsx +58 -0
  225. package/templates/apps/frontend-portal/src/pages/login/page.tsx +96 -0
  226. package/templates/apps/frontend-portal/template.json +6 -0
  227. package/templates/apps/frontend-portal/tsconfig.json +5 -0
  228. package/templates/apps/frontend-saas/README.md +73 -0
  229. package/templates/apps/frontend-saas/app.config.ts +46 -0
  230. package/templates/apps/frontend-saas/package.json +32 -0
  231. package/templates/apps/frontend-saas/src/config.ts +8 -0
  232. package/templates/apps/frontend-saas/src/globals.css +85 -0
  233. package/templates/apps/frontend-saas/src/globals.d.ts +6 -0
  234. package/templates/apps/frontend-saas/src/lib/api.ts +47 -0
  235. package/templates/apps/frontend-saas/src/locales/de.ts +72 -0
  236. package/templates/apps/frontend-saas/src/locales/en.ts +81 -0
  237. package/templates/apps/frontend-saas/src/locales/index.ts +14 -0
  238. package/templates/apps/frontend-saas/src/pages/(marketing)/layout.tsx +30 -0
  239. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.test.tsx +58 -0
  240. package/templates/apps/frontend-saas/src/pages/(marketing)/login/page.tsx +96 -0
  241. package/templates/apps/frontend-saas/src/pages/(marketing)/page.tsx +27 -0
  242. package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +72 -0
  243. package/templates/apps/frontend-saas/src/pages/dashboard/error.tsx +20 -0
  244. package/templates/apps/frontend-saas/src/pages/dashboard/layout.test.tsx +32 -0
  245. package/templates/apps/frontend-saas/src/pages/dashboard/layout.tsx +69 -0
  246. package/templates/apps/frontend-saas/src/pages/dashboard/not-found.tsx +17 -0
  247. package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +113 -0
  248. package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +76 -0
  249. package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +73 -0
  250. package/templates/apps/frontend-saas/src/pages/layout.tsx +12 -0
  251. package/templates/apps/frontend-saas/template.json +6 -0
  252. package/templates/apps/frontend-saas/tsconfig.json +5 -0
  253. package/templates/apps/frontend-spa/package.json +7 -7
  254. package/templates/apps/frontend-ssr/package.json +7 -7
  255. package/templates/apps/frontend-ssr-api/package.json +8 -8
  256. package/templates/apps/frontend-static-blog/package.json +6 -6
  257. package/templates/apps/frontend-status/README.md +51 -0
  258. package/templates/apps/frontend-status/app.config.ts +39 -0
  259. package/templates/apps/frontend-status/package.json +32 -0
  260. package/templates/apps/frontend-status/src/config.ts +7 -0
  261. package/templates/apps/frontend-status/src/globals.css +70 -0
  262. package/templates/apps/frontend-status/src/globals.d.ts +6 -0
  263. package/templates/apps/frontend-status/src/lib/status.ts +81 -0
  264. package/templates/apps/frontend-status/src/locales/de.ts +41 -0
  265. package/templates/apps/frontend-status/src/locales/en.ts +45 -0
  266. package/templates/apps/frontend-status/src/locales/index.ts +13 -0
  267. package/templates/apps/frontend-status/src/pages/layout.tsx +27 -0
  268. package/templates/apps/frontend-status/src/pages/page.test.tsx +123 -0
  269. package/templates/apps/frontend-status/src/pages/page.tsx +136 -0
  270. package/templates/apps/frontend-status/template.json +6 -0
  271. package/templates/apps/frontend-status/tsconfig.json +11 -0
  272. package/templates/baselines/compose/docker-compose.prod.yml +15 -0
  273. package/templates/baselines/compose-mariadb/docker-compose.prod.yml +15 -0
  274. package/dist/apiBuild-DgBS9ayv.js +0 -2
  275. package/dist/dbCommand-DvguqlzF.js +0 -2
  276. package/dist/fileConventions-3bffWssN.js +0 -30
  277. package/dist/frameworkTableAssembly-lrjZtk0G.js +0 -2
  278. package/dist/inspect-gt8bq-Tz.js +0 -2
  279. package/dist/manifestBuild-ifczArzr.js +0 -2
  280. package/dist/serveCommand-ZTn-dPFa.js +0 -1425
@@ -0,0 +1,159 @@
1
+ // The editor — URL `/`. Introspects the api's registered content types
2
+ // (`content.types`) and generates a form per type with @voltro/cms's
3
+ // `<ContentForm>`; lists a type's drafts live (`content.list`), saves them
4
+ // (`content.saveDraft`, with per-field validation surfaced inline), and
5
+ // publishes / unpublishes them. One page, no dynamic routes — the selected type
6
+ // and the row being edited are component state.
7
+ import type { ReactNode } from 'react'
8
+ import { useEffect, useMemo, useState, type FormEvent } from 'react'
9
+ import type { PageMeta } from '@voltro/web'
10
+ import { useAction, useMutation, useSubscription } from '@voltro/client'
11
+ import { ContentForm } from '@voltro/cms/web'
12
+ import { T, useT } from '@voltro/i18n'
13
+ import { getCatalog } from '../../locales'
14
+ import {
15
+ toContentFormType,
16
+ type ContentRow,
17
+ type ContentTypeDescriptor,
18
+ type SaveResult,
19
+ } from '../../lib/api'
20
+
21
+ export const renderMode = 'ssr' as const
22
+
23
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
24
+ title: getCatalog(locale)['meta.editor.title'],
25
+ })
26
+
27
+ interface Violation { readonly field: string; readonly rule: string; readonly message: string }
28
+
29
+ export default function Editor(): ReactNode {
30
+ const types = useAction<Record<string, never>, { types: ReadonlyArray<ContentTypeDescriptor> }>(
31
+ 'cms', 'content.types',
32
+ )
33
+ const getOne = useAction<{ type: string; id: string; status: string }, { row: Record<string, unknown> | null }>(
34
+ 'cms', 'content.get',
35
+ )
36
+ const save = useMutation<{ type: string; values: Record<string, unknown> }, SaveResult>('cms', 'content.saveDraft')
37
+ const publish = useMutation<{ type: string; id: string }, { ok: boolean }>('cms', 'content.publish')
38
+ const unpublish = useMutation<{ type: string; id: string }, { ok: boolean }>('cms', 'content.unpublish')
39
+
40
+ const [typeList, setTypeList] = useState<ReadonlyArray<ContentTypeDescriptor>>([])
41
+ const [active, setActive] = useState<string | null>(null)
42
+ const [values, setValues] = useState<Record<string, unknown>>({})
43
+ const [editingId, setEditingId] = useState<string | null>(null)
44
+ const [violations, setViolations] = useState<ReadonlyArray<Violation>>([])
45
+ const [saved, setSaved] = useState(false)
46
+
47
+ // Load the content types once; select the first.
48
+ useEffect(() => {
49
+ void types.run({}).then((r) => {
50
+ setTypeList(r.types)
51
+ setActive((cur) => cur ?? r.types[0]?.name ?? null)
52
+ })
53
+ }, []) // mount-only fetch of the content-type descriptors
54
+
55
+ const activeType = typeList.find((t) => t.name === active) ?? null
56
+
57
+ // Live drafts of the active type. Memoised input so a stable object identity
58
+ // avoids resubscribing every render; an unset/unknown type yields an empty list.
59
+ const listInput = useMemo(() => ({ type: active ?? '—', status: 'draft' as const }), [active])
60
+ const { data: rows } = useSubscription<ReadonlyArray<ContentRow>>('cms', 'content.list', listInput)
61
+ const drafts = rows ?? []
62
+
63
+ const resetForm = (): void => { setValues({}); setEditingId(null); setViolations([]) }
64
+
65
+ const onSelectType = (name: string): void => { setActive(name); resetForm(); setSaved(false) }
66
+
67
+ const onEdit = (id: string): void => {
68
+ if (active === null) return
69
+ void getOne.run({ type: active, id, status: 'draft' }).then((r) => {
70
+ if (r.row) { setValues(r.row); setEditingId(id); setViolations([]); setSaved(false) }
71
+ })
72
+ }
73
+
74
+ const onSubmit = (e: FormEvent): void => {
75
+ e.preventDefault()
76
+ if (active === null) return
77
+ const payload = editingId ? { ...values, id: editingId } : values
78
+ void save.mutate({ type: active, values: payload }).then((res) => {
79
+ if (res.ok) { resetForm(); setSaved(true) } else { setViolations(res.violations); setSaved(false) }
80
+ })
81
+ }
82
+
83
+ const onPublish = (id: string): void => { if (active) void publish.mutate({ type: active, id }) }
84
+ const onUnpublish = (id: string): void => { if (active) void unpublish.mutate({ type: active, id }) }
85
+
86
+ const statusKey = (s: string): 'editor.status.draft' | 'editor.status.published' | 'editor.status.archived' =>
87
+ s === 'published' ? 'editor.status.published' : s === 'archived' ? 'editor.status.archived' : 'editor.status.draft'
88
+
89
+ if (typeList.length === 0) {
90
+ return <section className="cms-main"><p className="empty"><T id="editor.form.loading" /></p></section>
91
+ }
92
+
93
+ return (
94
+ <div className="cms-grid">
95
+ {/* Left: type chooser + live draft list */}
96
+ <section className="cms-list">
97
+ <h2 className="label"><T id="editor.types.heading" /></h2>
98
+ <nav className="cms-types">
99
+ {typeList.map((t) => (
100
+ <button
101
+ key={t.name} type="button"
102
+ className={t.name === active ? 'chip active' : 'chip'}
103
+ onClick={() => onSelectType(t.name)}
104
+ >
105
+ {t.displayName}
106
+ </button>
107
+ ))}
108
+ </nav>
109
+
110
+ <h3 className="label" style={{ marginTop: '1rem' }}>
111
+ <T id="editor.list.heading" values={{ type: activeType?.pluralName ?? '' }} />
112
+ </h3>
113
+ {drafts.length === 0 ? (
114
+ <p className="empty"><T id="editor.list.empty" /></p>
115
+ ) : (
116
+ <ul className="rows">
117
+ {drafts.map((row) => (
118
+ <li key={row.id}>
119
+ <span className="grow">{row.title ?? row.slug ?? row.id}</span>
120
+ <span className="tag"><T id={statusKey(row.status)} /></span>
121
+ <button type="button" className="link" onClick={() => onEdit(row.id)}><T id="editor.row.edit" /></button>
122
+ <button type="button" className="link" onClick={() => onPublish(row.id)}><T id="editor.row.publish" /></button>
123
+ <button type="button" className="link" onClick={() => onUnpublish(row.id)}><T id="editor.row.unpublish" /></button>
124
+ </li>
125
+ ))}
126
+ </ul>
127
+ )}
128
+ </section>
129
+
130
+ {/* Right: the auto-generated form */}
131
+ <section className="cms-editor">
132
+ <h2>
133
+ {editingId
134
+ ? <T id="editor.form.editing" />
135
+ : <T id="editor.form.new" values={{ type: activeType?.displayName ?? '' }} />}
136
+ </h2>
137
+ {activeType ? (
138
+ <form onSubmit={onSubmit}>
139
+ <ContentForm contentType={toContentFormType(activeType)} value={values} onChange={setValues} />
140
+ {violations.length > 0 ? (
141
+ <ul className="violations" role="alert">
142
+ {violations.map((v) => <li key={v.field + v.rule}>{v.field}: {v.message}</li>)}
143
+ </ul>
144
+ ) : null}
145
+ {saved ? <p className="ok"><T id="editor.form.saved" /></p> : null}
146
+ <div className="row" style={{ marginTop: '0.75rem' }}>
147
+ <button type="submit" className="btn" disabled={save.pending}>
148
+ <T id={save.pending ? 'editor.form.saving' : 'editor.form.save'} />
149
+ </button>
150
+ {editingId ? (
151
+ <button type="button" className="btn ghost" onClick={resetForm}><T id="editor.form.cancel" /></button>
152
+ ) : null}
153
+ </div>
154
+ </form>
155
+ ) : null}
156
+ </section>
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,12 @@
1
+ // Root layout — wraps EVERY page (marketing + dashboard). Thin: it loads the
2
+ // stylesheet and lets each AREA layout ((marketing)/layout.tsx,
3
+ // dashboard/layout.tsx) supply its own chrome. Layouts nest, so a dashboard
4
+ // page is wrapped root → dashboard.
5
+ //
6
+ // Don't render <html>/<head>/<body> here — the framework shell owns them.
7
+ import type { ReactNode } from 'react'
8
+ import '../globals.css'
9
+
10
+ export default function RootLayout({ children }: { readonly children: ReactNode }): ReactNode {
11
+ return <>{children}</>
12
+ }
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // The auth form. We assert config + that it renders email/password fields and
4
+ // toggles between sign-in and sign-up. The network submit (CSRF + POST /auth/*)
5
+ // is exercised against a running api by `voltro dev`, not unit-mocked here.
6
+
7
+ import { afterEach, describe, expect, test } from 'vitest'
8
+ import { act, createElement, type ReactNode } from 'react'
9
+ import { createRoot, type Root } from 'react-dom/client'
10
+ import { I18nProvider } from '@voltro/i18n'
11
+ import enCatalog from '../../locales/en'
12
+
13
+ const { default: Login, meta, renderMode } = await import('./page')
14
+
15
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
16
+
17
+ let container: HTMLDivElement
18
+ let root: Root
19
+
20
+ const render = (node: ReactNode): void => {
21
+ container = document.createElement('div')
22
+ document.body.appendChild(container)
23
+ act(() => {
24
+ root = createRoot(container)
25
+ root.render(
26
+ createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
27
+ )
28
+ })
29
+ }
30
+
31
+ afterEach(() => {
32
+ if (root) act(() => root.unmount())
33
+ container?.remove()
34
+ document.body.innerHTML = ''
35
+ })
36
+
37
+ describe('login — config', () => {
38
+ test('is a static page titled Sign in', () => {
39
+ expect(renderMode).toBe('static')
40
+ expect(meta({ locale: 'en' }).title).toBe('Sign in')
41
+ })
42
+ })
43
+
44
+ describe('login — form', () => {
45
+ test('renders email + password inputs and defaults to sign-in', () => {
46
+ render(createElement(Login))
47
+ expect(container.querySelector('input[type="email"]')).not.toBeNull()
48
+ expect(container.querySelector('input[type="password"]')).not.toBeNull()
49
+ expect(container.querySelector('h1')?.textContent).toBe('Sign in to the CMS')
50
+ })
51
+
52
+ test('toggling switches to the create-account heading', () => {
53
+ render(createElement(Login))
54
+ const toggle = container.querySelector('.link')!
55
+ act(() => { toggle.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
56
+ expect(container.querySelector('h1')?.textContent).toBe('Create an editor account')
57
+ })
58
+ })
@@ -0,0 +1,93 @@
1
+ // Sign in / sign up — URL `/login`. A REAL auth form: it POSTs to the api's
2
+ // `/auth/*` routes (contributed by @voltro/plugin-auth on api-cms), which set
3
+ // the HttpOnly session cookie. State-changing auth routes are CSRF-protected, so
4
+ // we fetch a token first. On success we do a FULL navigation so the editor's SSR
5
+ // gate re-runs with the freshly-set cookie.
6
+ import type { ReactNode } from 'react'
7
+ import { useState, type FormEvent } from 'react'
8
+ import type { PageMeta } from '@voltro/web'
9
+ import { T, useT } from '@voltro/i18n'
10
+ import { getCatalog } from '../../locales'
11
+
12
+ export const renderMode = 'static' as const
13
+
14
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
15
+ title: getCatalog(locale)['meta.login.title'],
16
+ })
17
+
18
+ type Mode = 'signIn' | 'signUp'
19
+
20
+ async function submitAuth(mode: Mode, email: string, password: string): Promise<boolean> {
21
+ const csrf = (await fetch('/auth/csrf').then((r) => r.json())) as { csrfToken: string }
22
+ const path = mode === 'signUp' ? '/auth/sign-up' : '/auth/sign-in'
23
+ const res = await fetch(path, {
24
+ method: 'POST',
25
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrf.csrfToken },
26
+ body: JSON.stringify({ email, password }),
27
+ })
28
+ return res.ok
29
+ }
30
+
31
+ export default function Login(): ReactNode {
32
+ const [mode, setMode] = useState<Mode>('signIn')
33
+ const [email, setEmail] = useState('')
34
+ const [password, setPassword] = useState('')
35
+ const [pending, setPending] = useState(false)
36
+ const [failed, setFailed] = useState(false)
37
+ const emailLabel = useT('auth.email')
38
+ const passwordLabel = useT('auth.password')
39
+
40
+ const onSubmit = (e: FormEvent): void => {
41
+ e.preventDefault()
42
+ setPending(true)
43
+ setFailed(false)
44
+ void submitAuth(mode, email, password)
45
+ .then((ok) => {
46
+ if (ok) {
47
+ const from = new URLSearchParams(window.location.search).get('from') ?? '/'
48
+ window.location.assign(from)
49
+ } else {
50
+ setFailed(true)
51
+ setPending(false)
52
+ }
53
+ })
54
+ .catch(() => {
55
+ setFailed(true)
56
+ setPending(false)
57
+ })
58
+ }
59
+
60
+ return (
61
+ <section className="hero">
62
+ <h1><T id={mode === 'signUp' ? 'auth.signUp.heading' : 'auth.signIn.heading'} /></h1>
63
+ <form className="auth-form" onSubmit={onSubmit}>
64
+ <label>
65
+ <span><T id="auth.email" /></span>
66
+ <input
67
+ type="email" required aria-label={emailLabel} value={email}
68
+ onChange={(e) => setEmail(e.target.value)}
69
+ />
70
+ </label>
71
+ <label>
72
+ <span><T id="auth.password" /></span>
73
+ <input
74
+ type="password" required aria-label={passwordLabel} value={password}
75
+ onChange={(e) => setPassword(e.target.value)}
76
+ />
77
+ </label>
78
+ {failed ? <p className="error-text"><T id="auth.error" /></p> : null}
79
+ <button type="submit" className="btn" disabled={pending}>
80
+ <T id={pending ? 'auth.pending' : mode === 'signUp' ? 'auth.signUp.submit' : 'auth.signIn.submit'} />
81
+ </button>
82
+ </form>
83
+ <p style={{ marginTop: '1rem' }}>
84
+ <button
85
+ type="button" className="link"
86
+ onClick={() => { setMode((m) => (m === 'signIn' ? 'signUp' : 'signIn')); setFailed(false) }}
87
+ >
88
+ <T id={mode === 'signIn' ? 'auth.toggle.toSignUp' : 'auth.toggle.toSignIn'} />
89
+ </button>
90
+ </p>
91
+ </section>
92
+ )
93
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "frontend-cms",
3
+ "kind": "web",
4
+ "summary": "The CMS editor UI for the api-cms backend: a sign-in page (POST /auth/*), an SSR auth gate (session.me → redirect anonymous editors to /login), and a content editor that fetches the registered content types (content.types) and auto-generates a form per type with @voltro/cms's <ContentForm> — create/edit drafts (content.saveDraft, validation surfaced inline), live-list them (content.list), and publish/unpublish. Bilingual (en/de).",
5
+ "tags": ["web", "frontend", "cms", "editor", "content", "auth", "ssr", "ContentForm"]
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "include": ["**/*.ts", "**/*.tsx"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
@@ -15,18 +15,18 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@effect/platform": "^0.97.0",
18
- "@voltro/cli": "0.28.0",
19
- "@voltro/client": "0.28.0",
20
- "@voltro/env": "0.28.0",
21
- "@voltro/i18n": "0.28.0",
22
- "@voltro/serverless": "0.28.0",
23
- "@voltro/web": "0.28.0",
18
+ "@voltro/cli": "0.29.0",
19
+ "@voltro/client": "0.29.0",
20
+ "@voltro/env": "0.29.0",
21
+ "@voltro/i18n": "0.29.0",
22
+ "@voltro/serverless": "0.29.0",
23
+ "@voltro/web": "0.29.0",
24
24
  "effect": "^3.22.0",
25
25
  "react": "^19.0.0",
26
26
  "react-dom": "^19.0.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@voltro/devtools": "0.28.0",
29
+ "@voltro/devtools": "0.29.0",
30
30
  "@types/react": "^19.0.0",
31
31
  "@types/react-dom": "^19.0.0",
32
32
  "typescript": "^6.0.3",
@@ -12,17 +12,17 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/client": "0.29.0",
17
+ "@voltro/env": "0.29.0",
18
+ "@voltro/i18n": "0.29.0",
19
+ "@voltro/ui-shadcn": "0.29.0",
20
+ "@voltro/web": "0.29.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.28.0",
25
+ "@voltro/devtools": "0.29.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "typescript": "^6.0.3",
@@ -12,17 +12,17 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/client": "0.29.0",
17
+ "@voltro/env": "0.29.0",
18
+ "@voltro/i18n": "0.29.0",
19
+ "@voltro/ui-shadcn": "0.29.0",
20
+ "@voltro/web": "0.29.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.28.0",
25
+ "@voltro/devtools": "0.29.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "typescript": "^6.0.3",
@@ -12,16 +12,16 @@
12
12
  "test": "voltro test"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/web": "0.28.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/client": "0.29.0",
17
+ "@voltro/env": "0.29.0",
18
+ "@voltro/i18n": "0.29.0",
19
+ "@voltro/web": "0.29.0",
20
20
  "react": "^19.0.0",
21
21
  "react-dom": "^19.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/devtools": "0.28.0",
24
+ "@voltro/devtools": "0.29.0",
25
25
  "@types/react": "^19.0.0",
26
26
  "@types/react-dom": "^19.0.0",
27
27
  "typescript": "^6.0.3",
@@ -12,17 +12,17 @@
12
12
  "typecheck": "tsc --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@voltro/cli": "0.28.0",
16
- "@voltro/client": "0.28.0",
17
- "@voltro/env": "0.28.0",
18
- "@voltro/i18n": "0.28.0",
19
- "@voltro/ui-shadcn": "0.28.0",
20
- "@voltro/web": "0.28.0",
15
+ "@voltro/cli": "0.29.0",
16
+ "@voltro/client": "0.29.0",
17
+ "@voltro/env": "0.29.0",
18
+ "@voltro/i18n": "0.29.0",
19
+ "@voltro/ui-shadcn": "0.29.0",
20
+ "@voltro/web": "0.29.0",
21
21
  "react": "^19.0.0",
22
22
  "react-dom": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/devtools": "0.28.0",
25
+ "@voltro/devtools": "0.29.0",
26
26
  "@types/react": "^19.0.0",
27
27
  "@types/react-dom": "^19.0.0",
28
28
  "typescript": "^6.0.3",
@@ -0,0 +1,71 @@
1
+ # {{capProjectName}} {{capAppName}} — billing & account portal
2
+
3
+ The lean "manage your account" surface customers visit outside the product:
4
+ plan, payment method, invoices, API keys, profile. Consumes the
5
+ **api-saas-starter** backend; billing is plugin-orchestrated, so the template
6
+ never renders an invoice or holds a card. Bilingual (en/de).
7
+
8
+ ```bash
9
+ voltro init my-saas --api=api-saas-starter --web=frontend-portal
10
+ ```
11
+
12
+ ## Pages
13
+
14
+ | URL | Reads / drives | Notes |
15
+ |---|---|---|
16
+ | `/` | `billing.subscription` | overview: plan, status, renewal + quick links |
17
+ | `/billing` | `billing.subscription`, `billing.portalUrl`, `billing.previewChange`, `billing.changePlan` | plan card; card management redirects to the provider portal; a plan change QUOTES the provider's proration before applying |
18
+ | `/invoices` | `billing.invoices` | list; "View" / "PDF" link out to the provider's hosted pages |
19
+ | `/api-keys` | `GET/POST /v1/api-keys*` | the framework's built-in key management (see below) |
20
+ | `/profile` | `session.me`, `/auth/password-reset`, `/auth/sign-out` | identity, self-serve password reset, sign out |
21
+ | `/login` | `POST /auth/*` | sign-in / sign-up |
22
+
23
+ `(portal)/layout.tsx` is the SSR auth gate — it asks `session.me` server-side and
24
+ RedirectErrors an anonymous visitor to `/login`.
25
+
26
+ ## API keys — one config step on the API
27
+
28
+ The API-keys page drives the framework's **built-in** management surface
29
+ (`/v1/api-keys`), which the API exposes only when it sets `apiKeys: true` in its
30
+ `app.config.ts`. **api-saas-starter does not enable it by default**, so out of the
31
+ box this page shows a clear "enable it" notice rather than a broken table. To make
32
+ it live:
33
+
34
+ 1. On the API: `export default { …, apiKeys: true }`.
35
+ 2. Grant the signed-in user an issuing scope (`apikey:issue:self`) or `admin` —
36
+ the management routes are admin-scoped by design (keys are org credentials).
37
+
38
+ The raw key is shown exactly **once** at mint time (hash-stored, unrecoverable).
39
+ The page degrades on `404` (not enabled), `401/403` (no scope), and errors.
40
+
41
+ ## What's intentionally NOT here
42
+
43
+ Per the lean brief: **no** support-ticket system and **no** team/member
44
+ management — those belong in the product's own dashboard or a dedicated tool.
45
+ Seat changes stay with the dashboard (`billing.changeSeats`); this portal is plan,
46
+ payment, invoices, keys, profile.
47
+
48
+ ## Files
49
+
50
+ ```
51
+ app.config.ts web app; apis.app → @{{projectName}}/api (api-saas-starter)
52
+ src/lib/api.ts wire shapes (subscription/invoice/apiKey) + formatMoney
53
+ src/pages/
54
+ login/page.tsx(.test) sign-in / sign-up (POST /auth/*)
55
+ (portal)/layout.tsx(.test) shell + SSR auth gate (session.me)
56
+ (portal)/page.tsx overview
57
+ (portal)/billing/page.tsx(.test) plan + payment method + quoted plan change
58
+ (portal)/invoices/page.tsx invoice history (links out)
59
+ (portal)/api-keys/page.tsx(.test) framework key management (graceful)
60
+ (portal)/profile/page.tsx identity + password reset + sign out
61
+ (portal)/error.tsx · not-found.tsx
62
+ src/locales/{en,de}.ts bilingual catalogs (parity-enforced)
63
+ ```
64
+
65
+ ## Going to production
66
+
67
+ - **Real Stripe** — configure `billingPlugin({ provider: 'stripe', … })` on the
68
+ API; `billing.portalUrl` then opens Stripe's real hosted portal and
69
+ `billing.invoices` returns real `hostedUrl`/`pdfUrl` links.
70
+ - **Email** — wire `sendEmail` on the API so the profile password-reset actually
71
+ delivers.
@@ -0,0 +1,37 @@
1
+ // Web app for the {{projectName}} project — the customer BILLING + ACCOUNT
2
+ // portal over the sibling `api-saas-starter` api. Slim by design: the screens a
3
+ // customer visits to manage their account OUTSIDE the product — plan, payment
4
+ // method, invoices, API keys, profile.
5
+ //
6
+ // - `/login` — static sign-in that POSTs to the api's `/auth/*`.
7
+ // - The authed portal in the SILENT route group `src/pages/(portal)/`. Its
8
+ // `layout.tsx` loader is the auth gate (asks the api's `session.me`,
9
+ // RedirectErrors an anonymous visitor to /login). Its pages are
10
+ // `renderMode: 'ssr'` so the gate runs per request.
11
+ //
12
+ // Billing is plugin-orchestrated: `billing.subscription` / `billing.invoices`
13
+ // are live reads, and card management / plan changes go through the provider via
14
+ // `billing.portalUrl` / `billing.changePlan` — the template never renders an
15
+ // invoice or holds a card itself.
16
+ import { defineEnv, envVar } from '@voltro/env'
17
+
18
+ export const env = defineEnv({
19
+ VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
20
+ })
21
+
22
+ export default {
23
+ type: 'web' as const,
24
+ name: '{{capProjectName}}{{capAppName}}',
25
+ port: {{port}},
26
+ theme: 'system' as const,
27
+ env,
28
+ locales: ['en', 'de'] as const,
29
+ defaultLocale: 'en' as const,
30
+ // The api this portal consumes. Pair with api-saas-starter:
31
+ // `voltro init my-saas --api=api-saas-starter --web=frontend-portal`.
32
+ // The hooks pass 'app' as the lookup key; codegen pulls its typed rpc surface
33
+ // (session.me + the billing plugin's `billing.*`).
34
+ apis: {
35
+ app: { package: '@{{projectName}}/api' },
36
+ },
37
+ }
@@ -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
+ "build": "voltro build .",
10
+ "start": "voltro start .",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "voltro test"
13
+ },
14
+ "dependencies": {
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
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@voltro/devtools": "0.29.0",
27
+ "@types/react": "^19.0.0",
28
+ "@types/react-dom": "^19.0.0",
29
+ "typescript": "^6.0.3",
30
+ "vitest": "^4.1.10"
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ // The brand name shown in the marketing header + dashboard sidebar.
2
+ //
3
+ // A plain constant — not `publicEnv` — so the template typechecks BEFORE
4
+ // `voltro dev` generates the env types. `VOLTRO_PUBLIC_APP_NAME` is still
5
+ // declared in app.config.ts (validated + in the env manifest); to read it typed
6
+ // instead, after one `voltro dev` swap this for
7
+ // `publicEnv.VOLTRO_PUBLIC_APP_NAME` from '@voltro/env/public'.
8
+ export const APP_NAME = '{{capProjectName}}'