@voltro/cli 0.33.0 → 0.34.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 +1801 -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-BICVGnEC.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
- 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-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.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-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
- package/dist/dev-CoG-ZPx8.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-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.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-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.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/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-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -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-CkEZQrtt.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-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +179 -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 +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -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 +69 -3
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -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 +1 -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 +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +150 -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 +10 -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 +1700 -104
- package/templates/agent-docs/workflows.md +130 -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/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-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
|
@@ -19,7 +19,7 @@ The framework ships some plugins; you write your own; the contract is small enou
|
|
|
19
19
|
|
|
20
20
|
- [The plugin contract](/docs/plugins/contract) — `definePlugin`, lifecycle hooks, rpc interceptors, framework-version compatibility
|
|
21
21
|
- [plugin-audit](/docs/plugins/audit) — mutation audit log + `audit()` mixin
|
|
22
|
-
- [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
|
|
22
|
+
- [plugin-auth](/docs/plugins/auth) — full auth suite: password (rehash-on-verify) + sessions (multi-key rotation + sliding-window) + magic-link/reset + email verification + tenant invitations + user impersonation + passkeys (atomic clone detection, BYO multi-replica challenge store) + CSRF + session revocation + memberships/switch-tenant + TOTP/MFA (sign-in enforcement + recovery codes), mounted by `authRoutesPlugin()` (see also the [Authentication section](/docs/authentication/overview))
|
|
23
23
|
- [plugin-multitenancy](/docs/plugins/multitenancy) — `tenant()` schema mixin + `assertOwnTenant` write-guard + `TenantMismatch`
|
|
24
24
|
- [plugin-soft-delete](/docs/plugins/soft-delete) — `softDelete()` schema mixin (hide on delete, `hardDelete()` bypass)
|
|
25
25
|
- [plugin-rbac](/docs/plugins/rbac) — roles + permissions + the `permission()` guard
|
|
@@ -46,7 +46,8 @@ The framework ships some plugins; you write your own; the contract is small enou
|
|
|
46
46
|
- [plugin-governance](/docs/plugins/governance) — data governance: retention TTL sweep, GDPR export + erasure, consent ledger, field encryption
|
|
47
47
|
- [plugin-openapi](/docs/plugins/openapi) — OpenAPI 3.1 spec + Swagger-UI docs generated from your `defineRestRoute` descriptors and (opt-in) rpc procedures
|
|
48
48
|
- [plugin-versioning](/docs/plugins/versioning) — full row history + time-travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions`); what-changed-to-what on every write
|
|
49
|
-
- [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, cross-instance
|
|
49
|
+
- [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, held in memory; cross-instance with [plugin-broadcast](/docs/plugins/broadcast)
|
|
50
|
+
- [plugin-auth-social](/docs/plugins/auth-social) — first-party Sign in with Google / GitHub / Apple: mandatory PKCE + state, JWKS-verified ID tokens, a deliberate account-linking policy, sessions issued through plugin-auth
|
|
50
51
|
- [plugin-scim](/docs/plugins/scim) — SCIM 2.0 provisioning (Users + Groups at `/scim/v2`) so an enterprise IdP can create/deactivate users
|
|
51
52
|
- [plugin-sso-saml](/docs/plugins/sso-saml) — enterprise SAML 2.0 SSO: SP-initiated login + Single Logout, ACS, metadata (+ IdP-metadata-URL auto cert rotation, encrypted assertions, SP request signing); mints a framework session
|
|
52
53
|
- [API keys](/docs/configuration/api-keys) — **first-class** (not a plugin): `apiKeys: true` enables Bearer-key auth + admin-gated issue/list/revoke
|
|
@@ -60,7 +61,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
60
61
|
| Plugin | Status | What it does |
|
|
61
62
|
|---|---|---|
|
|
62
63
|
| `@voltro/plugin-audit` | ✓ | Mutation audit log + `audit()` mixin |
|
|
63
|
-
| `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
|
|
64
|
+
| `@voltro/plugin-auth` | ✓ | Full auth suite via `authRoutesPlugin()`: password (rehash-on-verify), sessions (multi-key rotation + sliding-window), magic-link + password-reset, email verification (off/soft/strict policy), tenant invitations (addressed, single-use, role chosen by the inviter), user impersonation (marked, time-bounded, escalation-proof), passkeys/WebAuthn (atomic clone detection, BYO multi-replica challenge store), CSRF, session enumeration + revocation, memberships + switch-tenant, TOTP/MFA (sign-in enforcement + recovery codes); `authTables` schemas |
|
|
64
65
|
| `@voltro/plugin-multitenancy` | ✓ | `tenant()` schema mixin (read-scope + write-fill) + `assertOwnTenant` guard + typed `TenantMismatch` |
|
|
65
66
|
| `@voltro/plugin-soft-delete` | ✓ | `softDelete()` schema mixin — `deletedAt` / `deletedBy`; `delete` → UPDATE, `hardDelete()` bypass |
|
|
66
67
|
| `@voltro/plugin-rbac` | ✓ | Roles compile to scopes + the `permission()` handler guard + typed `ScopeError` |
|
|
@@ -71,6 +72,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
71
72
|
| `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
|
|
72
73
|
| `@voltro/plugin-broadcast` | ✓ | Cross-replica reactivity — fans out app-mutation change events to every replica over a pub/sub bus (Redis / NATS). Closes the single-instance gap for every non-postgres dialect. [→ details](/docs/plugins/broadcast) |
|
|
73
74
|
| `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `defineEvent` (durable delivery workflow, HMAC signing, retries, filters). [→ details](/docs/plugins/webhooks) |
|
|
75
|
+
| `@voltro/plugin-auth-social` | ✓ | First-party social login — Sign in with Google / GitHub / Apple with no identity vendor: authorize URL + code exchange + JWKS-verified ID tokens, mandatory PKCE (S256) and `state`, an explicit account-linking policy (`never` by default), Apple's signed-JWT client secret / one-time name / private-relay email all handled; sessions via `issueUserSession`. [→ details](/docs/plugins/auth-social) |
|
|
74
76
|
| `@voltro/plugin-auth-{workos,kinde,clerk,auth0,supabase,oidc}` | ✓ | Six IdP adapters over the shared `jwtBearerStrategy` — JWKS verify + claims→tenant mapping; WorkOS additionally ships hosted-login OAuth primitives (`workosAuthorizationUrl` / `workosAuthenticateWithCode`) for a redirect-based SSO login flow. [→ details](/docs/authentication/external-idp) |
|
|
75
77
|
| `@voltro/plugin-analytics-postgres` | ✓ | First-party lite — events on the main DataStore, cross-dialect (postgres / mysql / mariadb / mssql / sqlite / turso). [→ details](/docs/plugins/analytics#voltroplugin-analytics-postgres) |
|
|
76
78
|
| `@voltro/plugin-duckdb` | ✓ | Embedded DuckDB sidecar — real OLAP performance, no external service. [→ details](/docs/plugins/analytics#voltroplugin-duckdb) |
|
|
@@ -91,7 +93,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
91
93
|
| `@voltro/plugin-governance` | ✓ | Data governance — retention TTL sweep (delete / anonymise), GDPR subject export + erasure (admin-gated routes + `GovernanceService`), consent ledger, field encryption. [→ details](/docs/plugins/governance) |
|
|
92
94
|
| `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
|
|
93
95
|
| `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
|
|
94
|
-
| `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator
|
|
96
|
+
| `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator. Held **in memory**, owner-partitioned — no table is written; cross-instance requires [`@voltro/plugin-broadcast`](/docs/plugins/broadcast), and without a broker each replica sees only its own clients. [→ details](/docs/plugins/presence) |
|
|
95
97
|
| `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
|
|
96
98
|
| `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
|
|
97
99
|
|
|
@@ -126,7 +128,7 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
|
|
|
126
128
|
| `extendSchema` | Contribute tables + custom SQL migrations (tracked in `_voltro_plugin_migrations`). |
|
|
127
129
|
| `services` | Provide an Effect `Layer` whose Tags every handler can `yield*` (e.g. `MailService`, `StorageService`). |
|
|
128
130
|
| `routes` | Register plugin-owned rpc queries / mutations / actions (alias-prefixed tags). |
|
|
129
|
-
| `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. |
|
|
131
|
+
| `httpRoutes` | Serve public raw-HTTP endpoints on the framework listener (e.g. `GET /_voltro/storage/:id`). The request carries `store` — the app's DataStore — for a route that must read or write (a login endpoint minting a session row cannot be an rpc mutation), and `remoteAddr`, the client address already resolved through `security.trustedProxies` (use it instead of `x-forwarded-for`). Not tenant-scoped: raw HTTP has no resolved Subject, so scope it yourself. A state-changing route is [origin-checked](/docs/security/overview#cross-site-requests-are-refused) unless it declares `originGuard: 'exempt'`. |
|
|
130
132
|
| `inspectEndpoints` | Mount tooling under `/_voltro/inspect/plugins/<alias>/…`. |
|
|
131
133
|
| `onScheduleFire` / `onWorkflowStep` / `onHttpRequest` | Wrap every cron firing, every workflow `step()`, every pre-auth HTTP request. |
|
|
132
134
|
| `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
|
|
@@ -207,10 +209,15 @@ To REPLACE one deliberately, declare it:
|
|
|
207
209
|
|
|
208
210
|
```ts
|
|
209
211
|
// Adopt the plugin's namespace, add your own leaves beside it…
|
|
210
|
-
defineQuery({ name: 'notifications.archive', … })
|
|
211
|
-
|
|
212
|
-
// …and REPLACE just the one you need to behave differently.
|
|
213
|
-
|
|
212
|
+
defineQuery({ name: 'notifications.archive', guards: [{ scope: 'notifications:read' }], … })
|
|
213
|
+
|
|
214
|
+
// …and REPLACE just the one you need to behave differently. Your replacement is
|
|
215
|
+
// YOUR procedure, so it needs its own access decision — the plugin's does not
|
|
216
|
+
// carry over with the tag.
|
|
217
|
+
defineMutation({
|
|
218
|
+
name: 'notifications.markRead', overridesPlugin: true,
|
|
219
|
+
guards: [{ scope: 'notifications:write' }], …
|
|
220
|
+
})
|
|
214
221
|
```
|
|
215
222
|
|
|
216
223
|
The plugin's route is dropped, not merely permitted alongside yours — permitting
|
|
@@ -345,6 +352,8 @@ onChangeEvent: (event: PluginChangeEvent) => Effect.Effect<void, unknown>
|
|
|
345
352
|
// PluginChangeEvent = {
|
|
346
353
|
// table, op: 'insert'|'update'|'delete', new: Row | null, old: Row | null,
|
|
347
354
|
// origin?: 'inline' | 'injected', changeScope: 'local' | 'fleet',
|
|
355
|
+
// traceId?, subjectId?, procedure?,
|
|
356
|
+
// oversized?: 'rehydrated' | 'tombstone' | 'unrecovered',
|
|
348
357
|
// }
|
|
349
358
|
```
|
|
350
359
|
|
|
@@ -359,6 +368,8 @@ onChangeEvent: (event) =>
|
|
|
359
368
|
)
|
|
360
369
|
```
|
|
361
370
|
|
|
371
|
+
**Check `event.oversized` before you treat an image as a snapshot.** It is absent on an ordinary event, and set when the transport could not carry the row and the images were reconstructed — a row over postgres' 8000-byte NOTIFY cap. `'rehydrated'` means `new` is the row re-read from the database (correct to index or forward, but the row as it is NOW rather than the image at commit); `'tombstone'` means a delete whose `old` is the primary key and nothing else (enough to REMOVE the row, never a record of what it held); `'unrecovered'` means both images are null and the content is gone. A tap that stores history must not write a tombstone as a snapshot. Full guarantee: [postgres — oversized rows](/docs/database/dialects/postgres).
|
|
372
|
+
|
|
362
373
|
Runs under BOTH `voltro dev` and `voltro serve` (the prod serve path fans out the same way). Requires the `'store:changes:read'` permission. It is NOT durable at the framework layer — a crash between commit and the fork loses the event; build durability INSIDE the Effect (insert into an outbox and retry against the typed error channel, the way `@voltro/plugin-cdc-out` does). Exactly-once / change-scope semantics are unchanged: read `event.origin` + `event.changeScope` inside the Effect to act once per change fleet-wide (skip `origin: 'injected'` on `'local'` scope; elect one worker on `'fleet'`). Used by `@voltro/plugin-search` to mirror rows into an external index. For in-transaction reactions use a mutation; for best-effort per-table reactions in app code prefer a `*.subscribe.ts` — `onChangeEvent` is the plugin-level equivalent.
|
|
363
374
|
|
|
364
375
|
### `onInstall` / `onUninstall` / `onActivate` / `onDeactivate` lifecycle
|
|
@@ -523,9 +534,8 @@ export const auditPlugin = (): VoltroPlugin =>
|
|
|
523
534
|
})
|
|
524
535
|
```
|
|
525
536
|
|
|
526
|
-
Tag derivation: `<plugin-alias>.<query.name
|
|
527
|
-
|
|
528
|
-
and kebab→camelCase:
|
|
537
|
+
Tag derivation: `<plugin-alias>.<query.name>`. The plugin alias strips
|
|
538
|
+
`@scope/` + the `plugin-` prefix and kebab→camelCase:
|
|
529
539
|
|
|
530
540
|
| Plugin name | Alias |
|
|
531
541
|
|---|---|
|
|
@@ -535,6 +545,71 @@ and kebab→camelCase:
|
|
|
535
545
|
| `plain-name` | `plainName` |
|
|
536
546
|
| `@voltro/audit` | `audit` (no `plugin-` to strip) |
|
|
537
547
|
|
|
548
|
+
A `query.name` that already contains a dot is handled by whether it names the
|
|
549
|
+
plugin's OWN namespace:
|
|
550
|
+
|
|
551
|
+
- `'notifications.inbox'` on a plugin whose canonical name is
|
|
552
|
+
`@voltro/plugin-notifications` is re-namespaced — under
|
|
553
|
+
`alias: 'inbox'` it becomes `inbox.inbox`, not `notifications.inbox`.
|
|
554
|
+
A deeper path keeps its depth: `'audit.admin.events'` under `alias: 'trail'`
|
|
555
|
+
becomes `trail.admin.events`.
|
|
556
|
+
- `'acme.legacyBridge'` — a namespace that is not the plugin's own — passes
|
|
557
|
+
through untouched. That is the escape hatch, and it is the only case that
|
|
558
|
+
still bypasses the alias.
|
|
559
|
+
|
|
560
|
+
The re-namespacing needs the plugin to declare `baseName` (its canonical name,
|
|
561
|
+
before any app-supplied `alias`); a plugin that omits it keeps the older
|
|
562
|
+
behaviour where any dotted name passes through.
|
|
563
|
+
|
|
564
|
+
### Naming a plugin: `alias` vs `name`
|
|
565
|
+
|
|
566
|
+
Two different app-side problems land on a plugin's name, so first-party plugins
|
|
567
|
+
that carry tables or routes accept two separate options:
|
|
568
|
+
|
|
569
|
+
| Option | Question it answers | Effect |
|
|
570
|
+
|---|---|---|
|
|
571
|
+
| `alias` | "your namespace collides with mine" | replaces the namespace — tags become `<alias>.<route>`, the inspect mount becomes `/_voltro/inspect/plugins/<alias>/…` |
|
|
572
|
+
| `name` | "I want two of these" | appends a `#suffix` discriminator so two installs never register the same tag |
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
notificationsPlugin({ alias: 'alerts' }) // alerts.inbox
|
|
576
|
+
notificationsPlugin({ name: 'ops' }) // notifications#ops.inbox
|
|
577
|
+
notificationsPlugin({ alias: 'alerts', name: 'ops' }) // alerts#ops.inbox
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
`alias` exists to escape a tag collision, which is fatal at codegen. Two costs
|
|
581
|
+
are worth knowing before you reach for it:
|
|
582
|
+
|
|
583
|
+
- the local and cloud dashboards fetch a plugin's inspect panel at its DEFAULT
|
|
584
|
+
slug, so an aliased plugin keeps serving its inspect endpoints while its
|
|
585
|
+
dashboard panel stops resolving;
|
|
586
|
+
- the plugin-migration ledger key is `<plugin-alias>__<migration.id>`, so
|
|
587
|
+
aliasing a plugin that ships `extendSchema.migrations` makes its already-applied
|
|
588
|
+
migrations look unapplied. Choose the alias before first boot, not after.
|
|
589
|
+
|
|
590
|
+
### `tables: false` — keeping your own tables
|
|
591
|
+
|
|
592
|
+
Plugins whose tables carry no authorization or safety decision accept
|
|
593
|
+
`tables: false`, which stops them contributing DDL through `extendSchema` so an
|
|
594
|
+
app can keep equivalent tables it already has. Everything else — routes,
|
|
595
|
+
inspect, interceptors — is unchanged.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
notificationsPlugin({ tables: false }) // you declare the six notification tables
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
It is offered on `@voltro/plugin-rbac`, `@voltro/plugin-audit`,
|
|
602
|
+
`@voltro/plugin-notifications` and `@voltro/plugin-ai-flows`. The plugin still
|
|
603
|
+
writes to those tables BY NAME, so you take over declaring each one with the
|
|
604
|
+
shape the package exports, and a missing or mis-shaped table fails at the first
|
|
605
|
+
write rather than at boot.
|
|
606
|
+
|
|
607
|
+
It is deliberately NOT offered on plugins whose tables carry a guarantee — the
|
|
608
|
+
SAML assertion replay cache, SCIM provisioning state, billing's usage counters,
|
|
609
|
+
cdc-out's delivery outbox, the governance consent ledger, search's tenant-scoped
|
|
610
|
+
index rows. A `tables: false` there would disable a security or correctness
|
|
611
|
+
decision with no signal to the app that it now owns it.
|
|
612
|
+
|
|
538
613
|
Boot fails with a clear error on tag collisions (between two plugins, or
|
|
539
614
|
with a user-authored tag).
|
|
540
615
|
|
|
@@ -770,7 +845,7 @@ markers. The plugin sees the api name + the list of discovered user-query
|
|
|
770
845
|
rpc tags so it can emit per-rpc bindings. Returning `null` contributes
|
|
771
846
|
nothing.
|
|
772
847
|
|
|
773
|
-
### `templates: PluginTemplate[]` — ship
|
|
848
|
+
### `templates: PluginTemplate[]` — ship scaffolding templates
|
|
774
849
|
|
|
775
850
|
```ts
|
|
776
851
|
definePlugin({
|
|
@@ -819,8 +894,14 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
|
|
|
819
894
|
name: '@vendor/plugin-rate-limit',
|
|
820
895
|
permissions: ['http:intercept'],
|
|
821
896
|
onHttpRequest: async (next, ctx) => {
|
|
822
|
-
|
|
823
|
-
|
|
897
|
+
// `ctx.remoteAddr` is ALREADY resolved through the app's
|
|
898
|
+
// `security.trustedProxies` policy — the same value the framework's own
|
|
899
|
+
// rate limiter, geo-block and audit rows use. Never read
|
|
900
|
+
// `ctx.headers['x-forwarded-for']`: it is a request header, so any
|
|
901
|
+
// caller can write it, and a limiter keyed on it is bypassed by one
|
|
902
|
+
// extra header. `undefined` only when the socket address is unavailable.
|
|
903
|
+
const clientAddr = ctx.remoteAddr ?? 'unknown'
|
|
904
|
+
if (buckets.consume(clientAddr, opts.perMinute) === 'exhausted') {
|
|
824
905
|
return {
|
|
825
906
|
status: 429,
|
|
826
907
|
headers: { 'retry-after': '60', 'content-type': 'application/json' },
|
|
@@ -832,10 +913,28 @@ export const rateLimitPlugin = (opts: { perMinute: number }) =>
|
|
|
832
913
|
})
|
|
833
914
|
```
|
|
834
915
|
|
|
916
|
+
**`ctx.remoteAddr` is the client address — `x-forwarded-for` is not.**
|
|
917
|
+
The framework resolves `remoteAddr` through the app's
|
|
918
|
+
`security.trustedProxies` policy (`VOLTRO_TRUSTED_PROXIES`) before it hands you
|
|
919
|
+
the context: with no trusted proxy declared the header is ignored entirely and
|
|
920
|
+
the socket address wins, and with one declared only the hops that are actually
|
|
921
|
+
a configured proxy are believed. Reading `ctx.headers['x-forwarded-for']`
|
|
922
|
+
yourself throws that away and keys your limiter on a string the caller typed —
|
|
923
|
+
one extra header and every request looks like a new client. The same rule
|
|
924
|
+
applies to a plugin's raw HTTP routes, where the resolved value arrives as
|
|
925
|
+
`req.remoteAddr`; see [Trusted proxies](/docs/security/overview#the-same-address-reaches-your-plugin-routes).
|
|
926
|
+
|
|
835
927
|
A per-plugin `plugin.<name>.http-intercept` metric is auto-emitted so
|
|
836
928
|
the dashboard's Plugins panel surfaces HTTP-intercept latency next to
|
|
837
929
|
RPC-intercept latency.
|
|
838
930
|
|
|
931
|
+
**Runs on both boot paths.** The chain is composed and installed identically by
|
|
932
|
+
`voltro dev` and `voltro serve` — this is a production capability, and for a
|
|
933
|
+
pre-auth shield production is the point. There is exactly ONE exemption, and it
|
|
934
|
+
is deliberate: `GET /internal/liveness` and `GET /internal/readiness` are
|
|
935
|
+
answered before the interceptor, so a rate-limit or geo-block plugin cannot 503
|
|
936
|
+
a Kubernetes probe and take the replica out of rotation.
|
|
937
|
+
|
|
839
938
|
### `extendSchema: { tables, migrations }` — contribute schema + migrations
|
|
840
939
|
|
|
841
940
|
A plugin contributes BOTH declarative table descriptors AND custom SQL
|
|
@@ -848,6 +947,14 @@ The ledger key is `<plugin-alias>__<migration.id>` so two plugins can
|
|
|
848
947
|
each ship `'001-init'` without collision. Failure aborts boot;
|
|
849
948
|
re-runs are no-ops.
|
|
850
949
|
|
|
950
|
+
**Which commands run them:** `voltro dev`'s boot auto-migrate, `voltro db apply`
|
|
951
|
+
(bare and `--plan`) and `voltro migrate --create-only`. NOT `voltro serve` —
|
|
952
|
+
serve never applies a schema, so a plugin's steps land in the pre-deploy job
|
|
953
|
+
alongside the schema, which is where they belong. Until 0.34.0 only the `voltro
|
|
954
|
+
dev` boot ran them, so a plugin's SQL steps executed on every developer machine
|
|
955
|
+
and on no deployed database; if you ship migrations, verify against a deployed
|
|
956
|
+
database rather than a dev boot.
|
|
957
|
+
|
|
851
958
|
```ts
|
|
852
959
|
import { Effect, Schema } from 'effect'
|
|
853
960
|
import { definePlugin } from '@voltro/protocol'
|
|
@@ -1387,17 +1494,42 @@ analytics: duckdbAnalytics({
|
|
|
1387
1494
|
}),
|
|
1388
1495
|
```
|
|
1389
1496
|
|
|
1390
|
-
The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data }` — `id` is the source row's primary key, `data` is the full row as JSON. Analytical queries JOIN events against the mirror:
|
|
1497
|
+
The postgres-lite (`postgresAnalytics({ mirrorTables: [...] })`) and ClickHouse (`clickhouseAnalytics({ url, mirrorTables: [...] })`) sinks take the same options. Each mirrored table lands as `_voltro_mirror_<table>` (`voltro_mirror_<table>` on DuckDB / ClickHouse) holding `{ id, data, version, is_deleted }` — `id` is the source row's primary key, `data` is the full row as JSON, `version` orders the writes (see below) and `is_deleted` marks a tombstone. Analytical queries JOIN events against the mirror and filter tombstones out:
|
|
1391
1498
|
|
|
1392
1499
|
```sql
|
|
1393
1500
|
-- DuckDB: events per user tier
|
|
1394
1501
|
SELECT json_extract_string(m.data, '$.tier'), COUNT(*)
|
|
1395
1502
|
FROM voltro_events e
|
|
1396
|
-
JOIN voltro_mirror_users m ON m.id = e.subject_id
|
|
1503
|
+
JOIN voltro_mirror_users m ON m.id = e.subject_id AND m.is_deleted = false
|
|
1397
1504
|
GROUP BY 1
|
|
1398
1505
|
```
|
|
1399
1506
|
|
|
1400
|
-
The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes
|
|
1507
|
+
The mirror is **opt-in** (omit `mirrorTables` → events-only) and **idempotent** — inserts/updates upsert by primary key, deletes write a tombstone by key, so a re-delivered change (e.g. after a reconnect) is a no-op-equivalent overwrite.
|
|
1508
|
+
|
|
1509
|
+
### Delivery guarantee
|
|
1510
|
+
|
|
1511
|
+
**At-least-once for the lifetime of the process, ordered per row.** Read that sentence literally — every clause is a promise the framework keeps, and the sentence stops where the implementation does:
|
|
1512
|
+
|
|
1513
|
+
- **Retried, not dropped.** A failing mirror write is retried with exponential backoff (`retryAttempts`, default 5). A write that outlives its retries is queued for **repair**: a timer re-reads the row's *current* state from your database and re-applies it, so the mirror converges on the truth rather than on a stale change that happened to be in flight.
|
|
1514
|
+
- **Ordered per row.** Writes for the same primary key are applied one at a time, in commit order, and every write carries a **version** stamped when the change left the store — never a clock read inside the warehouse client. A change that arrives late therefore *loses*: ClickHouse's `ReplacingMergeTree(version)` keeps the highest version, and the DuckDB / postgres mirrors apply the update only when the incoming version is newer. Different rows are still mirrored concurrently.
|
|
1515
|
+
- **A delete is a tombstone, not a row removal.** Filter `is_deleted = false` (`is_deleted = 0` / `FINAL` on ClickHouse). A physical delete would leave nothing for a late, stale insert of the same key to lose against — the row would silently come back.
|
|
1516
|
+
- **Never surfaces to the request.** The OLTP write that produced the change has already committed; a warehouse outage is isolated into the log channel and the metrics below.
|
|
1517
|
+
- **A graceful shutdown is not a crash.** On SIGINT / SIGTERM the mirror stops taking new changes and then **settles** what is already queued and in flight, before the sink itself is disposed. That drain is bounded (3 s of the teardown budget you set with `VOLTRO_SHUTDOWN_GRACE_MS`, default 10 s): a warehouse that has stopped answering cannot hold the process open until the orchestrator's SIGKILL, which would lose strictly more. The two outcomes log differently — `analytics mirror drained` at info, or a `warn` naming what was still pending when the deadline cut it, because that is the moment those counters can still be read.
|
|
1518
|
+
- **Not durable across a crash.** The repair queue lives in memory. A change still awaiting repair when the process dies — SIGKILL, an OOM, a host failure — is lost, as is one evicted after `repairQueueLimit`. Both are logged at error level and counted by `voltro_analytics_mirror_dropped_total` — if that counter is non-zero, the affected tables need a re-seed.
|
|
1519
|
+
|
|
1520
|
+
Metrics: `voltro_analytics_mirror_forwarded_total`, `..._retries_total`, `..._repair_queued_total`, `..._dropped_total`.
|
|
1521
|
+
|
|
1522
|
+
### Tuning
|
|
1523
|
+
|
|
1524
|
+
Every number the mirror picks on your behalf has a default and an environment override:
|
|
1525
|
+
|
|
1526
|
+
| Env var | Default | Meaning |
|
|
1527
|
+
|---|---|---|
|
|
1528
|
+
| `VOLTRO_ANALYTICS_MIRROR_RETRY_ATTEMPTS` | `5` | Total attempts per mirror write (`1` = no retry). |
|
|
1529
|
+
| `VOLTRO_ANALYTICS_MIRROR_RETRY_BASE_MS` | `100` | First backoff delay; doubles per attempt. |
|
|
1530
|
+
| `VOLTRO_ANALYTICS_MIRROR_RETRY_MAX_MS` | `30000` | Ceiling for the doubling backoff. |
|
|
1531
|
+
| `VOLTRO_ANALYTICS_MIRROR_REPAIR_INTERVAL_MS` | `60000` | How often the repair loop re-drives exhausted changes (`0` disables it). |
|
|
1532
|
+
| `VOLTRO_ANALYTICS_MIRROR_REPAIR_QUEUE_LIMIT` | `10000` | Maximum keys held for repair before the oldest is dropped and counted. |
|
|
1401
1533
|
|
|
1402
1534
|
## Default — no sink configured
|
|
1403
1535
|
|
|
@@ -13,10 +13,13 @@ _The client-side hook surface, grouped by purpose._
|
|
|
13
13
|
|
|
14
14
|
The web side of a Voltro app talks to API apps through React hooks. Data hooks are keyed by API name and RPC tag; routing hooks are provided by `@voltro/web`.
|
|
15
15
|
|
|
16
|
+
App code should reach for the **typed binding** rather than the tag-taking hooks directly: [`createHooks(apiName)`](/docs/reference/hooks-data) takes the api's generated `AppProcedures` map and returns `useSubscription` / `useMutation` / `useAction` whose tag is a literal union and whose input and output types are inferred — so a typo'd tag is a compile error and the result needs no annotation. Bind it once per api in `src/lib/api.ts`. The tag-taking forms documented below are the primitive underneath, for code that only learns the tag at runtime.
|
|
17
|
+
|
|
16
18
|
## Data Hooks
|
|
17
19
|
|
|
18
20
|
| Hook | Purpose |
|
|
19
21
|
|---|---|
|
|
22
|
+
| [`createHooks`](/docs/reference/hooks-data) | Bind the typed hook surface for one api — the recommended app-facing entry point. |
|
|
20
23
|
| [`useSubscription`](/docs/reference/hooks-data) | Subscribe to a reactive query (`*.query.ts`). |
|
|
21
24
|
| [`useMutation`](/docs/reference/hooks-data) | Run an atomic write (`*.mutation.ts`). |
|
|
22
25
|
| [`useAction`](/docs/reference/hooks-data) | Run a unary non-transactional action (`*.action.ts`). |
|
|
@@ -112,6 +115,51 @@ _useSubscription, useMutation, useAction, useWorkflow, workflow status hooks, us
|
|
|
112
115
|
|
|
113
116
|
Data hooks from `@voltro/client` are keyed by **api name** plus **RPC tag**. The tags come from descriptors discovered in the API app.
|
|
114
117
|
|
|
118
|
+
## `createHooks(apiName)` — the typed hook binding
|
|
119
|
+
|
|
120
|
+
`createHooks` turns the api's generated procedure map into hooks whose **RPC tag is a literal union** and whose **input and output types are inferred**. It is the recommended way to call an api from app code.
|
|
121
|
+
|
|
122
|
+
Codegen emits `AppProcedures` into the api's `rpcGroup.generated.ts` — a type-level map of every tag (yours and every plugin's) to the descriptor behind it. Bind it once, at module scope, next to the rest of your api glue:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// src/lib/api.ts — one file, one line, once per app
|
|
126
|
+
import { createHooks } from '@voltro/client'
|
|
127
|
+
import type { AppProcedures } from '@app/api/rpcGroup'
|
|
128
|
+
|
|
129
|
+
export const { useSubscription, useMutation, useAction } = createHooks<AppProcedures>('app')
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`'app'` is the key this web app gave the api in `app.config.ts` → `apis`. It is now spelled exactly once per app instead of once per call site:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useSubscription, useMutation } from '../lib/api'
|
|
136
|
+
|
|
137
|
+
const { data } = useSubscription('notes.list') // ReadonlyArray<Note>, inferred
|
|
138
|
+
const create = useMutation('notes.create') // input + output inferred
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
What the compiler catches that it could not before:
|
|
142
|
+
|
|
143
|
+
| Mistake | Before | Now |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| Typo in the tag (`'notes.lst'`) | Runtime dev-console error | Compile error |
|
|
146
|
+
| Wrong hook for the kind (a mutation passed to `useSubscription`) | Runtime `console.error` | Compile error |
|
|
147
|
+
| Missing a required input field | Request sent with `undefined` | Compile error |
|
|
148
|
+
| Wrongly-shaped input | Server-side decode failure | Compile error |
|
|
149
|
+
| Result type annotation disagreeing with the server | Never detected | Impossible — there is no annotation |
|
|
150
|
+
|
|
151
|
+
The result keeps every narrowing rule of the untyped hook: `loading` still discriminates `data`, `fallback` / `initialSnapshot` still remove the branch, and only a **dynamic** `skip` adds the `idle` state.
|
|
152
|
+
|
|
153
|
+
Row types include the auto-optimistic marker the client adds, so `row.optimistic` type-checks on a live list without a hand-written row mirror.
|
|
154
|
+
|
|
155
|
+
**Destructure the result** — do not export the object and call `api.useSubscription(...)`. `react-hooks/rules-of-hooks` only recognises a member call as a hook when the object is PascalCase, so a lowercase namespace silently switches off rules-of-hooks and exhaustive-deps at every call site.
|
|
156
|
+
|
|
157
|
+
`import type` is erased at build time, so the binding adds nothing to the browser bundle.
|
|
158
|
+
|
|
159
|
+
**`rpcGroup.generated.ts` is written by codegen**, so a tree that has never booted does not have it yet and `tsc` reports `Cannot find module '@app/api/rpcGroup'`. `voltro dev` generates it on boot; run `voltro codegen` once for a fresh clone or a CI job that only typechecks. The scaffolded api templates do this in their own `typecheck` script, and `pnpm -r` runs the api before anything that depends on it.
|
|
160
|
+
|
|
161
|
+
The tag-taking hooks below are the primitive underneath. Reach for them when the tag is only known at runtime — plugin web bindings and libraries shipped against an unknown app — not in app code.
|
|
162
|
+
|
|
115
163
|
## `useSubscription(apiName, rpcTag, input?, options?)`
|
|
116
164
|
|
|
117
165
|
Subscribes to a reactive `defineQuery` RPC.
|
|
@@ -1101,13 +1149,18 @@ If the work is "do X once and finish", it's a seed. If it's "keep X running unti
|
|
|
1101
1149
|
<!-- source: en/reference/templates.md -->
|
|
1102
1150
|
## App templates
|
|
1103
1151
|
|
|
1104
|
-
_The scaffolding catalogue —
|
|
1152
|
+
_The scaffolding catalogue — the api / web / serverless / mobile templates, what each demonstrates, and when to pick it._
|
|
1105
1153
|
|
|
1106
1154
|
Every template is a dogfooded, runnable reference. Scaffold one with
|
|
1107
1155
|
`voltro create-project --api <id> --web <id>` (or `voltro add-app <name>
|
|
1108
|
-
--template <id>`)
|
|
1156
|
+
--template <id>`).
|
|
1157
|
+
|
|
1158
|
+
**`voltro list-templates` is the authority, not this page.** It prints the
|
|
1159
|
+
templates your installed CLI actually ships — there are dozens — while the
|
|
1160
|
+
tables below cover the ones worth a paragraph of explanation. If an id appears
|
|
1161
|
+
in the command's output and not here, it exists and works.
|
|
1109
1162
|
|
|
1110
|
-
Templates come in **
|
|
1163
|
+
Templates come in **four kinds**, matching the four things you deploy:
|
|
1111
1164
|
|
|
1112
1165
|
- **`api`** — a long-running backend (`app.config` `type: 'api'`).
|
|
1113
1166
|
- **`web`** — a frontend (`type: 'web'`); gets a dev-server port.
|
|
@@ -1115,6 +1168,10 @@ Templates come in **three kinds**, matching the three things you deploy:
|
|
|
1115
1168
|
functions](/docs/deployment/serverless-functions) shipped on their own with
|
|
1116
1169
|
`voltro serverless`. No long-running server, no port — add it to a project
|
|
1117
1170
|
with `voltro add-app`.
|
|
1171
|
+
- **`mobile`** — an Expo (React Native) app (`mobile-app`), scaffolded with
|
|
1172
|
+
`--mobile`. Expo owns its own dev loop, so it is not part of `voltro dev`; the
|
|
1173
|
+
sibling api still is, and the app consumes it over the network with the same
|
|
1174
|
+
typed hooks the web app uses.
|
|
1118
1175
|
|
|
1119
1176
|
## API backends (`kind: api`)
|
|
1120
1177
|
|