@voltro/cli 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
> Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- source: en/deployment/voltro-cloud.md -->
|
|
10
|
+
## Voltro Cloud
|
|
11
|
+
|
|
12
|
+
_Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps._
|
|
13
|
+
|
|
14
|
+
Voltro Cloud is the managed tier of the same open-core runtime you run locally. It's not a fork — `voltro start` is what powers your production instances, just with custom domains, multi-region routing, provisioned Postgres / Redis / object storage, and a control plane that handles deploys.
|
|
15
|
+
|
|
16
|
+
**Status:** managed cloud **deploy + provisioning is coming soon** and is not yet available. What's live today is the **Free control plane**: sign up and use it to **register and observe your self-hosted apps** (hosted observability, quotas, governance, teams). The managed-tier features described below are the planned shape of Voltro Cloud — see the per-section notes.
|
|
17
|
+
|
|
18
|
+
## When to use Cloud vs self-host
|
|
19
|
+
|
|
20
|
+
| | Cloud (coming soon) | Self-host (today) |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Managed Postgres + Redis + pgvector | ✓ | You provision |
|
|
23
|
+
| Multi-region + edge routing | ✓ | You set up Caddy / Cloudflare / your CDN |
|
|
24
|
+
| Custom domains + automatic SSL | ✓ | You manage certs |
|
|
25
|
+
| AI provider gateway (key rotation, fallback) | ✓ | You wire `AI_PROVIDER` directly |
|
|
26
|
+
| Per-tenant vanity domains | ✓ (Enterprise) | Possible but you build the routing |
|
|
27
|
+
| Observability (logs, traces, metrics) | ✓ | You wire OpenTelemetry to your sink |
|
|
28
|
+
| Audit log retention | ≥ 90 days | Whatever you configure |
|
|
29
|
+
| You own the data | ✓ — export anytime | ✓ |
|
|
30
|
+
| You own the source code | ✓ | ✓ |
|
|
31
|
+
|
|
32
|
+
Pre-release: the **Free tier is live** — sign up (passwordless, an email one-time code) and use the control plane to register + observe your **self-hosted** apps. Managed cloud **deploy + provisioning is coming soon** (`voltro cloud deploy` prints `control-plane deploy pipeline pending — not yet available` until it ships); paid tiers open with it at launch.
|
|
33
|
+
|
|
34
|
+
## The `voltro cloud` client
|
|
35
|
+
|
|
36
|
+
The control-plane client is `voltro cloud`, not a top-level `voltro deploy`. It authenticates against the control plane (base url via `VOLTRO_CLOUD_URL`) and manages projects + env vars:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
voltro cloud login [--token <t>] # store a session token in ~/.voltro/credentials.json
|
|
40
|
+
voltro cloud whoami # the current account
|
|
41
|
+
voltro cloud projects # list your projects
|
|
42
|
+
voltro cloud env list [--project P] # list a project's env vars
|
|
43
|
+
voltro cloud env set <KEY> <VALUE> [--project P]
|
|
44
|
+
voltro cloud env pull [--project P] # write .env.cloud from the project's env
|
|
45
|
+
voltro cloud import # scaffold voltro.cloud.toml from app.config.ts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Scaffolding the deploy config — `voltro cloud import`
|
|
49
|
+
|
|
50
|
+
`voltro cloud import` reads the app's `app.config.ts` (name + type) and writes a `voltro.cloud.toml` next to it. It refuses to overwrite an existing file. The generated shape:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
# voltro.cloud.toml — generated by `voltro cloud import`.
|
|
54
|
+
# Describes how this app deploys to the framework cloud control-plane.
|
|
55
|
+
|
|
56
|
+
[project]
|
|
57
|
+
name = "acme"
|
|
58
|
+
type = "api"
|
|
59
|
+
|
|
60
|
+
[deploy]
|
|
61
|
+
# Target region + replica count. Tune before `voltro cloud deploy`.
|
|
62
|
+
region = "auto"
|
|
63
|
+
replicas = 1
|
|
64
|
+
|
|
65
|
+
[env]
|
|
66
|
+
# Project env-var names resolved from the control-plane at deploy time.
|
|
67
|
+
# Use `voltro cloud env set <KEY> <VALUE>` to populate them.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Deploy is pending
|
|
71
|
+
|
|
72
|
+
`voltro cloud deploy` exists but the control-plane deploy pipeline is not yet available — running it prints `control-plane deploy pipeline pending — not yet available`. Build, upload, migrate, traffic-shifting, and one-click rollback are the planned shape of that pipeline; until it lands, use the `voltro cloud` env + project commands above to prepare a project.
|
|
73
|
+
|
|
74
|
+
## Environments
|
|
75
|
+
|
|
76
|
+
Per-project environments (`preview` / `staging` / `production`), each with its own env vars + DB + storage bucket, are part of the planned control-plane surface. Manage env vars today with `voltro cloud env list/set/pull`.
|
|
77
|
+
|
|
78
|
+
## Custom domains
|
|
79
|
+
|
|
80
|
+
*Coming soon with managed cloud hosting — not yet available.* When it ships, you'll add a domain in the Cloud UI and the platform handles:
|
|
81
|
+
|
|
82
|
+
- DNS verification (TXT record)
|
|
83
|
+
- ACME-issued SSL (Let's Encrypt or Google Trust Services)
|
|
84
|
+
- Auto-renewal
|
|
85
|
+
- Per-tenant vanity domains (Enterprise) — `tenant1.your-product.com` routes to the right tenant scope automatically
|
|
86
|
+
|
|
87
|
+
## Observability
|
|
88
|
+
|
|
89
|
+
*Available today for your registered self-hosted apps* (this is the live Free-tier control plane). OpenTelemetry-native:
|
|
90
|
+
|
|
91
|
+
- **Logs** flow to the project's log stream (search + filter in-UI; export to Datadog / Loki / etc.)
|
|
92
|
+
- **Traces** for every mutation, query, workflow, agent run
|
|
93
|
+
- **Metrics** for request rates, error rates, p50/p95/p99 latencies, AI token spend per tenant
|
|
94
|
+
|
|
95
|
+
## Provisioning
|
|
96
|
+
|
|
97
|
+
*Coming soon with managed cloud hosting — not yet available.* When it ships, Cloud will provision managed Postgres, Redis, R2, Resend, etc. on your behalf. You'll also be able to BYO (point at your own Neon, Upstash, etc. via env vars) — billing for those goes through your account with the provider. Today you provision these yourself and self-host.
|
|
98
|
+
|
|
99
|
+
See the [Cloud Premium Matrix](https://github.com/voltro-cloud/cloud-public/blob/main/PREMIUM.md) for the full feature breakdown.
|
|
100
|
+
|
|
101
|
+
## Migration from self-hosted to Cloud
|
|
102
|
+
|
|
103
|
+
*Deploying onto Cloud is coming soon — not yet available.* You can prepare a project today (scaffold the config, populate env), but there is no managed destination to deploy to yet.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
voltro cloud import # scaffold voltro.cloud.toml from the app's app.config.ts
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`voltro cloud import` reads the app's `app.config.ts` and writes a `voltro.cloud.toml` (project name + type + a deploy/env skeleton). Populate the project env with `voltro cloud env set`. When managed hosting ships, your local Postgres data won't be migrated for you — you'll use `pg_dump` + `pg_restore` against the target database directly.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
<!-- source: en/deployment/self-hosting.md -->
|
|
116
|
+
## Self-hosting
|
|
117
|
+
|
|
118
|
+
_Run Voltro on your own infra — Docker compose, env vars, reverse proxy, scaling._
|
|
119
|
+
|
|
120
|
+
Voltro is open-core. The runtime that powers Voltro Cloud is the same runtime you run locally with `voltro start`. Self-hosting is fully supported — no separate "lite" runtime, no feature gates around the core API.
|
|
121
|
+
|
|
122
|
+
## The shape of a self-hosted deploy
|
|
123
|
+
|
|
124
|
+
You need:
|
|
125
|
+
|
|
126
|
+
1. **Node.js 24+** to run the api + web processes.
|
|
127
|
+
2. **Postgres 16+** with `pgvector` extension (for AI / search) and `wal_level=logical` (for subscriptions). Managed Postgres at Neon, Supabase, RDS, or your own server all work.
|
|
128
|
+
3. **A reverse proxy** (Caddy / nginx / Cloudflare). Terminates TLS, routes paths, fronts the framework's processes.
|
|
129
|
+
4. **(Optional) Redis** for cross-instance subscription fan-out when you scale beyond one api process.
|
|
130
|
+
5. **(Optional) Object storage** (R2, S3, GCS) — required if you use `@voltro/plugin-storage`.
|
|
131
|
+
|
|
132
|
+
Voltro doesn't require Kubernetes or any orchestrator. A single Docker host + Caddy is enough for ≥99% of deployments.
|
|
133
|
+
|
|
134
|
+
## The supported path: `voltro baseline`
|
|
135
|
+
|
|
136
|
+
You don't have to hand-write any of the deploy plumbing below — the CLI ships baselines that generate it. Pick one at project creation or switch later:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
voltro create-project acme --api=api-backend --web=frontend-blank --baseline=compose
|
|
140
|
+
# or, on an existing project:
|
|
141
|
+
voltro baseline set compose # generates docker/{api,web}.Dockerfile + docker-compose*.yml
|
|
142
|
+
voltro baseline set helm # generates charts/voltro-app/ for Kubernetes
|
|
143
|
+
voltro baseline status # which baseline is active
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `bare` — `.env.example` + a sample systemd unit. You bring your own Postgres + deploy.
|
|
147
|
+
- `compose` — `docker/{api,web}.Dockerfile`, `docker-compose.yml` (postgres infra), `docker-compose.dev.yml` (HMR), `docker-compose.prod.yml` (built images).
|
|
148
|
+
- `helm` — a `charts/voltro-app/` chart with per-env values + Deployment / Service / Ingress / Postgres StatefulSet.
|
|
149
|
+
|
|
150
|
+
The hand-rolled compose + Caddyfile below are a reference for *what `voltro baseline set compose` generates* — read them to understand the shape, but prefer the generated files.
|
|
151
|
+
|
|
152
|
+
## Docker compose
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
# docker-compose.yml
|
|
156
|
+
services:
|
|
157
|
+
postgres:
|
|
158
|
+
image: pgvector/pgvector:pg16
|
|
159
|
+
environment:
|
|
160
|
+
POSTGRES_PASSWORD: change-me
|
|
161
|
+
POSTGRES_DB: voltro
|
|
162
|
+
command: postgres -c wal_level=logical -c max_replication_slots=10
|
|
163
|
+
volumes:
|
|
164
|
+
- pg-data:/var/lib/postgresql/data
|
|
165
|
+
|
|
166
|
+
api:
|
|
167
|
+
build: ./apps/api
|
|
168
|
+
environment:
|
|
169
|
+
DB_DIALECT: postgres
|
|
170
|
+
DB_URL: postgres://postgres:change-me@postgres:5432/voltro
|
|
171
|
+
VOLTRO_SESSION_SECRET: ${VOLTRO_SESSION_SECRET}
|
|
172
|
+
AI_PROVIDER: anthropic
|
|
173
|
+
AI_API_KEY: ${ANTHROPIC_API_KEY}
|
|
174
|
+
depends_on: [postgres]
|
|
175
|
+
|
|
176
|
+
web:
|
|
177
|
+
build: ./apps/web
|
|
178
|
+
environment:
|
|
179
|
+
PORT: 5173
|
|
180
|
+
depends_on: [api]
|
|
181
|
+
|
|
182
|
+
caddy:
|
|
183
|
+
image: caddy:2
|
|
184
|
+
ports: ["80:80", "443:443"]
|
|
185
|
+
volumes:
|
|
186
|
+
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
187
|
+
|
|
188
|
+
volumes:
|
|
189
|
+
pg-data:
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Caddyfile
|
|
193
|
+
|
|
194
|
+
```caddyfile
|
|
195
|
+
your-product.com {
|
|
196
|
+
# The api serves the rpc WebSocket upgrade at /ws, the one-shot HTTP rpc
|
|
197
|
+
# at /rpc, and introspection under /_voltro/*. Everything else is the web app.
|
|
198
|
+
reverse_proxy /ws api:4000
|
|
199
|
+
reverse_proxy /rpc api:4000
|
|
200
|
+
reverse_proxy /_voltro/* api:4000
|
|
201
|
+
reverse_proxy * web:5173
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Single-origin deployment — no CORS, `SameSite=strict` cookies, no `Domain` attribute needed. The cleanest production layout. (Adjust the paths if your app mounts the rpc transport on a custom path — see below.)
|
|
206
|
+
|
|
207
|
+
### Custom WebSocket path — `transport.wsPath`
|
|
208
|
+
|
|
209
|
+
By default the browser connects the realtime rpc WebSocket to a same-origin path derived per api (`/ws/<name>`), and the api serves it at `/ws`. When your edge only allows WebSocket **upgrades** on a *specific* path (a WAF / CDN rule), or you must match a path a **previous stack** used, set `transport.wsPath` on the **api** app. The api then serves the socket there AND the web client **auto-derives the same path** — declare it once, no web-side config, no drift:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
// apps/api/app.config.ts
|
|
213
|
+
export default {
|
|
214
|
+
type: 'api' as const,
|
|
215
|
+
name: 'api',
|
|
216
|
+
store: 'postgres' as const,
|
|
217
|
+
// Serve the rpc WebSocket on a fixed absolute path. The web app that
|
|
218
|
+
// consumes this api reads this value and connects here — nothing to set on
|
|
219
|
+
// the web side.
|
|
220
|
+
transport: { wsPath: '/api/sync' },
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Then route that path to the api in your proxy — everything else is unchanged:
|
|
225
|
+
|
|
226
|
+
```caddyfile
|
|
227
|
+
your-product.com {
|
|
228
|
+
reverse_proxy /api/sync api:4000 # the custom WS path (served by the api)
|
|
229
|
+
reverse_proxy /rpc api:4000
|
|
230
|
+
reverse_proxy /_voltro/* api:4000
|
|
231
|
+
reverse_proxy * web:5173
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The path is **relative** in the bundle, so the host resolves at runtime — **one built image works across every environment**; only the path is fixed. Keep the socket **same-origin** with the web app: the session cookie rides the WS upgrade, and a cross-origin socket wouldn't carry it (every subscription would run anonymous). For an api on a genuinely separate origin, use the api spec's absolute `url` on the web side plus a token-based `headers` resolver instead of a cookie. (One custom `wsPath` per origin — the framework fails the build if two same-origin apis would collide on it.)
|
|
236
|
+
|
|
237
|
+
### `allowedHosts` — fronting the web app on a real domain
|
|
238
|
+
|
|
239
|
+
Vite 8's DNS-rebind guard returns HTTP **403** for any request whose `Host`
|
|
240
|
+
header isn't an IP or `localhost`. So a Voltro web app served through a
|
|
241
|
+
reverse proxy / ingress on a real domain (e.g. `app.dev.example.com`) 403s
|
|
242
|
+
in the browser until that host is allow-listed. Add the new optional field on
|
|
243
|
+
the **web** app's `app.config.ts`:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
// apps/web/app.config.ts
|
|
247
|
+
export default {
|
|
248
|
+
type: 'web' as const,
|
|
249
|
+
name: 'web',
|
|
250
|
+
// string[] | true — default: unset = Vite default (only IPs + localhost).
|
|
251
|
+
allowedHosts: ['app.dev.example.com'], // a leading-dot entry like
|
|
252
|
+
// '.example.com' matches all subdomains
|
|
253
|
+
// allowedHosts: true, // disable the check entirely —
|
|
254
|
+
// only behind a trusted proxy, since
|
|
255
|
+
// it removes the rebind protection
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This applies to the dev / Vite-served surface — a production `voltro start`
|
|
260
|
+
SSR/SPA host fronted by the proxy. If you see a 403 in the browser but the
|
|
261
|
+
proxy logs show the request reaching the app, this is almost always the
|
|
262
|
+
cause.
|
|
263
|
+
|
|
264
|
+
## Env vars
|
|
265
|
+
|
|
266
|
+
| Var | Required | Notes |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `DB_DIALECT` | for SQL stores | `postgres` (default) / `mysql` / `mariadb` / `mssql` / `sqlite` / `turso` |
|
|
269
|
+
| `DB_URL` | for SQL stores | Database connection string (falls back to `DB_PRIMARY_URL`; or the discrete `DB_*` / `PG_*` fields) |
|
|
270
|
+
| `VOLTRO_SESSION_SECRET` | when using `@voltro/plugin-auth` | 32+ random bytes from a real CSPRNG — the session-cookie signing key (`voltro secret generate session`) |
|
|
271
|
+
| `VOLTRO_DATA_TRANSFER_SECRET` | to enable `--target api` export/import | Gates `POST /_voltro/admin/{export,import}`; ≥16 chars or the routes stay unmounted (`voltro secret generate data-transfer`) |
|
|
272
|
+
| `VOLTRO_BUNDLE_KEY` | for encrypted `.vbundle` exports | Passphrase for at-rest bundle encryption — a DEDICATED key, not the transfer secret (`voltro secret generate bundle-key`) |
|
|
273
|
+
| `VOLTRO_FIELD_ENCRYPTION_KEY` | with `governancePlugin({ fieldEncryption: true })` | Key for `.encrypted()` columns; boot fails loudly if missing while such columns exist (`voltro secret generate field-encryption`) |
|
|
274
|
+
| `VOLTRO_STORAGE_SECRET` | optional (`@voltro/plugin-storage`) | Signs private-file grant tokens; falls back to the session secret if unset |
|
|
275
|
+
| `AI_PROVIDER` | when using `@voltro/ai` | `anthropic` / `openai` / `mock` |
|
|
276
|
+
| `AI_API_KEY` | with `AI_PROVIDER` | Provider's API key |
|
|
277
|
+
| `SSR_CACHE` | optional | `memory` (default) or `postgres` (needs `PG_*` set too) for the ISR cache |
|
|
278
|
+
| `PORT` | optional | App's listen port (overrides `app.config.ts.port`) |
|
|
279
|
+
| `VOLTRO_INSPECT` | optional | `off` to disable `_voltro/inspect/*` in prod |
|
|
280
|
+
| `VOLTRO_INSPECT_TOKEN` | recommended | Bearer token guard on inspect endpoints |
|
|
281
|
+
| `VOLTRO_DASHBOARD_APPS` | dashboard only | JSON `[{name?,url,token?}]` — target apps + inspect tokens the deployed DevTools dashboard shows (served at runtime from `/api/dashboard/config`) |
|
|
282
|
+
|
|
283
|
+
## Scaling beyond one instance
|
|
284
|
+
|
|
285
|
+
When one api process isn't enough:
|
|
286
|
+
|
|
287
|
+
1. Run multiple api containers behind the reverse proxy. The proxy's load-balancing default (round-robin) is fine for HTTP — for WebSocket, use sticky sessions (Caddy: `lb_policy ip_hash`).
|
|
288
|
+
2. Install `@voltro/plugin-cluster` in `app.config.ts.plugins`. It sets up cross-instance subscription invalidation via Postgres NOTIFY (or Redis if you set `CLUSTER_TRANSPORT=redis`).
|
|
289
|
+
3. Workflows: `@effect/cluster` shards work across instances by workflow ID. No further config — every instance pulls from the shared workflow queue.
|
|
290
|
+
|
|
291
|
+
For multi-region: you need to run Postgres logical replication between regions yourself (or move to Voltro Cloud which handles it).
|
|
292
|
+
|
|
293
|
+
## Backups
|
|
294
|
+
|
|
295
|
+
Postgres is the source of truth. Use your provider's backup features (Neon PITR, RDS snapshots, `pg_dump` on a cron). Object-storage assets back up via the provider's lifecycle policies.
|
|
296
|
+
|
|
297
|
+
## Deploying the DevTools dashboard
|
|
298
|
+
|
|
299
|
+
The DevTools dashboard (route sitemap, rpc list, logs / traces, schedules, the per-plugin inspect panels) can run as its **own deployment on its own URL**. It reaches each observed app's `/_voltro/inspect/*` through its own same-origin proxy, so the target apps only need to be reachable from the dashboard pod — not the browser.
|
|
300
|
+
|
|
301
|
+
- **Image:** the official `docker.io/voltro/dashboard` — a web-only Voltro app served by `voltro start`.
|
|
302
|
+
- **Targets at runtime:** set `VOLTRO_DASHBOARD_APPS` to a JSON array of `{ name?, url, token? }`, where `url` is each app's origin and `token` is that app's `VOLTRO_INSPECT_TOKEN`. The dashboard serves it from `/api/dashboard/config` on boot, so **one image serves any environment** and the tokens stay server-side (never baked into the bundle):
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
VOLTRO_DASHBOARD_APPS='[{"name":"prod","url":"https://app.example.com","token":"…"}]'
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- **Gate it:** the dashboard is an ops surface — put it behind an IP allow-list and/or your SSO proxy. Each target app's inspect surface should itself be token-gated (`VOLTRO_INSPECT_TOKEN`); the dashboard forwards the configured token as `Authorization: Bearer`.
|
|
309
|
+
|
|
310
|
+
## Production checklist
|
|
311
|
+
|
|
312
|
+
- [ ] HTTPS terminates at the edge with HSTS preload
|
|
313
|
+
- [ ] `VOLTRO_SESSION_SECRET` rotated from CSPRNG, stored in a secrets manager (not env file in git!)
|
|
314
|
+
- [ ] `cookieSecure: true` in the `@voltro/plugin-auth` config
|
|
315
|
+
- [ ] Postgres `wal_level=logical`, `max_replication_slots≥10`
|
|
316
|
+
- [ ] `pgvector` extension if you use AI features
|
|
317
|
+
- [ ] OpenTelemetry exporter pointed at your sink (Datadog / Honeycomb / Grafana / Loki)
|
|
318
|
+
- [ ] `VOLTRO_INSPECT_TOKEN` set, or `VOLTRO_INSPECT=off` in production
|
|
319
|
+
- [ ] CORS allow-list is exact origins (no `*` with credentials)
|
|
320
|
+
- [ ] Database backups verified (a backup you've never restored isn't a backup)
|
|
321
|
+
- [ ] At least one staging deploy that mirrors production exactly
|
|
322
|
+
|
|
323
|
+
See `@voltro/plugin-auth/COOKIES.md` for the full cookie audit, and the [Why Voltro?](/docs/intro/why-voltro) page for what we won't build (and what we will).
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
<!-- source: en/deployment/scale-to-zero.md -->
|
|
330
|
+
## Scale to zero
|
|
331
|
+
|
|
332
|
+
_Dormancy — stop an idle app and bring it back on a request or a due cron/workflow, with no Kubernetes._
|
|
333
|
+
|
|
334
|
+
Dormancy lets an idle app **scale to zero** — the app process stops while nothing is happening and comes back automatically on the next request, or when a scheduled job or a sleeping workflow is due. Idle apps cost almost nothing.
|
|
335
|
+
|
|
336
|
+
It is the same runtime as always; an always-on **wake-orchestrator** sits in front and manages the app's lifecycle. Nothing about your queries, mutations, routes, schedules, or workflows changes.
|
|
337
|
+
|
|
338
|
+
## Turn it on
|
|
339
|
+
|
|
340
|
+
Two pieces: opt the app into sleep mode, then run the orchestrator in front of it.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// apps/api/app.config.ts
|
|
344
|
+
export default {
|
|
345
|
+
type: 'api' as const,
|
|
346
|
+
name: 'myApi',
|
|
347
|
+
store: 'postgres' as const,
|
|
348
|
+
dormancy: 'sleep', // 'always-on' (default) | 'sleep'
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
In `dormancy: 'sleep'`, self-scheduled crons and durable-workflow waits register a row in the `_voltro_wakeups` table instead of holding an in-process timer — so the orchestrator knows exactly when the app must be awake.
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Single-node, no Kubernetes — requires a SQL store (postgres/mysql/mariadb/mssql)
|
|
356
|
+
voltro dormancy
|
|
357
|
+
voltro dormancy --port 4000 --app-port 4001 --idle-grace-ms 60000
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`voltro dormancy` owns the public port, spawns `voltro serve` on an internal port, and proxies to it. It **wakes** the app on the first request / WebSocket upgrade or when a `_voltro_wakeups` row falls due, and **stops** it once it has been idle — no clients, no in-flight requests, no actively-running workflow, no imminent wakeup — for the grace window.
|
|
361
|
+
|
|
362
|
+
A reconnecting reactive client transparently wakes the app: the WebSocket upgrade brings it up, then the client re-subscribes from its cursor.
|
|
363
|
+
|
|
364
|
+
> The orchestrator and the app share the app's database (the `_voltro_wakeups` table), so dormancy needs a **SQL store** — an in-process `memory` store can't be shared across the two processes.
|
|
365
|
+
|
|
366
|
+
## What keeps an app awake
|
|
367
|
+
|
|
368
|
+
The app is only stopped when, continuously for the grace window:
|
|
369
|
+
|
|
370
|
+
- no WebSocket/reactive clients are connected,
|
|
371
|
+
- no HTTP request is in flight,
|
|
372
|
+
- no workflow is actively **executing** — a *suspended/sleeping* workflow is durable in the database and resumes on wake, so it does **not** keep the app awake,
|
|
373
|
+
- no wakeup is due within the lookahead horizon.
|
|
374
|
+
|
|
375
|
+
A long-sleeping workflow (`sleep('3 days')`) is exactly the case dormancy is built for: the app stops, and the orchestrator brings a fresh process up when the sleep is due — the workflow resumes from its journal.
|
|
376
|
+
|
|
377
|
+
## Tiers (Voltro Cloud — coming soon)
|
|
378
|
+
|
|
379
|
+
Managed cloud hosting is not yet available; the Pro / Enterprise rows below are the planned managed tiers. Today you self-host and run `voltro dormancy` yourself.
|
|
380
|
+
|
|
381
|
+
| Tier | Behavior |
|
|
382
|
+
| --- | --- |
|
|
383
|
+
| Free / Self-Host (today) | aggressive sleep — `voltro dormancy`, honest cold-start on first hit per idle period; idle ≈ \$0 |
|
|
384
|
+
| Pro (coming soon) | keep-warm — sub-second wake via the managed warm-pool/snapshot adapter |
|
|
385
|
+
| Enterprise (coming soon) | always-on / dedicated — never scales down |
|
|
386
|
+
|
|
387
|
+
On Voltro Cloud (when it ships) the wake adapter and grace values will be chosen for you per tier; on self-host you run `voltro dormancy` and tune the knobs yourself. The runtime is identical in every case.
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
<!-- source: en/deployment/serverless-functions.md -->
|
|
394
|
+
## Serverless functions
|
|
395
|
+
|
|
396
|
+
_Run isolated, independently-scaled work as *.serverless.ts functions — self-hosted (Node) or offloaded to Cloudflare Workers / Scaleway._
|
|
397
|
+
|
|
398
|
+
A **`*.serverless.ts`** function is a self-contained unit run + deployed SEPARATELY
|
|
399
|
+
from your api. It's written ONCE, Effect-first and schema-typed; the framework runs
|
|
400
|
+
it on your own infra (`node`) or adapts it to an edge host's entrypoint
|
|
401
|
+
(`fetch(request, env, ctx)` on Cloudflare Workers, `handle(event)` on Scaleway) —
|
|
402
|
+
you never write platform boilerplate.
|
|
403
|
+
|
|
404
|
+
## First: do you actually need one?
|
|
405
|
+
|
|
406
|
+
For an always-on api, **reach for [`defineAction`](/docs/data/actions) first.** An
|
|
407
|
+
action runs in-process and shares the rpc schema, session/tenant resolution, the
|
|
408
|
+
DB handle, typed errors, CDC, tracing, and the WebSocket transport — all of which a
|
|
409
|
+
serverless function THROWS AWAY (it gets only `HttpClient` + `ctx.env`: no DB, no
|
|
410
|
+
session, no tracing). A serverless function is the right tool only when you
|
|
411
|
+
specifically want one of:
|
|
412
|
+
|
|
413
|
+
- **process isolation** — a heavy/risky dependency (native module, fat ML lib,
|
|
414
|
+
untrusted code) you don't want in the api's memory or crash domain;
|
|
415
|
+
- **independent scaling** — a spiky endpoint scaled (or scaled-to-zero) separately
|
|
416
|
+
from the always-on api;
|
|
417
|
+
- **independent release cadence** — deploy/restart that one unit without bouncing
|
|
418
|
+
the api and its live WebSocket connections.
|
|
419
|
+
|
|
420
|
+
If none of those apply, write an action. The rest of this page assumes one does.
|
|
421
|
+
|
|
422
|
+
## Writing a function
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
// api/functions/resizeImage.serverless.ts
|
|
426
|
+
import { Effect, Schema } from 'effect'
|
|
427
|
+
import { HttpClient } from '@effect/platform'
|
|
428
|
+
import { defineServerless } from '@voltro/serverless'
|
|
429
|
+
|
|
430
|
+
export default defineServerless({
|
|
431
|
+
name: 'resize-image', // kebab-case, DNS-safe — the deployed id
|
|
432
|
+
method: 'POST', // default POST; GET reads input from the query string
|
|
433
|
+
input: Schema.Struct({ url: Schema.String, width: Schema.Number }),
|
|
434
|
+
output: Schema.Struct({ resized: Schema.String }),
|
|
435
|
+
runtime: { memoryMb: 512, timeoutSeconds: 30, region: 'fr-par' }, // best-effort host hints
|
|
436
|
+
handler: ({ url, width }, ctx) =>
|
|
437
|
+
Effect.gen(function* () {
|
|
438
|
+
const http = yield* HttpClient.HttpClient // provided by the framework base layer
|
|
439
|
+
// ctx.env.SOME_SECRET → the host's env / secret bindings
|
|
440
|
+
// ctx.waitUntil(p) → background work that outlives the response (Cloudflare)
|
|
441
|
+
return { resized: `${url}?w=${width}` }
|
|
442
|
+
}),
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- **One default export per file** — a `defineServerless({ … })`.
|
|
447
|
+
- `input` decodes the request (JSON body, or the query string for a `GET`).
|
|
448
|
+
`output` encodes the JSON response. A malformed input returns **400**.
|
|
449
|
+
- The handler is Effect-first and may `yield* HttpClient.HttpClient` — the **only**
|
|
450
|
+
service the base layer provides. A serverless function is standalone: it has no
|
|
451
|
+
app database, plugins, or session. If your work needs those, it belongs in the
|
|
452
|
+
api as a mutation/action, not here.
|
|
453
|
+
- To control the HTTP status, fail with `ServerlessHttpError({ status, message })`;
|
|
454
|
+
any other failure becomes a **500**.
|
|
455
|
+
|
|
456
|
+
> **Start from a template.** The [`edge-functions`](/docs/reference/templates)
|
|
457
|
+
> template ships eight runnable `*.serverless.ts` examples across the common
|
|
458
|
+
> shapes (pure compute, geo from request headers, outbound HTTP, Web Crypto HMAC,
|
|
459
|
+
> an LLM call, status-controlled errors). For the static-site-plus-form combo,
|
|
460
|
+
> [`frontend-contact`](/docs/reference/templates) wires a static page's island
|
|
461
|
+
> form to a serverless email function. `voltro add-app fns --template
|
|
462
|
+
> edge-functions` drops the library into any project.
|
|
463
|
+
|
|
464
|
+
## Develop locally
|
|
465
|
+
|
|
466
|
+
Run one function on a real http port — same decode → run → encode the deployed
|
|
467
|
+
function uses:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
voltro serverless dev api/functions/resizeImage.serverless.ts --port 8910
|
|
471
|
+
# POST http://localhost:8910/ with a JSON body matching `input`
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Environment comes from `process.env` (load a `.env` with `node --env-file` if you
|
|
475
|
+
like). `voltro serverless list` shows every discovered function.
|
|
476
|
+
|
|
477
|
+
**CORS works locally.** `voltro serverless dev` / `serve` send
|
|
478
|
+
`Access-Control-Allow-Origin` and answer the OPTIONS preflight — the same headers
|
|
479
|
+
Cloudflare / Scaleway add at the edge. So a browser form on a static dev site
|
|
480
|
+
(`localhost:5190`) can POST to the function (`localhost:8910`) cross-origin out of
|
|
481
|
+
the box. The default is permissive (`*`); lock it down via the `cors` option on
|
|
482
|
+
`serveServerless` when you self-host.
|
|
483
|
+
|
|
484
|
+
## Self-hosted (default) — run it on your own infra
|
|
485
|
+
|
|
486
|
+
The default target is **`node`** — no vendor, no cloud. Two shapes:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
# Run ALL functions in ONE process — the cheap "one sidecar" case.
|
|
490
|
+
# Drop it behind your baseline's reverse proxy (Caddy/nginx) next to the api.
|
|
491
|
+
voltro serverless serve --port 8910 --host 0.0.0.0
|
|
492
|
+
# ▸ each function mounts at its `path` (or /<name> when several share /)
|
|
493
|
+
# ▸ GET /internal/liveness + /internal/readiness probes, like `voltro start`
|
|
494
|
+
|
|
495
|
+
# OR build a standalone per-function kit (process isolation / independent scaling):
|
|
496
|
+
voltro serverless build --target node # → index.mjs + Dockerfile + README (a complete kit)
|
|
497
|
+
# then: node index.mjs (PORT from env) — or `docker build` the emitted Dockerfile
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`build --target node` produces a COMPLETE, self-hostable folder: the bundled
|
|
501
|
+
`index.mjs` server **plus a `Dockerfile` and a `README`** with the exact run /
|
|
502
|
+
docker / compose / reverse-proxy commands — drop it on any box with Node, or any
|
|
503
|
+
container platform. (`deploy --target node` is the same, with the run plan
|
|
504
|
+
printed.)
|
|
505
|
+
|
|
506
|
+
`serve` is the everyday self-hosted runner; `build --target node` is the
|
|
507
|
+
per-function kit you scale on its own. Either way the function runs the SAME
|
|
508
|
+
`serverlessWebHandler` it would on the edge.
|
|
509
|
+
|
|
510
|
+
## Edge offload (optional) — Cloudflare / Scaleway
|
|
511
|
+
|
|
512
|
+
To save money on spiky or globally-distributed work, push a function to an edge
|
|
513
|
+
host instead. The framework owns the bundle (esbuild) and the platform entry; the
|
|
514
|
+
host's official CLI does only the upload, so those CLIs must be on PATH +
|
|
515
|
+
authenticated.
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
# Cloudflare Workers — needs `wrangler` + CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
|
|
519
|
+
voltro serverless deploy --target cloudflare
|
|
520
|
+
voltro serverless deploy --target cloudflare --node-compat # add nodejs_compat for node:* builtins
|
|
521
|
+
|
|
522
|
+
# Scaleway Functions — needs `scw` + SCW_ACCESS_KEY + SCW_SECRET_KEY + a namespace
|
|
523
|
+
voltro serverless deploy --target scaleway --namespace-id <ns-id> --runtime node22
|
|
524
|
+
|
|
525
|
+
# Inspect the plan without uploading:
|
|
526
|
+
voltro serverless deploy --target cloudflare --dry-run
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Build output lands in `.voltro/serverless/<target>/<name>/` — add `.voltro/` to
|
|
530
|
+
your `.gitignore`.
|
|
531
|
+
|
|
532
|
+
## How the three targets differ
|
|
533
|
+
|
|
534
|
+
| Aspect | Node (self-hosted) | Cloudflare Workers | Scaleway Functions |
|
|
535
|
+
|---|---|---|---|
|
|
536
|
+
| Where it runs | your own infra | V8 isolate (web-standard, **not** Node) | Scaleway's Node.js (node20 / node22) |
|
|
537
|
+
| `node:*` builtins | native | only with `--node-compat` | native |
|
|
538
|
+
| Env / secrets | `process.env` | the `env` binding | `process.env` (`scw` args) |
|
|
539
|
+
| `ctx.waitUntil` | best-effort | real (after the response) | best-effort |
|
|
540
|
+
| Scaling | you (compose replicas / helm) | isolate, near-zero cold start | scale-to-zero, real cold starts (`min-scale ≥ 1` keeps warm) |
|
|
541
|
+
| Runs the whole set | `serve` (one process) | one Worker per function | one function per namespace entry |
|
|
542
|
+
|
|
543
|
+
Your handler code is identical across all three — only the target changes.
|
|
544
|
+
|
|
545
|
+
## Notes
|
|
546
|
+
|
|
547
|
+
- **Effect on the edge:** the framework pins `effect ≥ 3.20.0`, which carries the
|
|
548
|
+
fix for cross-request context isolation under concurrency. Don't pin an older
|
|
549
|
+
`effect` for a function you deploy to Workers.
|
|
550
|
+
- **Browser/edge safety:** a Worker bundle must not pull `node:*` unless you pass
|
|
551
|
+
`--node-compat`. Keep function dependencies edge-safe, or deploy to Scaleway.
|
|
552
|
+
- This is distinct from [scale-to-zero](/docs/deployment/scale-to-zero), which
|
|
553
|
+
idles your WHOLE api on a single node. Serverless functions split INDIVIDUAL
|
|
554
|
+
units off to a per-invocation host.
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
<!-- source: en/deployment/static-sites.md -->
|
|
561
|
+
## Static-site deploy
|
|
562
|
+
|
|
563
|
+
_Ship the static dist/ from `voltro build` to a cheap CDN (Cloudflare Pages, any S3-compatible bucket, Netlify) instead of serving it off the app server._
|
|
564
|
+
|
|
565
|
+
`voltro build` emits a static `dist/` — pre-rendered HTML plus content-hashed
|
|
566
|
+
assets. Serving that off your app server wastes the server on work a CDN does for
|
|
567
|
+
pennies. `voltro static` ships `dist/` to a cheap external host, off the app
|
|
568
|
+
server, so you save money at the edge.
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
voltro static hosts # list supported hosts
|
|
572
|
+
voltro static deploy --host cloudflare-pages --project-name my-site --spa
|
|
573
|
+
voltro static deploy --host s3 --bucket my-bucket --endpoint https://s3.fr-par.scw.cloud
|
|
574
|
+
voltro static deploy --host netlify --site-id <site-id>
|
|
575
|
+
voltro static deploy --host cloudflare-pages --project-name my-site --dry-run
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The framework owns the policy that's easy to get wrong; the host's official tool
|
|
579
|
+
does the transfer (so it must be on PATH and authenticated).
|
|
580
|
+
|
|
581
|
+
> **Static templates to start from:** [`frontend-static-blog`](/docs/reference/templates)
|
|
582
|
+
> (SSG from a content source via `getStaticPaths` + islands),
|
|
583
|
+
> [`frontend-landing`](/docs/reference/templates) (zero-JS marketing),
|
|
584
|
+
> [`frontend-spa`](/docs/reference/templates) (client-rendered, `--spa`), and
|
|
585
|
+
> [`frontend-contact`](/docs/reference/templates) (a static page + a serverless
|
|
586
|
+
> form — the page deploys here, the function via `voltro serverless`).
|
|
587
|
+
|
|
588
|
+
## Self-hosting already serves `dist/`
|
|
589
|
+
|
|
590
|
+
You don't NEED `voltro static`. When you [self-host](/docs/deployment/self-hosting),
|
|
591
|
+
`voltro start` serves `dist/` end-to-end (static HTML + SSR/ISR on demand), and
|
|
592
|
+
your baseline's reverse proxy (Caddy/nginx) can serve the files directly too. So
|
|
593
|
+
`voltro static` is a pure **cost-offload**: push the immutable assets to a cheap
|
|
594
|
+
CDN while your api + Postgres stay on your own box.
|
|
595
|
+
|
|
596
|
+
The split works because the browser reaches the self-hosted api over its own URL.
|
|
597
|
+
Point the web app's api config at the api's public WebSocket endpoint:
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
// apps/web/app.config.ts
|
|
601
|
+
apis: { app: { package: '@app/api', url: 'wss://api.yourdomain.com/ws' } }
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The api stays self-hosted (it must — WebSocket subscriptions + Postgres are
|
|
605
|
+
always-on); only the static bytes move to the edge. `POST /rpc` forwards the
|
|
606
|
+
session cookie exactly as the WS path, so SSR loaders + auth resolve identically.
|
|
607
|
+
|
|
608
|
+
## What the framework decides for you
|
|
609
|
+
|
|
610
|
+
- **Content-Type** per file, from its extension (including the ones generic tools
|
|
611
|
+
get wrong: `.wasm`, `.woff2`, `.svg`, `.webmanifest`, `.avif`).
|
|
612
|
+
- **Cache-Control:** content-hashed assets (`assets/index-a1b2c3d4.js`) →
|
|
613
|
+
`public, max-age=31536000, immutable`; HTML → revalidate on every request, so a
|
|
614
|
+
redeploy is visible immediately.
|
|
615
|
+
- **SPA fallback** (`--spa`): serve `index.html` for unknown routes — for a
|
|
616
|
+
single-page app. Leave it off for per-route SSG output.
|
|
617
|
+
|
|
618
|
+
For hosts that read them (Cloudflare Pages, Netlify) this is emitted as
|
|
619
|
+
`_headers` / `_redirects`. **If you already ship your own `_headers` or
|
|
620
|
+
`_redirects` in `dist/`, the framework leaves them untouched** — your intent wins.
|
|
621
|
+
|
|
622
|
+
## The render-mode gate
|
|
623
|
+
|
|
624
|
+
A pure static CDN can only serve pre-rendered HTML. So before it uploads,
|
|
625
|
+
`voltro static deploy` checks the app's render-mode profile and **blocks** if the
|
|
626
|
+
app isn't static-safe:
|
|
627
|
+
|
|
628
|
+
- **`ssr` / `isr` pages** — these need a runtime to render per request; a CDN
|
|
629
|
+
can't run them.
|
|
630
|
+
- **dynamic routes without `getStaticPaths`** — the build emits no artifact for
|
|
631
|
+
them, so they'd be 404 on a CDN.
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
voltro static deploy --host cloudflare-pages --project-name my-site
|
|
635
|
+
# → voltro static deploy: "." is NOT pure-static —
|
|
636
|
+
# • 1 ssr page(s): need a runtime; a CDN can't render them
|
|
637
|
+
# → Serve those on a runtime (`voltro start` / a container), OR add
|
|
638
|
+
# getStaticPaths / set the pages to `static` / `spa`. To ship anyway
|
|
639
|
+
# (ssr/isr become client-rendered), pass --allow-dynamic.
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Three ways forward:
|
|
643
|
+
|
|
644
|
+
1. **Keep it static** — give dynamic routes a `getStaticPaths`, set pages to
|
|
645
|
+
`static` / `spa`. Then the deploy passes.
|
|
646
|
+
2. **Ship anyway** — `--allow-dynamic`. The ssr/isr pages become client-rendered
|
|
647
|
+
(no server first-paint); fine if they degrade gracefully.
|
|
648
|
+
3. **Use a runtime** — if the app genuinely needs SSR/ISR, it belongs on `voltro
|
|
649
|
+
start` / a container, not a pure CDN. See
|
|
650
|
+
[self-hosting](/docs/deployment/self-hosting). `voltro deploy plan` tells you,
|
|
651
|
+
per app, which tier it belongs to.
|
|
652
|
+
|
|
653
|
+
`voltro build` records the profile to `dist/.voltro-build.json`, so the gate (and
|
|
654
|
+
the cloud control-plane) classify without re-scanning. Point the scan at a
|
|
655
|
+
non-default app root with `--app <dir>`.
|
|
656
|
+
|
|
657
|
+
## Hosts
|
|
658
|
+
|
|
659
|
+
| Host | `--host` | Mechanism | Auth |
|
|
660
|
+
|---|---|---|---|
|
|
661
|
+
| Cloudflare Pages | `cloudflare-pages` | `wrangler pages deploy` (built-in hash dedup) | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
|
|
662
|
+
| S3-compatible | `s3` | `aws s3 sync` (two-pass cache) | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` |
|
|
663
|
+
| Netlify | `netlify` | `netlify deploy` (SHA1 digest deploy) | `NETLIFY_AUTH_TOKEN` |
|
|
664
|
+
|
|
665
|
+
### Cloudflare Pages
|
|
666
|
+
|
|
667
|
+
```bash
|
|
668
|
+
# one-time: wrangler pages project create my-site
|
|
669
|
+
voltro static deploy --host cloudflare-pages --project-name my-site --spa
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
The project must exist first. Uploads are incremental (wrangler dedups by hash);
|
|
673
|
+
new deploys auto-invalidate the edge.
|
|
674
|
+
|
|
675
|
+
### S3-compatible (AWS, Scaleway, R2, B2, MinIO)
|
|
676
|
+
|
|
677
|
+
One code path for every S3-compatible bucket — only the endpoint and keys change.
|
|
678
|
+
Omit `--endpoint` for AWS; set it for the others.
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Scaleway Object Storage (has native static-website hosting)
|
|
682
|
+
AWS_ACCESS_KEY_ID=<scw-access> AWS_SECRET_ACCESS_KEY=<scw-secret> \
|
|
683
|
+
voltro static deploy --host s3 --bucket my-bucket \
|
|
684
|
+
--endpoint https://s3.fr-par.scw.cloud \
|
|
685
|
+
--website-url https://my-bucket.s3-website.fr-par.scw.cloud
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
The sync runs two passes: immutable hashed assets first, then HTML with
|
|
689
|
+
`no-cache`, and `--delete` prunes removed files. SPA fallback for a raw bucket is
|
|
690
|
+
bucket-website / CDN configuration (and R2/B2/MinIO need a router in front
|
|
691
|
+
entirely) — `--spa` warns rather than silently doing nothing here.
|
|
692
|
+
|
|
693
|
+
### Netlify
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
voltro static deploy --host netlify --site-id <site-id> --spa
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Netlify infers content types server-side and its digest-based deploy is
|
|
700
|
+
incremental and atomic.
|
|
701
|
+
|
|
702
|
+
## CI
|
|
703
|
+
|
|
704
|
+
All three host CLIs run headless with a token in the environment — drop the
|
|
705
|
+
deploy command into your pipeline after `voltro build`. Use `--dry-run` to print
|
|
706
|
+
the exact plan in a PR check without uploading.
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
<!-- source: en/deployment/production-hardening.md -->
|
|
713
|
+
## Production hardening
|
|
714
|
+
|
|
715
|
+
_The production checklist — session secret, health probes, request limits, tenant isolation, observability, graceful shutdown, and multi-replica config._
|
|
716
|
+
|
|
717
|
+
The defaults are tuned for `voltro dev`, where fast iteration wins. Before you point real traffic at `voltro serve`, walk this checklist — most items are a single env var or a one-line config, but skipping them is how a service leaks across tenants, forges sessions, or falls over under load.
|
|
718
|
+
|
|
719
|
+
## 1. Session secret (REQUIRED)
|
|
720
|
+
|
|
721
|
+
In production (`NODE_ENV=production`) `voltro serve` **refuses to boot** unless `VOLTRO_SESSION_SECRET` is set to a real value. It rejects three cases explicitly:
|
|
722
|
+
|
|
723
|
+
- a **missing** value,
|
|
724
|
+
- the built-in **public dev fallback** (it is committed in the framework source),
|
|
725
|
+
- any value **shorter than 32 chars** — which catches placeholders like `changeme`.
|
|
726
|
+
|
|
727
|
+
Generate a real one:
|
|
728
|
+
|
|
729
|
+
```sh
|
|
730
|
+
voltro secret generate session
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Store it in your secrets manager and inject it as an env var — never commit it. Why the hard fail: a forgotten secret would otherwise sign session cookies with a key that is **public in the framework source**, letting anyone forge any session.
|
|
734
|
+
|
|
735
|
+
**Rotation** is zero-downtime. Set the new value as `VOLTRO_SESSION_SECRET` and move the old one to `VOLTRO_SESSION_SECRET_PREVIOUS`:
|
|
736
|
+
|
|
737
|
+
```sh
|
|
738
|
+
VOLTRO_SESSION_SECRET=<new> # signs new cookies
|
|
739
|
+
VOLTRO_SESSION_SECRET_PREVIOUS=<old> # still verifies live cookies
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Keep `_PREVIOUS` in place for one session-TTL window, then drop it. Existing cookies verify against `previous` until they naturally expire — no live session is invalidated.
|
|
743
|
+
|
|
744
|
+
## 2. Kubernetes health probes
|
|
745
|
+
|
|
746
|
+
`voltro serve` exposes two unauthenticated endpoints, both handled **before** any rate-limit interceptor:
|
|
747
|
+
|
|
748
|
+
- `GET /internal/liveness` — always `200 ok`. The process is up.
|
|
749
|
+
- `GET /internal/readiness` — `200 ready` only after full boot, `503` before.
|
|
750
|
+
|
|
751
|
+
Readiness **also** runs a DB ping (`SELECT 1`) on SQL stores. So a pod whose connection pool has died reports `503` and is pulled from the Service endpoints — instead of staying in rotation and erroring every request.
|
|
752
|
+
|
|
753
|
+
```yaml
|
|
754
|
+
# k8s Deployment — probes
|
|
755
|
+
livenessProbe:
|
|
756
|
+
httpGet:
|
|
757
|
+
path: /internal/liveness
|
|
758
|
+
port: 4000
|
|
759
|
+
periodSeconds: 10
|
|
760
|
+
readinessProbe:
|
|
761
|
+
httpGet:
|
|
762
|
+
path: /internal/readiness
|
|
763
|
+
port: 4000
|
|
764
|
+
periodSeconds: 5
|
|
765
|
+
failureThreshold: 3
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
> **Run `voltro serve` in serving pods — not `voltro dev`.** `voltro dev` is the
|
|
769
|
+
> local-iteration supervisor: file-watch, respawn, codegen, and a boot-time
|
|
770
|
+
> auto-migrate that introspects the whole schema. It does **not** expose the
|
|
771
|
+
> probes above and binds its port only after that boot work finishes — so a TCP
|
|
772
|
+
> probe can't tell "still booting" from "dead", and a large-schema migrate can
|
|
773
|
+
> blow past a fixed startup window and get a healthy pod killed. Production pods
|
|
774
|
+
> run `voltro serve`.
|
|
775
|
+
|
|
776
|
+
## 3. Schema migration at deploy (don't migrate in the serving pod)
|
|
777
|
+
|
|
778
|
+
`voltro serve` does **not** auto-migrate — schema changes go through an explicit
|
|
779
|
+
deploy step, never on the serving pod's boot. The full contract (per-env
|
|
780
|
+
fingerprint check, refuse-to-boot on mismatch, apply timing relative to the image
|
|
781
|
+
swap) is [Prod pipeline](/docs/database/migrations/prod-pipeline); the k8s wiring
|
|
782
|
+
is here.
|
|
783
|
+
|
|
784
|
+
Run the apply as a **pre-deploy Job** (or `initContainer`) that holds the
|
|
785
|
+
migration credentials and executes `voltro db apply`. It re-diffs the deployed
|
|
786
|
+
code's declared schema against the live DB and applies the resulting plan — so
|
|
787
|
+
it's a clean no-op on an already-current DB, which makes it safe to re-run and to
|
|
788
|
+
run in every pod of a stateless deploy. A bare apply **refuses under
|
|
789
|
+
`NODE_ENV=production`** by design (auto-apply on prod is not allowed), so the Job
|
|
790
|
+
runs with `NODE_ENV` unset or `staging`; the serving pods keep
|
|
791
|
+
`NODE_ENV=production`:
|
|
792
|
+
|
|
793
|
+
```yaml
|
|
794
|
+
# Helm pre-install/pre-upgrade Job — runs to completion BEFORE the new pods roll.
|
|
795
|
+
apiVersion: batch/v1
|
|
796
|
+
kind: Job
|
|
797
|
+
metadata:
|
|
798
|
+
name: myapp-migrate
|
|
799
|
+
annotations:
|
|
800
|
+
"helm.sh/hook": pre-install,pre-upgrade
|
|
801
|
+
"helm.sh/hook-weight": "-5"
|
|
802
|
+
spec:
|
|
803
|
+
template:
|
|
804
|
+
spec:
|
|
805
|
+
restartPolicy: Never
|
|
806
|
+
containers:
|
|
807
|
+
- name: migrate
|
|
808
|
+
image: myapp:{{ .Values.image.tag }}
|
|
809
|
+
command: ["voltro", "db", "apply", "--note", "release {{ .Values.image.tag }}"]
|
|
810
|
+
env:
|
|
811
|
+
- { name: NODE_ENV, value: "staging" } # bare apply is refused under "production"
|
|
812
|
+
# …plus the same DB_* connection env as the serving pods
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
```yaml
|
|
816
|
+
# Serving Deployment — auto-migrate OFF; the Job already applied the schema.
|
|
817
|
+
env:
|
|
818
|
+
- name: VOLTRO_AUTO_MIGRATE
|
|
819
|
+
value: "0"
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
For the reviewed-exact-diff flow — generate `voltro db plan --json > plan.json`
|
|
823
|
+
in CI, apply it with `voltro db apply --plan plan.json` — see
|
|
824
|
+
[Prod pipeline](/docs/database/migrations/prod-pipeline). That variant IS allowed
|
|
825
|
+
under `NODE_ENV=production` (it refuses unless both fingerprints still match the
|
|
826
|
+
reviewed plan). A Job runs **once per release** vs an `initContainer`'s once per
|
|
827
|
+
replica, so it's the better fit for a multi-replica rollout.
|
|
828
|
+
|
|
829
|
+
### If you DO run `voltro dev` in a cluster (dev / staging only)
|
|
830
|
+
|
|
831
|
+
`voltro dev` binds a small **boot-health surface** on its own port so a probe can
|
|
832
|
+
watch the slow boot (codegen + migrate) it otherwise couldn't see:
|
|
833
|
+
|
|
834
|
+
- `GET /internal/liveness` → `200 ok` from the moment the process is up, through
|
|
835
|
+
the whole boot. Point `startupProbe` **and** `livenessProbe` here so migration
|
|
836
|
+
time counts as *alive*, not dead.
|
|
837
|
+
- `GET /internal/readiness` → `503` until the app port is serving, then `200`.
|
|
838
|
+
- `GET /internal/startup` → `200` JSON `{ phase, ready, tablesTotal, elapsedMs }`
|
|
839
|
+
(`phase` is `booting` → `migrating` → `ready`, or `error` with a message) — for
|
|
840
|
+
humans and dashboards watching progress.
|
|
841
|
+
|
|
842
|
+
The port defaults to **app port + 1**; override with `VOLTRO_DEV_HEALTH_PORT`
|
|
843
|
+
(set `0` to disable). A generous `startupProbe.failureThreshold` on
|
|
844
|
+
`/internal/liveness` then gives a large-schema first migrate minutes instead of a
|
|
845
|
+
fixed TCP window:
|
|
846
|
+
|
|
847
|
+
```yaml
|
|
848
|
+
# dev/staging pod running `voltro dev` — probe the boot-health port (4001)
|
|
849
|
+
startupProbe:
|
|
850
|
+
httpGet: { path: /internal/liveness, port: 4001 }
|
|
851
|
+
periodSeconds: 10
|
|
852
|
+
failureThreshold: 60 # up to 10 minutes for a first cold migrate
|
|
853
|
+
livenessProbe:
|
|
854
|
+
httpGet: { path: /internal/liveness, port: 4001 }
|
|
855
|
+
readinessProbe:
|
|
856
|
+
httpGet: { path: /internal/readiness, port: 4001 }
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
Even a local `voltro dev` boots faster on reboots now: the auto-migrate skips the
|
|
860
|
+
full introspect when the declared schema is unchanged (a fingerprint check — one
|
|
861
|
+
indexed query instead of scanning every table). Force a full re-introspect with
|
|
862
|
+
`VOLTRO_MIGRATE_FORCE=1`.
|
|
863
|
+
|
|
864
|
+
## 4. Request limits & DoS
|
|
865
|
+
|
|
866
|
+
`POST /rpc` (the buffered JSON endpoint the SSR loaders use) rejects an over-`Content-Length` body with **413** *before* buffering it into memory:
|
|
867
|
+
|
|
868
|
+
```sh
|
|
869
|
+
VOLTRO_MAX_RPC_BODY_BYTES=8388608 # default 8 MiB
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
Scope: this guards the `/rpc` JSON path only. File uploads ride separate storage routes with their own `limits.maxBytes`, and WebSocket frames are capped by the `ws` library default (100 MiB).
|
|
873
|
+
|
|
874
|
+
**Put per-IP rate limiting and the primary body-size cap at the ingress** — that is the correct layer: it holds per-IP state and works across replicas, which an in-process limit can't.
|
|
875
|
+
|
|
876
|
+
```yaml
|
|
877
|
+
# nginx ingress — annotations on the Ingress resource
|
|
878
|
+
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
|
|
879
|
+
nginx.ingress.kubernetes.io/limit-rps: "20"
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
For **app-level** throttling (per-subject / per-tenant, e.g. an expensive action), use `@voltro/plugin-ratelimit` and the plugin `onHttpRequest` interceptor seam. It complements the ingress cap — it does not replace it.
|
|
883
|
+
|
|
884
|
+
## 5. Multi-tenant isolation
|
|
885
|
+
|
|
886
|
+
Tables carrying the `tenant()` mixin are auto-scoped to the request's tenant — nothing more to do there. The gap is the **anonymous** request that matches no auth strategy: by default it resolves to a tenant-less anonymous Subject that can read any non-`tenant()` table across the DB.
|
|
887
|
+
|
|
888
|
+
For an app where **every** request must be tenant-scoped (no anonymous public data), close that door:
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
// app.config.ts
|
|
892
|
+
export default {
|
|
893
|
+
// ...
|
|
894
|
+
auth: {
|
|
895
|
+
anonymousTenantRequired: true,
|
|
896
|
+
},
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
A request that matches no auth strategy **and** sends no `x-tenant` header is then rejected with `Unauthenticated` instead of resolving to a tenant-less Subject. It applies identically under `voltro dev` and `voltro serve`.
|
|
901
|
+
|
|
902
|
+
> **Do NOT enable this if the app serves legitimate anonymous public data** — public read endpoints, reference tables — it would reject those callers. In that case, rely on putting `tenant()` on every private table instead.
|
|
903
|
+
|
|
904
|
+
## 6. Observability (wire it or fly blind)
|
|
905
|
+
|
|
906
|
+
By default nothing is exported. Wire it before you need it.
|
|
907
|
+
|
|
908
|
+
**Traces + metrics** ship to any OTLP collector (Tempo / Jaeger / Honeycomb / Grafana Agent) from one env var:
|
|
909
|
+
|
|
910
|
+
```sh
|
|
911
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
|
|
912
|
+
OTEL_SERVICE_NAME=my-api
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**Structured logs** as JSON:
|
|
916
|
+
|
|
917
|
+
```sh
|
|
918
|
+
VOLTRO_LOG_FORMAT=json
|
|
919
|
+
VOLTRO_LOG_LEVEL=info
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Error reporting** — add `sentryPlugin()` from `@voltro/plugin-sentry`. It stays inert until `SENTRY_DSN` is set, and reported errors correlate to the request `traceId`:
|
|
923
|
+
|
|
924
|
+
```ts
|
|
925
|
+
import { sentryPlugin } from '@voltro/plugin-sentry'
|
|
926
|
+
|
|
927
|
+
export default {
|
|
928
|
+
// ...
|
|
929
|
+
plugins: [sentryPlugin()],
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Durable in-DB traces are **off in production by default** — traces belong in your OTLP backend, not your OLTP database.
|
|
934
|
+
|
|
935
|
+
## 7. Graceful shutdown
|
|
936
|
+
|
|
937
|
+
On `SIGTERM` / `SIGINT`, `voltro serve` shuts down cleanly (exit 0): it stops
|
|
938
|
+
schedulers, detaches subscribers / reactions / aggregates, drains the analytics
|
|
939
|
+
sink, stops accepting new connections while **finishing already-accepted
|
|
940
|
+
requests**, and closes the SQL connection pool **last** (waiting for in-flight
|
|
941
|
+
transactions). Verified under concurrent load — accepted requests complete.
|
|
942
|
+
|
|
943
|
+
**But the app cannot drain a rolling update by itself.** The runtime
|
|
944
|
+
(`NodeRuntime`) owns the `SIGTERM` signal and closes the HTTP listener promptly —
|
|
945
|
+
so a request that *arrives* during shutdown is refused. Failing readiness from
|
|
946
|
+
inside the process doesn't help: the listener is already closing. Draining is a
|
|
947
|
+
**k8s-layer** job, done with a **preStop hook**:
|
|
948
|
+
|
|
949
|
+
```yaml
|
|
950
|
+
spec:
|
|
951
|
+
# Must exceed the preStop sleep + the app's own shutdown.
|
|
952
|
+
terminationGracePeriodSeconds: 30
|
|
953
|
+
containers:
|
|
954
|
+
- name: api
|
|
955
|
+
lifecycle:
|
|
956
|
+
preStop:
|
|
957
|
+
exec:
|
|
958
|
+
# k8s removes the pod from the Service endpoints AND runs this
|
|
959
|
+
# BEFORE sending SIGTERM. The sleep holds the pod up (listener open,
|
|
960
|
+
# finishing in-flight) while endpoint removal propagates — so no new
|
|
961
|
+
# request lands on a pod that's about to close.
|
|
962
|
+
command: ["sh", "-c", "sleep 5"]
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
Without the preStop hook, a rolling update drops the small window of requests
|
|
966
|
+
that route to a terminating pod before k8s finishes removing it from the Service
|
|
967
|
+
endpoints. With it, that window is drained. `terminationGracePeriodSeconds` must
|
|
968
|
+
be larger than the sleep plus the app's own teardown, or k8s SIGKILLs mid-drain.
|
|
969
|
+
|
|
970
|
+
## 8. Multiple replicas
|
|
971
|
+
|
|
972
|
+
Cache and KV default to **in-process** (per-replica). For a shared backend across replicas:
|
|
973
|
+
|
|
974
|
+
```sh
|
|
975
|
+
CACHE_BACKEND=redis
|
|
976
|
+
KV_BACKEND=redis
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Cross-replica reactivity** is the subtle one: a write on one pod never surfaces on another pod's open subscriptions unless the replicas share a change bus. Use:
|
|
980
|
+
|
|
981
|
+
- **postgres** — LISTEN/NOTIFY (built in),
|
|
982
|
+
- **mariadb** — binlog CDC (built in),
|
|
983
|
+
- **other dialects** — `@voltro/plugin-broadcast` (Redis).
|
|
984
|
+
|
|
985
|
+
Schedules and aggregates auto-coordinate via an advisory lock on SQL stores — no extra config to keep them from double-firing across replicas.
|
|
986
|
+
|
|
987
|
+
## Checklist
|
|
988
|
+
|
|
989
|
+
- [ ] `VOLTRO_SESSION_SECRET` set from `voltro secret generate session`, in a secrets manager
|
|
990
|
+
- [ ] Rotation window uses `VOLTRO_SESSION_SECRET_PREVIOUS`
|
|
991
|
+
- [ ] Liveness / readiness probes point at `/internal/liveness` + `/internal/readiness`
|
|
992
|
+
- [ ] Serving pods run `voltro serve` (not `voltro dev`), with `VOLTRO_AUTO_MIGRATE=0`
|
|
993
|
+
- [ ] Schema applied by a pre-deploy Job / initContainer (`voltro db apply`), not in the serving pod
|
|
994
|
+
- [ ] `VOLTRO_MAX_RPC_BODY_BYTES` sane; ingress caps body size + per-IP rate
|
|
995
|
+
- [ ] `auth.anonymousTenantRequired: true` (unless the app serves anonymous public data)
|
|
996
|
+
- [ ] `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME` pointed at your collector
|
|
997
|
+
- [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
|
|
998
|
+
- [ ] `terminationGracePeriodSeconds` generous for graceful drain
|
|
999
|
+
- [ ] `CACHE_BACKEND` / `KV_BACKEND` + a cross-replica change bus when running >1 replica
|