@voltro/cli 0.33.0 → 0.35.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.
- package/CHANGELOG.md +1968 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CD8K4XOr.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Ct9xkTrS.js +232 -0
- package/dist/checkCommand-DKpDLlqu.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-D5wsCmA_.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-C4YoQIc2.js +30 -0
- package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-BE8E7zF3.js +816 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-D7SgZGaN.js +25 -0
- package/dist/dataCommand-BhYwDgg-.js +537 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
- package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
- package/dist/dev-D2BikO7a.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-CM4Ch9C7.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
- package/dist/dormancyCommand-QewYug_s.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
- package/dist/envCommand-CCGPRQY1.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-CHsLCtDf.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/generateCommand-DbgcUpGw.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-DwOgK1t6.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/inspectCmd-EHFZ9yYu.js +224 -0
- package/dist/inspectFetch-EMuhTG_9.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-D36xK7Zu.js +260 -0
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-BV7I-ZHZ.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/probeCommand-CZfaaUOZ.js +122 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CH3eUiMw.js +91 -0
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
- package/dist/seedRunner-ZmLSqNe2.js +333 -0
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-BgN62boB.js +3 -0
- package/dist/start-T4VesWiM.js +1087 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-DStmCJPi.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-BlMZhWgO.js +2 -0
- package/dist/updateCommand-x0pI_x-B.js +585 -0
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
- package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
- package/package.json +209 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +23 -6
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +313 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +853 -50
- package/templates/agent-docs/data.md +608 -12
- package/templates/agent-docs/database/migrations.md +223 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +284 -25
- package/templates/agent-docs/introduction.md +88 -17
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +188 -42
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +247 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +32 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +162 -8
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +152 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +27 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +98 -136
- package/templates/agent-docs/workflows.md +231 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/appGraph-CvQCte0z.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -43,6 +43,8 @@ No actions, workflows, agents, or `AGENTS.md` ship — this template is delibera
|
|
|
43
43
|
|
|
44
44
|
Every query / mutation / action is **two files paired by basename**: a browser-safe descriptor (`*.query.ts` / `*.mutation.ts`) holding `defineQuery` / `defineMutation`, and a server-only executor (`*.query.server.ts` / `*.mutation.server.ts`) with a default export. The framework pairs them at boot. The descriptor file must never import `node:*` / database / runtime modules — it's what the web client pulls through codegen.
|
|
45
45
|
|
|
46
|
+
**Every descriptor also declares an access decision** — `guards: [{ scope }]` or `openAccess: '<why>'`, or the app refuses to boot (`security.defaultDeny`). Both procedures here are `openAccess` with the reason in the file, and the reason is a real claim rather than the word "demo": `tenant()` confines every delivery to the request's tenant, but this template configures no auth strategy, so that tenant comes from the caller's own `x-tenant` header — it shapes the result, it does not authorize the caller. A `guards: [{ scope }]` would be *unsatisfiable* here: with no auth strategy and no rbac, every caller resolves to an anonymous Subject holding no scopes, so the guard would deny 100% of traffic — an outage, not security. Add an identity (see [`api-auth`](/docs/templates/api-auth) / [`api-rbac`](/docs/templates/api-rbac)), then swap the line for a `guards:` in the same change. **Your own new procedure files need one of these three from the first save.**
|
|
47
|
+
|
|
46
48
|
### Query — `queries/notes.query.ts` + `.server.ts`
|
|
47
49
|
|
|
48
50
|
```ts
|
|
@@ -52,6 +54,10 @@ import { Schema } from 'effect'
|
|
|
52
54
|
|
|
53
55
|
export const listNotes = defineQuery({
|
|
54
56
|
name: 'notes.list',
|
|
57
|
+
openAccess:
|
|
58
|
+
'lists notes for the request\'s tenant only (`tenant()` scopes every delivery). No auth '
|
|
59
|
+
+ 'strategy ships in this template, so that tenant comes from the caller\'s own `x-tenant` '
|
|
60
|
+
+ 'header — result shaping, not access control. Add a strategy, then a `guards:`.',
|
|
55
61
|
input: Schema.Struct({}),
|
|
56
62
|
output: Schema.Struct({
|
|
57
63
|
id: Schema.String, title: Schema.String, body: Schema.String,
|
|
@@ -86,6 +92,10 @@ import { Schema } from 'effect'
|
|
|
86
92
|
|
|
87
93
|
export const createNote = defineMutation({
|
|
88
94
|
name: 'notes.create',
|
|
95
|
+
openAccess:
|
|
96
|
+
'inserts a note carrying only what the caller sent, into the caller\'s own tenant '
|
|
97
|
+
+ '(`assertOwnTenant` rejects a mismatch). No auth strategy ships in this template, so '
|
|
98
|
+
+ 'there is no identity a scope guard could name.',
|
|
89
99
|
target: { table: 'notes', op: 'insert',
|
|
90
100
|
shape: (input: { tenantId: string; title: string; body: string }) => ({
|
|
91
101
|
title: input.title, body: input.body, done: false,
|
|
@@ -465,8 +475,10 @@ S3_ACCESS_KEY_ID=minioadmin
|
|
|
465
475
|
S3_SECRET_ACCESS_KEY=minioadmin
|
|
466
476
|
S3_FORCE_PATH_STYLE=1
|
|
467
477
|
|
|
468
|
-
# Auth — HMAC key for signed sessions
|
|
469
|
-
|
|
478
|
+
# Auth — the HMAC key for signed sessions is NOT set here. `voltro dev` mints
|
|
479
|
+
# a unique per-project value into a gitignored `.env.local` on first boot
|
|
480
|
+
# (`envVar.secret({ generate: 'base64url' })`). In production the variable
|
|
481
|
+
# must come from your secret store; a missing one is a boot failure on purpose.
|
|
470
482
|
|
|
471
483
|
# Cluster (K8s, >1 replica) — advertise a routable host so a workflow
|
|
472
484
|
# can resume on another pod. Inject POD_IP via the downward API in prod.
|
|
@@ -606,6 +618,10 @@ import { Schema } from 'effect'
|
|
|
606
618
|
|
|
607
619
|
export const placeOrder = defineMutation({
|
|
608
620
|
name: 'orders.place',
|
|
621
|
+
openAccess:
|
|
622
|
+
'inserts an order built only from caller-supplied fields into the caller\'s own tenant '
|
|
623
|
+
+ '(`assertOwnTenant` rejects a mismatch); reads nothing and modifies no existing row. '
|
|
624
|
+
+ 'No auth strategy ships here, so there is no identity a scope guard could name.',
|
|
609
625
|
// Auto-optimistic: every query whose `source: 'orders'` matches gets a
|
|
610
626
|
// placeholder row prepended; the server delta replaces it on commit.
|
|
611
627
|
target: {
|
|
@@ -809,6 +825,11 @@ import { Schema } from 'effect'
|
|
|
809
825
|
|
|
810
826
|
export const approveOrder = defineMutation({
|
|
811
827
|
name: 'orders.approve',
|
|
828
|
+
openAccess:
|
|
829
|
+
'injects the approval signal into the caller\'s own tenant\'s parked fulfilment run — the '
|
|
830
|
+
+ 'executionId is derived from `{ orderId, tenantId }` and `assertOwnTenant` rejects a '
|
|
831
|
+
+ 'mismatch, so no other tenant\'s run is addressable. It still decides whether an order '
|
|
832
|
+
+ 'ships: the first procedure here to grow an `orders:approve` guard once an identity exists.',
|
|
812
833
|
input: Schema.Struct({
|
|
813
834
|
orderId: Schema.String,
|
|
814
835
|
tenantId: Schema.String,
|
|
@@ -1059,7 +1080,7 @@ apps/acme/api/ # dir named by the app, not the template
|
|
|
1059
1080
|
│ └── searchDocs.tool.tsx # retrieval tool — nearestNeighbours over docs
|
|
1060
1081
|
├── actions/
|
|
1061
1082
|
│ ├── summarize.action.ts # descriptor — browser-safe
|
|
1062
|
-
│ └── summarize.action.server.
|
|
1083
|
+
│ └── summarize.action.server.ts # executor — generateObject structured output
|
|
1063
1084
|
└── seeds/
|
|
1064
1085
|
└── docs.seed.ts # boot seed of sample docs (auto-embedded on insert)
|
|
1065
1086
|
```
|
|
@@ -1192,7 +1213,7 @@ export default async (
|
|
|
1192
1213
|
|
|
1193
1214
|
Because `docs` carries `vectorEmbedding()`, the STRING overload of `nearestNeighbours(query, k)` is valid — the runtime embeds `query` before searching. Any failure (no AI key, embed error) is caught and returns `[]`, so a tool hiccup surfaces to the model as "no results" instead of aborting the whole agent turn.
|
|
1194
1215
|
|
|
1195
|
-
## The structured-output action — `actions/summarize.action.ts` + `.server.
|
|
1216
|
+
## The structured-output action — `actions/summarize.action.ts` + `.server.ts`
|
|
1196
1217
|
|
|
1197
1218
|
AI inference is external I/O, so it's an **action**, not a mutation. This one uses `generateObject` to take free-text and produce STRUCTURED output — a typed `{ title, summary, keyPoints, sentiment }` object instead of prose. The `output` Schema is exported from the descriptor so the executor can reuse the SAME schema to constrain the model: one source of truth for both the wire output and the shape the model must satisfy.
|
|
1198
1219
|
|
|
@@ -1214,6 +1235,9 @@ export type SummaryResult = Schema.Schema.Type<typeof SummaryResult>
|
|
|
1214
1235
|
|
|
1215
1236
|
export const summarize = defineAction({
|
|
1216
1237
|
name: 'support.summarize',
|
|
1238
|
+
openAccess:
|
|
1239
|
+
'summarises caller-supplied text; reads no table and no other caller\'s data. '
|
|
1240
|
+
+ 'Metered by the AI provider, so rate-limit and guard it before it is public.',
|
|
1217
1241
|
input: Schema.Struct({
|
|
1218
1242
|
// The raw text to summarise (a support thread, a doc, a transcript).
|
|
1219
1243
|
text: Schema.String,
|
|
@@ -1223,7 +1247,7 @@ export const summarize = defineAction({
|
|
|
1223
1247
|
```
|
|
1224
1248
|
|
|
1225
1249
|
```ts
|
|
1226
|
-
// summarize.action.server.
|
|
1250
|
+
// summarize.action.server.ts — EXECUTOR (server-only)
|
|
1227
1251
|
import { Effect } from 'effect'
|
|
1228
1252
|
import { generateObject } from '@voltro/ai'
|
|
1229
1253
|
import { SummaryResult } from './summarize.action'
|
|
@@ -1538,6 +1562,10 @@ const Book = Schema.Struct({
|
|
|
1538
1562
|
export const authorsWithBooks = defineQuery({
|
|
1539
1563
|
name: 'authors.withBooks',
|
|
1540
1564
|
source: ['authors', 'books'],
|
|
1565
|
+
openAccess:
|
|
1566
|
+
'streams the seeded demo catalogue (authors + their books) for the request\'s tenant; the '
|
|
1567
|
+
+ 'seed holds no personal data. `bio` is `.encrypted()` and still crosses the wire on '
|
|
1568
|
+
+ 'purpose — encryption at rest is not an exposure marker; `.serverOnly()` is.',
|
|
1541
1569
|
input: Schema.Struct({}),
|
|
1542
1570
|
output: Schema.Struct({
|
|
1543
1571
|
id: Schema.String, name: Schema.String,
|
|
@@ -1631,14 +1659,17 @@ export default {
|
|
|
1631
1659
|
|
|
1632
1660
|
**Where the key actually comes from is the load-bearing detail.** `governancePlugin({ fieldEncryption: true })` resolves the AES-256-GCM key through the **Secrets-Resolver** — i.e. the process environment — NOT through a `defineEnv` `default`. A `defineEnv` default would satisfy the boot validation gate while the cipher still failed to resolve a key. That's why the env declaration above deliberately has no `default`, and the template instead ships a `.env` carrying a **DEV-ONLY** placeholder so `voltro dev` boots out of the box:
|
|
1633
1661
|
|
|
1634
|
-
```
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1662
|
+
```ts
|
|
1663
|
+
// The key is OURS to invent, so declare it as minted. `voltro dev` writes a
|
|
1664
|
+
// unique per-project value into a gitignored `.env.local` before the env gate
|
|
1665
|
+
// runs — so the cipher resolves a key that exists nowhere else, and no key
|
|
1666
|
+
// value is ever committed or shipped.
|
|
1667
|
+
VOLTRO_FIELD_ENCRYPTION_KEY: envVar.secret({ generate: 'hex' }),
|
|
1639
1668
|
```
|
|
1640
1669
|
|
|
1641
|
-
> **
|
|
1670
|
+
> **Minting is dev-only.** `serve` / `build` / `start` have no such step: in production a missing key must remain a boot failure, and the value belongs in your deployment's secret store. **Lose the key → lose the data** (GCM fails closed, never silent corruption); **rotating it** makes every existing `.encrypted()` value unreadable.
|
|
1671
|
+
>
|
|
1672
|
+
> Never write a key value into a shipped file — not even an obviously-fake one. A placeholder in a template is a signing key published to everyone who downloads it, and a 64-hex placeholder passes every length check and appears on no blocklist.
|
|
1642
1673
|
|
|
1643
1674
|
See [Field encryption](/docs/plugins/governance) and the [columns reference](/docs/database/columns).
|
|
1644
1675
|
|
|
@@ -1654,6 +1685,10 @@ import { Schema } from 'effect'
|
|
|
1654
1685
|
export const searchBooks = defineQuery({
|
|
1655
1686
|
name: 'books.search',
|
|
1656
1687
|
source: 'books',
|
|
1688
|
+
openAccess:
|
|
1689
|
+
'full-text search over the seeded demo book catalogue, tenant-scoped; the seed holds no '
|
|
1690
|
+
+ 'personal data. The result cache is `scope: \'subject\'`, so an open read still cannot '
|
|
1691
|
+
+ 'serve one caller a snapshot built for another.',
|
|
1657
1692
|
input: Schema.Struct({ q: Schema.String }),
|
|
1658
1693
|
output: Schema.Struct({
|
|
1659
1694
|
id: Schema.String, authorId: Schema.String, title: Schema.String,
|
|
@@ -2182,7 +2217,7 @@ expect(row.tenantId).toBe('acme') // tenant() stamped it — the REAL store, i
|
|
|
2182
2217
|
voltro test
|
|
2183
2218
|
```
|
|
2184
2219
|
|
|
2185
|
-
The package also ships `MockClock` / `
|
|
2220
|
+
The package also ships `MockClock` / `mockAi` / `makeTestApp` / `defineFactory` / `makeWorkflowRunner` and `runDialectParity` (the `@voltro/testing/dialect` subpath). Mail is asserted through the mail plugin's own memory provider, not a mock — see [Unit testing → plugin services](/docs/testing/unit-testing#plugin-services-mail-storage-your-own).
|
|
2186
2221
|
|
|
2187
2222
|
## Going to production
|
|
2188
2223
|
|
|
@@ -2403,6 +2438,8 @@ curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json
|
|
|
2403
2438
|
|
|
2404
2439
|
The plugin synthesizes the `search.query` rpc — input `{ index, q, limit?, filters? }`, output the matching docs. Because the index spec set `tenantField: 'tenantId'`, the query auto-filters to the caller's tenant: a request resolved to tenant `acme` only ever sees `acme` docs, even though every tenant's docs share one index. Searching the same term as a different tenant returns nothing.
|
|
2405
2440
|
|
|
2441
|
+
**`search.query` is a SEPARATE surface with its own access decision, made by the plugin.** The boot gate that requires `guards:` / `openAccess:` / `internal: true` reads only the procedures *you* wrote — a plugin's are the plugin author's call and you cannot edit them. So the two files this template ships, `queries/articles.list.query.ts` and `mutations/articles.create.mutation.ts`, each carry their own `openAccess:` with the reason in the file (the rows are the template's own seeded demo articles; no auth strategy ships here, so a scope guard would deny every caller). Anything you add beside them needs one of the three from the first save, or the app will not boot. See [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).
|
|
2442
|
+
|
|
2406
2443
|
## Backfill existing rows
|
|
2407
2444
|
|
|
2408
2445
|
The change tap covers writes AFTER boot. `startup/searchBackfill.startup.tsx` seeds the index from rows already in the table — the demo seed's articles, or every row after a restart or a switch to a new engine. It uses the same `searchBackend` instance, so the docs it writes are exactly what `search.query` reads:
|
|
@@ -2429,15 +2466,28 @@ const { results, run, pending } = useSearch('articles')
|
|
|
2429
2466
|
|
|
2430
2467
|
The `/web` subpath imports nothing server-only — it's just a query over the `search.query` rpc, browser-safe.
|
|
2431
2468
|
|
|
2432
|
-
## Going to production — one line
|
|
2469
|
+
## Going to production — one line, and it is not optional
|
|
2433
2470
|
|
|
2434
|
-
`memoryBackend()` is in-process: great for dev,
|
|
2471
|
+
`memoryBackend()` is in-process: great for dev, and **under
|
|
2472
|
+
`NODE_ENV=production` it refuses to boot** (`SearchBackendNotDurable`). This
|
|
2473
|
+
template ships it because it needs zero infrastructure to run locally — it is
|
|
2474
|
+
not a deployable default, and a deploy that keeps it does not start. Swap it in
|
|
2475
|
+
`lib/search.ts` for a vendor engine; the indexing + query + backfill code is
|
|
2476
|
+
identical:
|
|
2435
2477
|
|
|
2436
2478
|
```ts
|
|
2437
2479
|
import { typesenseBackend } from '@voltro/plugin-search' // or meilisearch / algolia
|
|
2438
2480
|
export const searchBackend = typesenseBackend({ url: process.env.TYPESENSE_URL!, apiKey: process.env.TYPESENSE_KEY! })
|
|
2439
2481
|
```
|
|
2440
2482
|
|
|
2483
|
+
**Single-instance is not the exception it sounds like.** The heap-resident index
|
|
2484
|
+
is empty after every restart and deploy, so a genuinely one-process app searches
|
|
2485
|
+
nothing until something re-seeds it. If that is your deployment and a
|
|
2486
|
+
`*.startup.tsx` really does call `backfillIndex` for every index, declare it —
|
|
2487
|
+
`searchPlugin({ singleProcessMemoryIndex: true, indexes })` — and the plugin
|
|
2488
|
+
will hold you to both halves, warning if a peer replica ever announces itself.
|
|
2489
|
+
Full reasoning: [the memory backend refuses to boot in production](/docs/plugins/search#the-memory-backend-refuses-to-boot-in-production).
|
|
2490
|
+
|
|
2441
2491
|
## When to use api-search vs. the other backends
|
|
2442
2492
|
|
|
2443
2493
|
| You want… | Pick |
|
|
@@ -2981,7 +3031,7 @@ Compose the full lifecycle: `users.with(audit(), softDelete(), deactivation())`.
|
|
|
2981
3031
|
|
|
2982
3032
|
_Data governance in one plugin with @voltro/plugin-governance — AES-256-GCM field encryption for .encrypted() columns (handler sees plaintext, ciphertext at rest), admin-gated GDPR governance.export/erase over declared subjectScopes, a consent ledger, and retention TTL sweeps. Memory store, zero infra._
|
|
2983
3033
|
|
|
2984
|
-
Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **field encryption** (AES-256-GCM for `.encrypted()` columns — handlers see plaintext, the column holds `enc:v1:…` at rest), **GDPR** export/erase (admin-gated, walking declared subject scopes), a **consent ledger**, and **retention** sweeps (delete/anonymise rows past a TTL). The template boots zero-infra
|
|
3034
|
+
Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **field encryption** (AES-256-GCM for `.encrypted()` columns — handlers see plaintext, the column holds `enc:v1:…` at rest), **GDPR** export/erase (admin-gated, walking declared subject scopes), a **consent ledger**, and **retention** sweeps (delete/anonymise rows past a TTL). The template boots zero-infra on a key `voltro dev` mints for your project — no key ships with it. Template id: **`api-governance`**.
|
|
2985
3035
|
|
|
2986
3036
|
## Scaffold
|
|
2987
3037
|
|
|
@@ -2989,7 +3039,7 @@ Four compliance primitives, one plugin. `@voltro/plugin-governance` gives you **
|
|
|
2989
3039
|
voltro create-project acme --api=api-governance
|
|
2990
3040
|
```
|
|
2991
3041
|
|
|
2992
|
-
|
|
3042
|
+
**No encryption key ships with this template** — a key committed to a template is a key published to everyone who downloads it. `VOLTRO_FIELD_ENCRYPTION_KEY` is declared with `generate: 'hex'`, so `voltro dev` mints a unique one into a gitignored `.env.local` on first boot. Your deployment mints its own: `voltro secret generate field-encryption`.
|
|
2993
3043
|
|
|
2994
3044
|
## Field encryption
|
|
2995
3045
|
|
|
@@ -2997,7 +3047,8 @@ The shipped `.env` carries a DEV `VOLTRO_FIELD_ENCRYPTION_KEY`. Generate your ow
|
|
|
2997
3047
|
// database/schema.ts
|
|
2998
3048
|
export const profiles = table('profiles', {
|
|
2999
3049
|
id: id(), name: text(), email: text(),
|
|
3000
|
-
|
|
3050
|
+
// TWO markers, two different questions — say both when you mean both.
|
|
3051
|
+
ssn: text().encrypted().serverOnly(), // encrypted AT REST · and may never leave the server
|
|
3001
3052
|
}).with(audit())
|
|
3002
3053
|
|
|
3003
3054
|
// app.config.ts
|
|
@@ -3006,12 +3057,16 @@ governancePlugin({ fieldEncryption: true /* reads VOLTRO_FIELD_ENCRYPTION_KEY */
|
|
|
3006
3057
|
|
|
3007
3058
|
You pass plaintext; the store middleware encrypts on write and decrypts on read — handlers never touch the ciphertext. Boot **fails loud** if an `.encrypted()` column exists but no cipher is registered.
|
|
3008
3059
|
|
|
3060
|
+
**`.encrypted()` is not an exposure marker**, and reading it as one is a tempting category error: the runtime decrypts for the handler, so an encrypted column reaches a client exactly like any other unless it is *also* `.serverOnly()`. That is why the column above carries both, and why `profiles.get` returns the last four digits it derived server-side — proof the cipher ran, without publishing the value.
|
|
3061
|
+
|
|
3009
3062
|
```bash
|
|
3010
3063
|
ID=$(curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
3011
3064
|
-d '{"tag":"profiles.create","input":{"name":"Ada","email":"ada@acme.com","ssn":"123-45-6789"}}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["result"]["id"])')
|
|
3012
3065
|
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
|
|
3013
3066
|
-d "{\"tag\":\"profiles.get\",\"input\":{\"id\":\"$ID\"}}"
|
|
3014
|
-
# → { ok:true, result:{ …,
|
|
3067
|
+
# → { ok:true, result:{ …, ssnLast4:"6789" } }
|
|
3068
|
+
# The handler read "123-45-6789" as plaintext (the cipher decrypted it) and
|
|
3069
|
+
# published four digits, because the column is also `.serverOnly()`.
|
|
3015
3070
|
```
|
|
3016
3071
|
|
|
3017
3072
|
## GDPR, consent, retention
|
|
@@ -3032,6 +3087,8 @@ governancePlugin({
|
|
|
3032
3087
|
|
|
3033
3088
|
- **Don't encrypt what you filter on.** An `.encrypted()` column is ciphertext on disk — no `WHERE` / `ORDER BY` in SQL. Encrypt fields you read back WHOLE (PII, tokens, notes).
|
|
3034
3089
|
- **The key is everything.** GCM fails closed on a bad key — you get an error, never silent corruption. Back the key up; rotating it means re-encrypting.
|
|
3090
|
+
- **Three orthogonal markers, three questions.** `.encrypted()` = encrypted at rest · `.serverOnly()` = may it leave the server at all · `.sensitive()`/`.safe()` = may it appear in a `voltro data export`. Using one to answer another's question is the mistake.
|
|
3091
|
+
- **Every procedure declares an access decision.** `guards: [{ scope }]` or `openAccess: '<why>'`, or the app refuses to boot (`security.defaultDeny`). Both procedures here are `openAccess` with the reason in the file: the template configures no auth strategy and no rbac, so every caller resolves to an anonymous Subject holding no scopes and a scope guard would deny 100% of traffic — an outage, not security. Add an identity (see [`api-auth`](/docs/templates/api-auth) / [`api-rbac`](/docs/templates/api-rbac)), then the guard.
|
|
3035
3092
|
- **GDPR endpoints are admin-only by design.** Don't remove the `admin:full` gate — an unauthenticated export/erase is a data-exfiltration / griefing hole.
|
|
3036
3093
|
|
|
3037
3094
|
|
|
@@ -3132,6 +3189,10 @@ import { Schema } from 'effect'
|
|
|
3132
3189
|
|
|
3133
3190
|
export const syncPull = defineAction({
|
|
3134
3191
|
name: 'sync.pull',
|
|
3192
|
+
openAccess:
|
|
3193
|
+
'advances the request tenant\'s own sync watermark; every KV key is namespaced by that '
|
|
3194
|
+
+ 'tenant and the rows land in a `tenant()` table. The upstream is a local pure generator '
|
|
3195
|
+
+ '— no network call, no credential.',
|
|
3135
3196
|
input: Schema.Struct({
|
|
3136
3197
|
limit: Schema.optional(Schema.Int.pipe(Schema.greaterThan(0))),
|
|
3137
3198
|
}),
|
|
@@ -3378,6 +3439,10 @@ import { Schema } from 'effect'
|
|
|
3378
3439
|
export const setDocumentBody = defineMutation({
|
|
3379
3440
|
name: 'documents.setBody',
|
|
3380
3441
|
target: { table: 'documents', op: 'update' },
|
|
3442
|
+
openAccess:
|
|
3443
|
+
'merges a caller-supplied CRDT update into a document of the request\'s tenant — a merge, '
|
|
3444
|
+
+ 'not an overwrite, so no concurrent editor\'s text is lost, and an id outside the tenant '
|
|
3445
|
+
+ 'matches no row. Guard who may edit once the app has an identity.',
|
|
3381
3446
|
input: Schema.Struct({
|
|
3382
3447
|
id: Schema.NonEmptyString,
|
|
3383
3448
|
update: Schema.Uint8ArrayFromBase64,
|
|
@@ -3424,6 +3489,10 @@ import { Schema } from 'effect'
|
|
|
3424
3489
|
|
|
3425
3490
|
export const listDocuments = defineQuery({
|
|
3426
3491
|
name: 'documents.list',
|
|
3492
|
+
openAccess:
|
|
3493
|
+
'streams the documents of the request\'s tenant, CRDT body included (`tenant()` scopes '
|
|
3494
|
+
+ 'every delivery). No auth strategy ships here, so the tenant comes from the caller\'s own '
|
|
3495
|
+
+ '`x-tenant` header — add a strategy, then a `guards:`.',
|
|
3427
3496
|
input: Schema.Struct({}),
|
|
3428
3497
|
output: Schema.Struct({
|
|
3429
3498
|
id: Schema.String,
|
|
@@ -1986,37 +1986,53 @@ import { useCapabilityManifest, deriveEntityAdmins } from '@voltro/client'
|
|
|
1986
1986
|
|
|
1987
1987
|
const { manifest } = useCapabilityManifest('app') // one-shot fetch of the capability map
|
|
1988
1988
|
const entities = manifest ? deriveEntityAdmins(manifest) : []
|
|
1989
|
-
// each entity: { table, columns,
|
|
1989
|
+
// each entity: { table, columns, serverOnlyColumns, sensitiveColumns, reactive,
|
|
1990
|
+
// pkColumn?, editable, list, create, update, delete }
|
|
1991
|
+
// each of list/create/update/delete: { tag?, guards? }
|
|
1990
1992
|
```
|
|
1991
1993
|
|
|
1992
|
-
`deriveEntityAdmins` joins each user table to the procedures that actually serve it — the query whose `source` is the table (→ the list `<DataTable>`), the mutations whose `target` is `{table, op}` (→ create/edit/delete)
|
|
1994
|
+
`deriveEntityAdmins` joins each user table to the procedures that actually serve it — the query whose `source` is the table (→ the list `<DataTable>`), the mutations whose `target` is `{table, op}` (→ create/edit/delete) — and carries each one's **declared access**. So the admin binds only to procedures that **exist**, and gates only on permissions the api really declares; it never guesses either by naming convention.
|
|
1993
1995
|
|
|
1994
1996
|
## Per-entity CRUD
|
|
1995
1997
|
|
|
1996
1998
|
```tsx
|
|
1997
1999
|
// src/pages/admin/[entity]/page.tsx (abridged)
|
|
1998
|
-
const
|
|
1999
|
-
{spec.
|
|
2000
|
-
<AutoForm api="app" mutation={spec.
|
|
2000
|
+
const create = useAccessDecision(spec.create.guards) // the procedure's OWN guards
|
|
2001
|
+
{spec.create.tag && create !== 'denied' ? (
|
|
2002
|
+
<AutoForm api="app" mutation={spec.create.tag} submitLabel={`Add ${spec.table}`} />
|
|
2001
2003
|
) : null}
|
|
2002
2004
|
|
|
2003
|
-
{spec.
|
|
2004
|
-
<DataTable api="app" query={spec.
|
|
2005
|
+
{spec.list.tag ? (
|
|
2006
|
+
<DataTable api="app" query={spec.list.tag} rowActions={rowActions} />
|
|
2005
2007
|
) : null}
|
|
2006
2008
|
```
|
|
2007
2009
|
|
|
2008
|
-
The list is a **live subscription** — a create from the form (or anyone, in another tab) appears without a refetch. Each row's actions include a delete (gated by `
|
|
2010
|
+
The list is a **live subscription** — a create from the form (or anyone, in another tab) appears without a refetch. Each row's actions include a delete (keyed on `spec.pkColumn`, gated by `useAccessDecision(spec.delete.guards)`, run via `useMutation(spec.delete.tag)`) and a **provenance** drawer (`useProvenance`) answering "why is this row here?".
|
|
2009
2011
|
|
|
2010
|
-
## Permission gating
|
|
2012
|
+
## Permission gating — from the api's own declaration
|
|
2011
2013
|
|
|
2012
|
-
|
|
2014
|
+
Each action carries `guards`: the procedure's `guards:` / `openAccess:` declaration, straight off the capability map. `useAccessDecision` judges it against the scopes `<PermissionProvider>` supplies, so the UI gate and the server check read the **same data** and cannot drift. The demo seeds `admin:full` (bypass) — **swap it for your session's real scopes**:
|
|
2013
2015
|
|
|
2014
2016
|
```tsx
|
|
2015
2017
|
const { data } = useSubscription<{ scopes: string[] }>('app', 'auth.session')
|
|
2016
2018
|
<PermissionProvider scopes={data?.scopes ?? []}>…</PermissionProvider>
|
|
2017
2019
|
```
|
|
2018
2020
|
|
|
2019
|
-
|
|
2021
|
+
**The decision is three-valued.** `allowed` and `denied` are what they sound like; `unknown` means only the server can answer — a guard carrying a `resource` extractor is checked per **row**, and a browser holding global scopes cannot pre-compute it. The admin **shows** those controls and lets the api reply with a typed `ScopeError`. Hiding them would empty the back-office for exactly the multi-tenant apps whose subjects are minted with no global scopes at all.
|
|
2022
|
+
|
|
2023
|
+
This is still UX gating, not enforcement — the api's own guards remain the real authorization boundary.
|
|
2024
|
+
|
|
2025
|
+
## The three exposure axes
|
|
2026
|
+
|
|
2027
|
+
The columns come from your schema, and the admin honours each marker differently — they are orthogonal, and substituting one for another leaks or hides data:
|
|
2028
|
+
|
|
2029
|
+
| Marker | Admin behaviour |
|
|
2030
|
+
|---|---|
|
|
2031
|
+
| `.serverOnly()` | **Never rendered and never submitted.** It is absent from `spec.columns`; the page names it from `serverOnlyColumns` so the omission is visible rather than silent. The runtime refuses it as mutation input anyway (`assertNoServerOnlyInput`) — this is the same rule one layer earlier. |
|
|
2032
|
+
| `.encrypted()` | **Shown and editable.** At-rest encryption is not wire exposure; your procedures read it decrypted. |
|
|
2033
|
+
| `.sensitive(class)` | **Shown**, and listed in `sensitiveColumns` so a bulk export masks it. |
|
|
2034
|
+
|
|
2035
|
+
Rows are keyed on `spec.pkColumn`, not a hard-coded `id`. A table with no single primary key reports `editable: false` and renders list-only.
|
|
2020
2036
|
|
|
2021
2037
|
## Undo / redo
|
|
2022
2038
|
|
|
@@ -2057,9 +2073,11 @@ Everything under `src/pages/admin/` is yours. Swap the cookie gate (`lib/auth.ts
|
|
|
2057
2073
|
|
|
2058
2074
|
## Anti-patterns
|
|
2059
2075
|
|
|
2060
|
-
- **Shipping the demo `admin:full` scopes to production.** That bypasses every
|
|
2061
|
-
- **Treating
|
|
2062
|
-
- **Assuming a naming convention.** The admin binds to discovered tags
|
|
2076
|
+
- **Shipping the demo `admin:full` scopes to production.** That bypasses every gate. Feed the subject's real scopes to `<PermissionProvider>`.
|
|
2077
|
+
- **Treating the UI decision as authorization.** It hides buttons; the server's guards are the real gate. A hidden action is still callable over rpc by a crafted client.
|
|
2078
|
+
- **Assuming a naming convention — for tags *or* for scopes.** The admin binds to discovered tags and gates on discovered guards. A UI that invents `<table>:create` hides every write from every app that named its scope anything else: a total outage of the affordance that looks like a working permission check, and that a demo seeding `admin:full` can never surface.
|
|
2079
|
+
- **Collapsing `unknown` into `denied`.** That is the same outage by another route — it is the answer for every per-resource guard, which is the whole multi-tenant case.
|
|
2080
|
+
- **Hiding a column because it is `.encrypted()`.** Encryption-at-rest is not wire exposure. `.serverOnly()` is the only one of the three axes that means "do not render this".
|
|
2063
2081
|
|
|
2064
2082
|
|
|
2065
2083
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Templates
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Dozens of dogfooded starter templates ship with the framework — backend shapes, frontend shapes, a serverless function library, and an Expo mobile app. voltro list-templates is the authority; scaffold any of them with one CLI call.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -9,11 +9,16 @@
|
|
|
9
9
|
<!-- source: en/templates/overview.md -->
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
_Dozens of dogfooded starter templates ship with the framework — backend shapes, frontend shapes, a serverless function library, and an Expo mobile app. voltro list-templates is the authority; scaffold any of them with one CLI call._
|
|
13
13
|
|
|
14
|
-
Voltro ships **
|
|
14
|
+
Voltro ships **dozens** of starter templates across the **four kinds** you actually deploy: backends (`api-*`), frontends (`frontend-*` plus `changelog`), a serverless library (`edge-functions`), and an Expo mobile app (`mobile-app`). Each is a small, opinionated, dogfooded reference you scaffold once and then own — no update channel, no lock-in.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
**`voltro list-templates` is the authority, not this page.** It prints the
|
|
17
|
+
templates your installed CLI actually ships; the tables below cover the ones
|
|
18
|
+
worth a paragraph of explanation, and several ids exist that have no entry here.
|
|
19
|
+
If an id appears in the command's output and not below, it exists and works.
|
|
20
|
+
|
|
21
|
+
The CLI loads templates by walking `voltro-templates/apps/<id>/`; the directory name IS the template id you pass to `--api` / `--web` / `--template`.
|
|
17
22
|
|
|
18
23
|
```bash
|
|
19
24
|
# A project with a backend + a blank frontend:
|
|
@@ -27,7 +32,7 @@ Each app gets its own port from the project's `portRange`, wired through `pnpm d
|
|
|
27
32
|
|
|
28
33
|
## Backend templates (`--api=<id>`)
|
|
29
34
|
|
|
30
|
-
The
|
|
35
|
+
The `api-backend*` shapes share the same minimal `notes` domain and differ only in the plugin/store wired on top. The rest are **feature showcases** — each a cohesive domain that exercises one whole slice of the framework end to end.
|
|
31
36
|
|
|
32
37
|
| Template | Best for |
|
|
33
38
|
|---|---|
|
|
@@ -54,11 +59,11 @@ The four `api-backend*` shapes share the same minimal `notes` domain and differ
|
|
|
54
59
|
| [`api-governance`](/docs/templates/api-governance) | **Field encryption + GDPR + consent + retention** — `@voltro/plugin-governance`: AES-256-GCM `.encrypted()` columns (plaintext to handlers, ciphertext at rest), admin-gated `governance.export`/`erase`, a consent ledger, retention sweeps. |
|
|
55
60
|
| [`api-collab`](/docs/templates/api-collab) | **Local-first / CRDT collaborative editing** — a `documents` table with a `crdtText()` `body`; concurrent edits from many clients CONVERGE via an authoritative server-side merge on the write path (no last-write-wins loser), then broadcast over the reactive engine. Zero-infra; pairs with `frontend-collab`. |
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
Every one of these is `kind: 'api'` — and `voltro list-templates` lists more `api-*` ids than the table above covers.
|
|
58
63
|
|
|
59
64
|
## Frontend templates (`--web=<id>`)
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
Web shapes spanning every render mode — `static`, `spa`, `ssr`, `isr`, selective `islands` hydration — plus the reactive fullstack loop (and its local-first / CRDT variant).
|
|
62
67
|
|
|
63
68
|
**Every frontend template is bilingual (en/de) out of the box**, with the i18n strategy matched to its render mode: the hydrated/SSR shells use cookie i18n (`<LocaleSwitcher>`), the static/islands sites use URL-prefix i18n (`/de/…`, one pre-rendered HTML file per locale). `frontend-i18n` remains the dedicated URL-prefix reference. See [URL strategies](/docs/i18n/url-strategies) for why render mode decides.
|
|
64
69
|
|
|
@@ -79,7 +84,7 @@ Fourteen web shapes spanning every render mode — `static`, `spa`, `ssr`, `isr`
|
|
|
79
84
|
| [`frontend-ssr-api`](/docs/templates/ssr-api) | `ssr` · fed by api | **SSR from your backend** — an `ssr` loader calls `query('notes.list', {})` over the sibling api's POST /rpc, so real rows are in the first paint + `<title>`; `useSubscription` upgrades to live. Pairs with `api-backend`. |
|
|
80
85
|
| [`frontend-contact`](/docs/templates/contact) | `static` + serverless | **Static page + a serverless email form** — the headline "static frontend, serverless backend" combo. The page ships to a CDN; the function scales to zero. |
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
Every one of these is `kind: 'web'`, and again there are more `web` ids than rows here. The CLI rejects an api template passed to `--web` (and vice-versa) on a kind mismatch.
|
|
83
88
|
|
|
84
89
|
## Serverless template (`--template=<id>` via `add-app`)
|
|
85
90
|
|