@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,666 @@
|
|
|
1
|
+
# Scheduling
|
|
2
|
+
|
|
3
|
+
> Deployment-agnostic scheduled jobs in Voltro — one *.cron.tsx definition that runs unchanged on a single box, a multi-instance fleet, or an external scheduler.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- source: en/scheduling/overview.md -->
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
_Deployment-agnostic scheduled jobs in Voltro — one *.cron.tsx definition that runs unchanged on a single box, a multi-instance fleet, or an external scheduler._
|
|
13
|
+
|
|
14
|
+
A **schedule** is a job the clock invokes — "send the digest at 09:00", "prune sessions every 15 minutes". In Voltro a schedule lives in a `*.cron.tsx` file and default-exports `defineSchedule({...})`.
|
|
15
|
+
|
|
16
|
+
The defining idea: **the job definition is identical across every hosting topology.** The cron expression, timezone, and handler never change whether you run one PM2 process, ten Kubernetes replicas, or hand the firing off to AWS EventBridge. What differs — *who* fires it and *how* exactly-once is guaranteed — is configuration, not code.
|
|
17
|
+
|
|
18
|
+
```tsx no-check
|
|
19
|
+
// apps/api/schedules/digest.cron.tsx
|
|
20
|
+
import { defineSchedule } from '@voltro/runtime'
|
|
21
|
+
|
|
22
|
+
export default defineSchedule({
|
|
23
|
+
name: 'dailyDigest',
|
|
24
|
+
cron: '0 9 * * *', // 09:00, five-field cron
|
|
25
|
+
timezone: 'Europe/Berlin', // REQUIRED — never server-local
|
|
26
|
+
handler: async ({ app }) => {
|
|
27
|
+
const users = await app.store.users.where({ digestOptIn: true }).all()
|
|
28
|
+
for (const u of users) await sendDigest(app, u)
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's the whole job. Drop the file in your api app, run `voltro dev`, and it fires at 09:00 Berlin time.
|
|
34
|
+
|
|
35
|
+
## What's in this section
|
|
36
|
+
|
|
37
|
+
- [Defining a schedule](/docs/scheduling/definition) — `defineSchedule`, cron syntax, the timezone rule, the handler context
|
|
38
|
+
- [Coordination](/docs/scheduling/coordination) — `single` / `advisoryLock` / `cluster`: exactly-once across instances
|
|
39
|
+
- [Trigger drivers](/docs/scheduling/triggers) — `self` (in-app timer) vs `external` (platform scheduler hits an HTTP endpoint)
|
|
40
|
+
- [Overlap & backfill](/docs/scheduling/overlap-and-backfill) — what happens on slow runs and missed firings
|
|
41
|
+
- [Deployment](/docs/scheduling/deployment) — generating Kubernetes / AWS / GCP / Azure manifests with `voltro schedule-manifest`
|
|
42
|
+
- [Dashboard](/docs/scheduling/dashboard) — firing history, status, and "Run now"
|
|
43
|
+
|
|
44
|
+
## Schedule vs workflow
|
|
45
|
+
|
|
46
|
+
Schedules and [workflows](/docs/workflows/overview) are both durable execution, but they answer different questions.
|
|
47
|
+
|
|
48
|
+
| You have… | Use |
|
|
49
|
+
|---|---|
|
|
50
|
+
| A job the clock starts on a recurring cadence | **Schedule** |
|
|
51
|
+
| A multi-step job that must survive a deploy mid-flight | [Workflow](/docs/workflows/overview) |
|
|
52
|
+
| "Run this daily, and each run is one quick operation" | Schedule |
|
|
53
|
+
| "Run this daily, and each run is a long multi-step saga" | Schedule with a direct **workflow target** or a handler that starts one |
|
|
54
|
+
| A user-facing request returning in <100ms | [Mutation](/docs/data/mutations) |
|
|
55
|
+
|
|
56
|
+
The wedge: **schedules answer *when*, workflows answer *how durably*.** A schedule handler is "a mutation the clock invokes" — same `app` context, same store, same plugin interceptors. If the handler's work itself needs to survive crashes, the handler kicks off a workflow and returns.
|
|
57
|
+
|
|
58
|
+
That handoff works the same in `voltro dev` and the production API server: when the app has workflow files, schedule handlers receive `app.workflows` and can start durable work without importing a client.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// apps/api/schedules/daily-rollup.cron.tsx
|
|
62
|
+
import { defineSchedule } from '@voltro/runtime'
|
|
63
|
+
|
|
64
|
+
export default defineSchedule({
|
|
65
|
+
name: 'dailyRollup',
|
|
66
|
+
cron: '0 2 * * *',
|
|
67
|
+
timezone: 'UTC',
|
|
68
|
+
handler: async ({ app, scheduledAt }) => {
|
|
69
|
+
await app.workflows!.start('billing.dailyRollup', {
|
|
70
|
+
day: scheduledAt.toISOString().slice(0, 10),
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> Earlier framework guidance suggested modelling cron with a workflow plus `Effect.sleep`. That still works for one-off durable delays, but recurring cadences belong in a `*.cron.tsx` schedule — you get coordination, backfill, overlap control, and a dashboard for free.
|
|
77
|
+
|
|
78
|
+
## The three things you configure
|
|
79
|
+
|
|
80
|
+
1. **The job** — `defineSchedule({ cron, timezone, handler })`. Lives in the file. Topology-independent.
|
|
81
|
+
2. **The trigger** — who drives the clock: an in-app timer (`self`) or an outside scheduler (`external`). App-wide default, overridable per job. See [trigger drivers](/docs/scheduling/triggers).
|
|
82
|
+
3. **The coordination** — how N instances avoid double-firing: `single`, `advisoryLock`, or `cluster`. App-wide. See [coordination](/docs/scheduling/coordination).
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// apps/api/app.config.ts
|
|
86
|
+
export default {
|
|
87
|
+
type: 'api' as const,
|
|
88
|
+
name: 'api',
|
|
89
|
+
scheduling: {
|
|
90
|
+
trigger: 'self', // default; 'external' to delegate the clock
|
|
91
|
+
coordination: 'advisoryLock', // default on every multi-instance SQL store; 'single' on memory/sqlite
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Sensible defaults derive from your store: **`memory` / `sqlite` → `self` / `single`**, **every multi-instance SQL store (`postgres` / `mariadb` / `mysql` / `mssql`) → `self` / `advisoryLock`** (the claim-row gate works on all of them). Most apps never set this block.
|
|
97
|
+
|
|
98
|
+
## Guarantees and honest caveats
|
|
99
|
+
|
|
100
|
+
- **Deterministic firing instant.** `scheduledAt` is derived from the cron expression, not each replica's `Date.now()`, so every instance computes the same time-bucket — the basis for clock-skew-safe coordination.
|
|
101
|
+
- **Non-dying timer.** The `self` driver re-arms after every firing, even if a handler throws; a handler defect can't break the chain.
|
|
102
|
+
- **Exactly-once is only as strong as your coordination.** `single` does not dedupe — running two `single` instances double-fires. Use `advisoryLock` or `cluster` for multi-instance. See [coordination](/docs/scheduling/coordination).
|
|
103
|
+
- **Sub-minute `cluster` firings lag.** The `cluster` strategy waits for shard assignment (~10s on a cold runner) before the first firing. Fine for minute-and-up cadences; not for "every second".
|
|
104
|
+
- **External triggers are minute-granular.** Six-field (seconds) cron is rejected when generating external manifests — k8s/EventBridge/Cloud Scheduler can't express sub-minute. See [deployment](/docs/scheduling/deployment).
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
<!-- source: en/scheduling/definition.md -->
|
|
111
|
+
## Defining a schedule
|
|
112
|
+
|
|
113
|
+
_defineSchedule — the *.cron.tsx file convention, cron expression syntax, the mandatory timezone, and the handler context._
|
|
114
|
+
|
|
115
|
+
A schedule is a `*.cron.tsx` file that default-exports `defineSchedule({...})`. The CLI discovers it the same way it discovers queries and mutations — no registration, no manifest.
|
|
116
|
+
|
|
117
|
+
```tsx no-check
|
|
118
|
+
// apps/api/schedules/cleanup.cron.tsx
|
|
119
|
+
import { defineSchedule } from '@voltro/runtime'
|
|
120
|
+
|
|
121
|
+
export default defineSchedule({
|
|
122
|
+
name: 'cleanupSessions',
|
|
123
|
+
cron: '*/15 * * * *',
|
|
124
|
+
timezone: 'UTC',
|
|
125
|
+
description: 'Prune expired sessions.',
|
|
126
|
+
handler: async ({ app, scheduledAt }) => {
|
|
127
|
+
await app.store.sessions.where({ expiresAt: { lt: scheduledAt } }).delete()
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## The config
|
|
133
|
+
|
|
134
|
+
| Field | Required | Default | Notes |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| `name` | yes | — | Stable identifier. Used as the idempotency/coordination key and the dashboard label. |
|
|
137
|
+
| `cron` | yes | — | Standard cron expression (see below). Validated eagerly. |
|
|
138
|
+
| `timezone` | yes | — | IANA zone (`"UTC"`, `"Europe/Berlin"`). Never server-local. |
|
|
139
|
+
| `handler` | one of `handler` / `workflow` | — | `(ctx) => void \| Promise<void>`. Custom work for this firing. |
|
|
140
|
+
| `workflow` | one of `handler` / `workflow` | — | Direct workflow target: `{ name, payload }`. Use when the schedule only starts durable work. |
|
|
141
|
+
| `trigger` | no | app default | `'self'` or `'external'` — per-job override. See [triggers](/docs/scheduling/triggers). |
|
|
142
|
+
| `onOverlap` | no | `'skip'` | `'skip'` / `'queue'` / `'parallel'`. See [overlap](/docs/scheduling/overlap-and-backfill). |
|
|
143
|
+
| `backfill` | no | `'skip'` | `'skip'` / `'latest'` / `'all'`. See [backfill](/docs/scheduling/overlap-and-backfill). |
|
|
144
|
+
| `maxRuntimeMs` | no | `1_800_000` (30 min) | Watchdog. A run exceeding it is recorded `failed` with reason `timeout`. |
|
|
145
|
+
| `description` | no | — | Shown in the dashboard and generated external manifests. |
|
|
146
|
+
|
|
147
|
+
## Direct workflow target
|
|
148
|
+
|
|
149
|
+
When the schedule's only job is to start a durable workflow, declare it directly instead of writing a one-line handler:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// apps/api/schedules/daily-rollup.cron.tsx
|
|
153
|
+
import { defineSchedule } from '@voltro/runtime'
|
|
154
|
+
|
|
155
|
+
export default defineSchedule({
|
|
156
|
+
name: 'dailyRollup',
|
|
157
|
+
cron: '0 2 * * *',
|
|
158
|
+
timezone: 'UTC',
|
|
159
|
+
workflow: {
|
|
160
|
+
name: 'billing.dailyRollup',
|
|
161
|
+
payload: ({ scheduledAt }) => ({
|
|
162
|
+
day: scheduledAt.toISOString().slice(0, 10),
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use `handler` when the firing needs custom branching, extra writes, or multiple side effects. You can still call `ctx.app.workflows.start(...)` manually from that handler.
|
|
169
|
+
|
|
170
|
+
## Cron syntax
|
|
171
|
+
|
|
172
|
+
The expression is parsed by `effect`'s `Cron` module, which accepts **five fields** (minute precision) or **six fields** (with a leading seconds field):
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
┌───────────── second (0–59) ← optional 6th field
|
|
176
|
+
│ ┌─────────── minute (0–59)
|
|
177
|
+
│ │ ┌───────── hour (0–23)
|
|
178
|
+
│ │ │ ┌─────── day of month (1–31)
|
|
179
|
+
│ │ │ │ ┌───── month (1–12)
|
|
180
|
+
│ │ │ │ │ ┌─── day of week (0–6, Sun=0)
|
|
181
|
+
│ │ │ │ │ │
|
|
182
|
+
* * * * * *
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Expression | Fires |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `0 9 * * *` | 09:00 every day |
|
|
188
|
+
| `*/15 * * * *` | every 15 minutes |
|
|
189
|
+
| `0 0 1 * *` | midnight on the 1st of each month |
|
|
190
|
+
| `0 0 * * 1` | midnight every Monday |
|
|
191
|
+
| `*/30 * * * * *` | every 30 **seconds** (six-field) |
|
|
192
|
+
|
|
193
|
+
A typo throws at **definition time**, surfaced in the boot log — not silently swallowed by a scheduler loop that then never fires:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
defineSchedule("dailyDigest"): invalid cron expression "0 25 * * *" (tz="UTC") — …
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
> Six-field (seconds) expressions only run under the `self` trigger. They are rejected when generating [external manifests](/docs/scheduling/deployment) because k8s/EventBridge/Cloud Scheduler are minute-granular.
|
|
200
|
+
|
|
201
|
+
## The timezone is mandatory — on purpose
|
|
202
|
+
|
|
203
|
+
There is no default timezone. Omitting it throws. Server-local time is a bug factory: a container's TZ is usually UTC regardless of where your users are, so "09:00" silently means something different in dev, CI, and prod. Stating the zone makes the intent explicit and identical everywhere.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
timezone: 'Europe/Berlin' // 09:00 Berlin — DST handled by effect's Cron
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## The handler context
|
|
210
|
+
|
|
211
|
+
The handler receives a `ScheduleContext` — the same `app` a mutation gets, plus firing metadata:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
handler: async (ctx) => {
|
|
215
|
+
ctx.scheduledAt // Date — the instant this firing was scheduled for (deterministic, cron-derived)
|
|
216
|
+
ctx.firedAt // Date — when the handler actually started (may lag under load/contention)
|
|
217
|
+
ctx.runId // string — the _voltro_schedule_runs row id for this firing
|
|
218
|
+
ctx.trigger // 'self' | 'external' | 'manual' — where the firing came from
|
|
219
|
+
ctx.app // AppContext — store, request subject, webhooks, plugin interceptors
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Use `scheduledAt`, not `Date.now()`, for any time-bucketed query (the "prune sessions older than this slot" pattern). It's stable across replicas and reflects the intended instant even if the run was delayed.
|
|
224
|
+
|
|
225
|
+
`ctx.trigger === 'manual'` distinguishes a dashboard **Run now** click from a clock firing — handy when a manual run should skip a guard (e.g. "only on weekdays") that the scheduled path enforces.
|
|
226
|
+
|
|
227
|
+
## Discovery
|
|
228
|
+
|
|
229
|
+
`voltro dev` and `voltro build` glob `**/*.cron.{ts,tsx}` under your api app. Each discovered schedule is logged at boot:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
scheduler: starting total=2 self=2 external=0 coordinator=advisoryLock
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The framework also creates the `_voltro_schedule_runs` and (for `advisoryLock`) `_voltro_schedule_claims` tables — `voltro migrate` emits their DDL automatically, so you never hand-write a migration for them.
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
<!-- source: en/scheduling/coordination.md -->
|
|
242
|
+
## Coordination
|
|
243
|
+
|
|
244
|
+
_How N instances of your app avoid double-firing a schedule — single, advisoryLock, and cluster exactly-once strategies._
|
|
245
|
+
|
|
246
|
+
When more than one instance of your app is running, each one's `self` timer wants to fire the same schedule at the same instant. **Coordination** is the gate that decides which instance actually runs it. It's an app-wide setting:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// apps/api/app.config.ts
|
|
250
|
+
scheduling: { coordination: 'advisoryLock' }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The job definition never mentions coordination — you can move from one box to a fleet without touching a `*.cron.tsx` file.
|
|
254
|
+
|
|
255
|
+
## The three strategies
|
|
256
|
+
|
|
257
|
+
| Strategy | Exactly-once across instances? | Needs | Use when |
|
|
258
|
+
|---|---|---|---|
|
|
259
|
+
| `single` | **No** — every instance fires | nothing | One process: PM2 single instance, a single container, local dev |
|
|
260
|
+
| `advisoryLock` | Yes — Postgres arbitrates | Postgres | Multiple instances, no orchestrator: 2–N replicas behind a load balancer |
|
|
261
|
+
| `cluster` | Yes — shard owner fires | Postgres + cluster runner | You already run `@effect/cluster` for workflows and want schedules on the same fabric |
|
|
262
|
+
|
|
263
|
+
Default: **`single` on a memory store, `advisoryLock` on Postgres.** You rarely set this explicitly.
|
|
264
|
+
|
|
265
|
+
## `single`
|
|
266
|
+
|
|
267
|
+
No gate. The instance's timer fires, the handler runs. Zero coordination overhead.
|
|
268
|
+
|
|
269
|
+
This is correct **only if exactly one instance runs the schedule.** Two `single` instances = two firings. That's not a bug to work around — it's the contract. If you scale past one instance, switch to `advisoryLock`.
|
|
270
|
+
|
|
271
|
+
## `advisoryLock`
|
|
272
|
+
|
|
273
|
+
The portable multi-instance strategy. It needs nothing but the Postgres you already have — no Redis, no orchestrator, no leader election.
|
|
274
|
+
|
|
275
|
+
**How it works.** Each firing computes a deterministic key, `<name>@<iso-second>` (e.g. `dailyDigest@2026-05-28T09:00:00`), from the cron-derived `scheduledAt` — *not* `Date.now()`, so every replica computes the identical key regardless of clock skew within the firing window. The bucket is second-precision (`YYYY-MM-DDTHH:MM:SS`, 19 chars), so a 6-field cron like `*/10 * * * * *` gets a distinct key for every firing instant within a minute. Every replica races to `INSERT` that key into `_voltro_schedule_claims`. The table's primary key makes exactly one `INSERT` win; the rest hit the conflict and stand down.
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
replica A ─┐ ┌─ INSERT dailyDigest@…09:00 → wins, runs
|
|
279
|
+
replica B ─┼─ same bucket, same key ─┤
|
|
280
|
+
replica C ─┘ └─ PK conflict → stands down (no run row)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- **Self-expiring.** The key includes the firing instant, so a crashed winner doesn't block the next firing — the next firing instant is a new key. Stale rows are pruned lazily.
|
|
284
|
+
- **Pool-safe.** Unlike a session-level `pg_advisory_lock` (tied to a connection a pool may reassign), a claims row is durable and connection-independent.
|
|
285
|
+
- **Fail-closed.** If the claims table is unreachable, the coordinator logs a warning and declines to fire rather than risk a double-fire. A missing run is recoverable via [backfill](/docs/scheduling/overlap-and-backfill); a double-fire (two charge emails) often isn't.
|
|
286
|
+
|
|
287
|
+
Losers don't write a run row — at scale that would be N−1 noise rows per firing. Only the winner's run appears in the dashboard, tagged `wonLock`.
|
|
288
|
+
|
|
289
|
+
## `cluster`
|
|
290
|
+
|
|
291
|
+
If you already run `@effect/cluster` (for [workflows](/docs/workflows/cluster)), schedules can ride the same sharding fabric. Each schedule becomes a `ClusterCron` singleton; the cluster assigns it to exactly one shard owner, and only that runner fires. The in-app `self` timer is **not** armed in this mode — the cluster owns the clock, so arming it too would double-fire.
|
|
292
|
+
|
|
293
|
+
Runs are tagged `cluster`. Coordination is handled by the cluster's shard assignment, so there's no claims table for cluster-mode schedules.
|
|
294
|
+
|
|
295
|
+
**Caveat — sub-minute lag.** A freshly started runner waits for shard assignment before the first firing (~10s observed on a cold runner). For minute-and-up cadences this is invisible. For "every second" it isn't — use `self` + `single`/`advisoryLock` for sub-minute work, or accept the warm-up.
|
|
296
|
+
|
|
297
|
+
**Caveat — Postgres only.** `cluster` (and `advisoryLock`) need Postgres. On a memory store they fall back to `single` with a boot warning:
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
scheduling.coordination=cluster needs store=postgres — falling back to single (dev/memory)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Choosing
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
Single process? ───────────────────────────────→ single
|
|
307
|
+
Multiple instances, no orchestrator? ──────────→ advisoryLock (← the default on Postgres)
|
|
308
|
+
Already running @effect/cluster for workflows? → cluster
|
|
309
|
+
Letting k8s/EventBridge drive the clock? ──────→ trigger: 'external' (coordination is moot — the platform fires once)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When the platform scheduler drives the firing (the [`external` trigger](/docs/scheduling/triggers)), the platform guarantees once-only, so coordination doesn't apply — the run is tagged `external`.
|
|
313
|
+
|
|
314
|
+
## What each run records
|
|
315
|
+
|
|
316
|
+
Every firing writes a `_voltro_schedule_runs` row with a `coordinationOutcome` so you can see, after the fact, *why* this instance ran it:
|
|
317
|
+
|
|
318
|
+
| `coordinationOutcome` | Meaning |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `single` | No coordination — single-instance mode |
|
|
321
|
+
| `wonLock` | Won the `advisoryLock` race |
|
|
322
|
+
| `cluster` | Fired as the cluster shard owner |
|
|
323
|
+
| `external` | Driven by an outside scheduler hitting the fire endpoint |
|
|
324
|
+
|
|
325
|
+
(`lostLock` firings are not recorded — see above.) The dashboard surfaces this per run; see [the dashboard doc](/docs/scheduling/dashboard).
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
<!-- source: en/scheduling/triggers.md -->
|
|
332
|
+
## Trigger drivers
|
|
333
|
+
|
|
334
|
+
_self vs external — whether an in-app supervised timer drives the clock, or an outside scheduler (k8s CronJob, EventBridge, Cloud Scheduler) hits an HTTP endpoint._
|
|
335
|
+
|
|
336
|
+
The **trigger** decides *who keeps time*. Two drivers:
|
|
337
|
+
|
|
338
|
+
- **`self`** — an in-app supervised timer fires the schedule from inside your process.
|
|
339
|
+
- **`external`** — your app exposes an HTTP endpoint; an outside scheduler (k8s CronJob, AWS EventBridge, GCP Cloud Scheduler, …) POSTs to it on the cadence.
|
|
340
|
+
|
|
341
|
+
It's an app-wide default, overridable per job:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// apps/api/app.config.ts
|
|
345
|
+
scheduling: { trigger: 'self' } // default
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
// override one heavy job to be platform-driven
|
|
350
|
+
export default defineSchedule({
|
|
351
|
+
name: 'monthlyInvoice',
|
|
352
|
+
cron: '0 3 1 * *',
|
|
353
|
+
timezone: 'UTC',
|
|
354
|
+
trigger: 'external', // a dedicated k8s CronJob drives this one
|
|
355
|
+
handler: async ({ app }) => { /* … */ },
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## `self` — the in-app timer
|
|
360
|
+
|
|
361
|
+
The default, and the right choice for most deployments. Each `self` schedule gets a self-rescheduling `setTimeout` (not `setInterval` — cron is not fixed-interval, and `setInterval` drifts and double-fires under event-loop pressure). After each firing the timer re-arms for the next cron occurrence.
|
|
362
|
+
|
|
363
|
+
Properties:
|
|
364
|
+
|
|
365
|
+
- **Non-dying.** The re-arm happens in a `finally` — a handler that throws cannot break the chain.
|
|
366
|
+
- **Long-wait safe.** `setTimeout` delays are 32-bit milliseconds (max ~24.8 days). A quarterly or yearly schedule is chunked into shorter sleeps and re-evaluated, so it doesn't silently clamp and fire on every tick.
|
|
367
|
+
- **Survives restarts.** Run state persists to `_voltro_schedule_runs`; on boot, missed firings are reconciled per the [backfill](/docs/scheduling/overlap-and-backfill) policy.
|
|
368
|
+
- **Coordinated.** Multiple `self` instances dedupe via the [coordination](/docs/scheduling/coordination) strategy.
|
|
369
|
+
|
|
370
|
+
This is what runs under `voltro dev`, a single PM2 process, or a fleet of replicas (with `advisoryLock`).
|
|
371
|
+
|
|
372
|
+
## `external` — the platform drives the clock
|
|
373
|
+
|
|
374
|
+
Some environments want the orchestrator, not the app, to own scheduling — an enterprise k8s policy that all cron lives in `CronJob` objects, or a serverless deployment that scales the app to zero between firings. In `external` mode the app does **not** arm an in-app timer. Instead it exposes:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
POST /_voltro/schedule/<name>/fire?trigger=external
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The outside scheduler hits that endpoint on the cron cadence; the app runs the handler and records the run tagged `external`. Because the platform fires exactly once, [coordination](/docs/scheduling/coordination) does not apply.
|
|
381
|
+
|
|
382
|
+
You don't hand-write the scheduler config. `voltro schedule-manifest` reads your `*.cron.tsx` files and emits the matching CronJob / EventBridge / Cloud Scheduler manifest pointing at this endpoint — see [deployment](/docs/scheduling/deployment).
|
|
383
|
+
|
|
384
|
+
> The endpoint should be protected in any non-trivial deployment (network policy, an auth token, or an ingress rule) so only your scheduler can POST to it. Treat it like any other privileged internal query.
|
|
385
|
+
|
|
386
|
+
## Mixing drivers
|
|
387
|
+
|
|
388
|
+
`trigger` is per-job, so you can split by workload:
|
|
389
|
+
|
|
390
|
+
| Job | Trigger | Why |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `cacheWarmer` (every 5 min) | `self` | Lightweight, in-process, no infra |
|
|
393
|
+
| `monthlyInvoice` (1st of month) | `external` | Heavy; run it as a dedicated k8s `CronJob` pod that scales independently |
|
|
394
|
+
|
|
395
|
+
A six-field (seconds) cron forces `self` — external schedulers can't express sub-minute, so such a job is rejected by the manifest generator.
|
|
396
|
+
|
|
397
|
+
## Picking a driver
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
Want zero scheduling infra, app owns the clock? ──────────→ self (the default)
|
|
401
|
+
Enterprise policy: all cron must be k8s CronJobs? ───────→ external
|
|
402
|
+
App scales to zero / serverless between firings? ────────→ external
|
|
403
|
+
Sub-minute cadence (six-field cron)? ────────────────────→ self (external can't do sub-minute)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
<!-- source: en/scheduling/overlap-and-backfill.md -->
|
|
411
|
+
## Overlap & backfill
|
|
412
|
+
|
|
413
|
+
_What happens when a run is still in flight at the next firing (overlap), what happens to firings missed during downtime (backfill), and the per-run watchdog._
|
|
414
|
+
|
|
415
|
+
Two timing edge cases every recurring job hits eventually: a run that's still going when the next firing is due, and firings that were missed while the process was down. Voltro makes both explicit policies on the schedule.
|
|
416
|
+
|
|
417
|
+
## Overlap — `onOverlap`
|
|
418
|
+
|
|
419
|
+
When a firing arrives and the previous run of the **same schedule** is still in flight:
|
|
420
|
+
|
|
421
|
+
| Policy | Behaviour |
|
|
422
|
+
|---|---|
|
|
423
|
+
| `skip` *(default)* | Don't start a second run. Record a `skipped` row and move on. |
|
|
424
|
+
| `queue` | Serialize: wait for the in-flight run to finish, then run this one. Never concurrent. |
|
|
425
|
+
| `parallel` | Start the new run immediately, alongside the old one. |
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
defineSchedule({
|
|
429
|
+
name: 'reindex',
|
|
430
|
+
cron: '*/5 * * * *',
|
|
431
|
+
timezone: 'UTC',
|
|
432
|
+
onOverlap: 'skip', // a slow reindex shouldn't pile up
|
|
433
|
+
handler: async ({ app }) => { /* … */ },
|
|
434
|
+
})
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Choosing:**
|
|
438
|
+
|
|
439
|
+
- `skip` — idempotent or "latest state wins" jobs (reindex, cache warm). The default, and almost always right.
|
|
440
|
+
- `queue` — every firing's work matters and must happen in order (sequential batch processing). Runs serialize behind one another.
|
|
441
|
+
- `parallel` — runs are independent and you genuinely want concurrency (fan-out to per-tenant work).
|
|
442
|
+
|
|
443
|
+
> **`queue` caveat — unbounded growth.** If a `queue` job consistently takes longer than its interval, the queue grows without bound and the schedule falls further behind. `queue` assumes runs are *usually* faster than the cadence, with occasional overruns. If runs are reliably slower than the interval, your cadence is wrong, not your overlap policy.
|
|
444
|
+
|
|
445
|
+
A manual **Run now** from the dashboard always runs, regardless of `onOverlap` — operators expect the button to fire.
|
|
446
|
+
|
|
447
|
+
## The watchdog — `maxRuntimeMs`
|
|
448
|
+
|
|
449
|
+
Every run races a watchdog (default **30 minutes**). A run that exceeds it stops being awaited and is recorded `failed` with `errorTag: 'timeout'`, so a run row never sits `running` forever.
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
defineSchedule({
|
|
453
|
+
name: 'nightlyExport',
|
|
454
|
+
cron: '0 2 * * *',
|
|
455
|
+
timezone: 'UTC',
|
|
456
|
+
maxRuntimeMs: 2 * 60 * 60_000, // 2 hours — a big export
|
|
457
|
+
handler: async ({ app }) => { /* … */ },
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
> The watchdog stops *waiting* and records the timeout; it cannot truly abort a Promise's in-flight side effects (JavaScript has no thread-kill). Make long handlers cooperative — check a deadline, or do the heavy lifting in a [workflow](/docs/workflows/overview) with its own step-level durability.
|
|
462
|
+
|
|
463
|
+
## Backfill — `backfill`
|
|
464
|
+
|
|
465
|
+
When the process was down across one or more firing instants, what should happen on boot? Computed from the last `_voltro_schedule_runs` row for the schedule.
|
|
466
|
+
|
|
467
|
+
| Policy | Behaviour |
|
|
468
|
+
|---|---|
|
|
469
|
+
| `skip` *(default)* | Ignore missed firings. Resume from the next future occurrence. |
|
|
470
|
+
| `latest` | Fire **once** to catch up to the most recent missed slot; record the older missed slots as `missed` (not silently dropped). |
|
|
471
|
+
| `all` | Fire **every** missed slot in order. |
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
defineSchedule({
|
|
475
|
+
name: 'dailyDigest',
|
|
476
|
+
cron: '0 9 * * *',
|
|
477
|
+
timezone: 'Europe/Berlin',
|
|
478
|
+
backfill: 'latest', // missed Tuesday's 9am after a deploy? send one catch-up, log the rest as missed
|
|
479
|
+
handler: async ({ app }) => { /* … */ },
|
|
480
|
+
})
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Choosing:**
|
|
484
|
+
|
|
485
|
+
- `skip` — the firing was time-sensitive and a late run is worse than no run ("send the 9am alert" — 9am has passed, don't send it at noon).
|
|
486
|
+
- `latest` — you want the side effect to have happened recently, but replaying every missed slot would spam ("the digest should be reasonably current").
|
|
487
|
+
- `all` — every slot represents real work that must not be lost (per-period billing rollups). **Dangerous for side-effecting jobs** — a week of downtime means a week of catch-up firings. Opt in deliberately.
|
|
488
|
+
|
|
489
|
+
Backfill runs **before** the live timer is armed, so a caught-up firing never races the first scheduled one. The catch-up walk is capped (1000 slots) so a schedule that hasn't run in months doesn't enumerate forever.
|
|
490
|
+
|
|
491
|
+
Missed slots recorded under `latest` show up in the dashboard with the `missed` status — visible evidence of the gap, not a silent hole.
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
<!-- source: en/scheduling/deployment.md -->
|
|
498
|
+
## Deployment
|
|
499
|
+
|
|
500
|
+
_Run the same schedule on PM2, a Kubernetes fleet, or an external scheduler — and generate the platform manifest with voltro schedule-manifest._
|
|
501
|
+
|
|
502
|
+
The point of the `*.cron.tsx` primitive is that **the job definition doesn't change when the topology does.** You pick a trigger + coordination per environment; the cron, timezone, and handler stay put. This page maps common hosting setups to the right configuration.
|
|
503
|
+
|
|
504
|
+
## The topology matrix
|
|
505
|
+
|
|
506
|
+
| Hosting | `trigger` | `coordination` | Notes |
|
|
507
|
+
|---|---|---|---|
|
|
508
|
+
| Local dev (`voltro dev`) | `self` | `single` | Memory store default. |
|
|
509
|
+
| Single VPS / one PM2 process | `self` | `single` | No coordination needed — one process. |
|
|
510
|
+
| PM2 cluster / multiple containers | `self` | `advisoryLock` | Postgres arbitrates. No extra infra. |
|
|
511
|
+
| Kubernetes, N replicas | `self` | `advisoryLock` | The pragmatic default — pods race the Postgres claim. |
|
|
512
|
+
| Kubernetes, already running `@effect/cluster` | `self` | `cluster` | Schedules ride the existing sharding fabric. |
|
|
513
|
+
| "All cron must be k8s CronJobs" (policy) | `external` | — | The cluster fires; generate a `CronJob` manifest. |
|
|
514
|
+
| AWS (EventBridge owns cron) | `external` | — | Generate an EventBridge rule. |
|
|
515
|
+
| GCP (Cloud Scheduler) / Azure (Functions timer) | `external` | — | Generate the matching job. |
|
|
516
|
+
|
|
517
|
+
The only code that changes between any two rows is the `scheduling` block in `app.config.ts` (and occasionally a per-job `trigger:` override). The schedules themselves are untouched.
|
|
518
|
+
|
|
519
|
+
## Self-hosted, in-process (the common case)
|
|
520
|
+
|
|
521
|
+
Nothing to deploy beyond your app. Set the coordination that matches your instance count:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
// apps/api/app.config.ts
|
|
525
|
+
scheduling: { coordination: 'advisoryLock' } // 2+ instances on Postgres
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`advisoryLock` is the default on Postgres, so multi-instance "just works" — see [coordination](/docs/scheduling/coordination). This is the right answer for most fleets: no orchestrator coupling, no separate scheduler to operate.
|
|
529
|
+
|
|
530
|
+
The production API server starts the same scheduler engine as `voltro dev`. If a schedule starts a workflow, `app.workflows.start(...)` is available in the handler; with `coordination: 'cluster'`, Voltro acquires the cluster-cron layer during boot so the clock does not wait for the first incoming request.
|
|
531
|
+
|
|
532
|
+
## Delegating to an external scheduler
|
|
533
|
+
|
|
534
|
+
When the platform must own the clock (enterprise policy, serverless scale-to-zero), set `trigger: 'external'` and generate the manifest:
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
voltro schedule-manifest --provider kubernetes --base-url https://api.internal.example.com
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
| `--provider` | Emits |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `kubernetes` | A `CronJob` per schedule that `curl`s the fire endpoint |
|
|
543
|
+
| `aws` | EventBridge Scheduler / rule config |
|
|
544
|
+
| `gcp` | A `gcloud scheduler jobs create http` command |
|
|
545
|
+
| `azure` | An Azure Functions timer-trigger outline |
|
|
546
|
+
| `generic` *(default)* | A README with the `curl` shape and the cron table |
|
|
547
|
+
|
|
548
|
+
Each generated job POSTs to:
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
POST <base-url>/_voltro/schedule/<name>/fire?trigger=external
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Example Kubernetes output (abridged):
|
|
555
|
+
|
|
556
|
+
```yaml
|
|
557
|
+
apiVersion: batch/v1
|
|
558
|
+
kind: CronJob
|
|
559
|
+
metadata:
|
|
560
|
+
name: dailydigest
|
|
561
|
+
spec:
|
|
562
|
+
schedule: "0 9 * * *"
|
|
563
|
+
jobTemplate:
|
|
564
|
+
spec:
|
|
565
|
+
template:
|
|
566
|
+
spec:
|
|
567
|
+
containers:
|
|
568
|
+
- name: trigger
|
|
569
|
+
image: curlimages/curl:8.11.0
|
|
570
|
+
command:
|
|
571
|
+
- curl
|
|
572
|
+
- -fsS
|
|
573
|
+
- -X
|
|
574
|
+
- POST
|
|
575
|
+
- "https://api.internal.example.com/_voltro/schedule/dailyDigest/fire?trigger=external"
|
|
576
|
+
restartPolicy: OnFailure
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
> **Six-field crons are rejected here.** External schedulers are minute-granular — k8s `CronJob`, EventBridge, and Cloud Scheduler can't express seconds. A schedule with a six-field expression must stay on the `self` trigger. The generator errors rather than silently dropping the seconds field.
|
|
580
|
+
|
|
581
|
+
## What to deploy alongside
|
|
582
|
+
|
|
583
|
+
- **`voltro migrate`** creates `_voltro_schedule_runs` (and `_voltro_schedule_claims` for `advisoryLock`) automatically — run it as part of your release, the same as your other tables. You never hand-write these migrations.
|
|
584
|
+
- **Protect the fire endpoint.** `/_voltro/schedule/<name>/fire` runs a handler — gate it (network policy, ingress rule, or a shared token) so only your scheduler can reach it.
|
|
585
|
+
- See [self-hosting](/docs/deployment/self-hosting) and [Voltro Cloud](/docs/deployment/voltro-cloud) for the broader deployment story.
|
|
586
|
+
|
|
587
|
+
## Switching topologies later
|
|
588
|
+
|
|
589
|
+
Moving from a single box to a fleet is a config change, not a rewrite:
|
|
590
|
+
|
|
591
|
+
```diff
|
|
592
|
+
scheduling: {
|
|
593
|
+
- coordination: 'single',
|
|
594
|
+
+ coordination: 'advisoryLock',
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Moving cron ownership to Kubernetes:
|
|
599
|
+
|
|
600
|
+
```diff
|
|
601
|
+
scheduling: {
|
|
602
|
+
- trigger: 'self',
|
|
603
|
+
+ trigger: 'external',
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
…then `voltro schedule-manifest --provider kubernetes --base-url …` and apply the output. The `*.cron.tsx` files don't change.
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
<!-- source: en/scheduling/dashboard.md -->
|
|
613
|
+
## Dashboard
|
|
614
|
+
|
|
615
|
+
_Inspecting schedules in the DevTools and Voltro Cloud dashboards — discovered jobs, firing history, status, and "Run now"._
|
|
616
|
+
|
|
617
|
+
Every discovered schedule shows up in the **Schedules** panel of both the local DevTools dashboard (`voltro dev`) and the Voltro Cloud dashboard. Same component, two transports: DevTools polls the app's inspect endpoints directly; Cloud streams firing history live via its reactive bridge.
|
|
618
|
+
|
|
619
|
+
## What you see
|
|
620
|
+
|
|
621
|
+
**Per schedule:**
|
|
622
|
+
|
|
623
|
+
- Name, cron expression, timezone, and a live **next-firing countdown** ("in 4h 59m").
|
|
624
|
+
- Trigger badge — `self` or `external`.
|
|
625
|
+
- The effective **coordination** strategy for the app (`single` / `advisoryLock` / `cluster`), shown once at the top.
|
|
626
|
+
- `overlap`, `backfill`, and `max runtime` settings.
|
|
627
|
+
- A **Run now** button (capability-gated) — fires the handler immediately, bypassing the clock and coordination, recorded as a `manual` run.
|
|
628
|
+
|
|
629
|
+
**Per firing (expand a schedule's run timeline):**
|
|
630
|
+
|
|
631
|
+
- Status — `succeeded`, `failed`, `skipped`, `missed`, or `running`, each colour-toned.
|
|
632
|
+
- When it fired (relative + absolute), how long it took, and which replica ran it.
|
|
633
|
+
- The `coordinationOutcome` (`single` / `wonLock` / `cluster` / `external`) — *why* this instance ran it.
|
|
634
|
+
- For failures: the `errorTag` and message (e.g. `SmtpError: connection refused`, or `timeout` from the [watchdog](/docs/scheduling/overlap-and-backfill)).
|
|
635
|
+
|
|
636
|
+
## Reading the statuses
|
|
637
|
+
|
|
638
|
+
| Status | Meaning |
|
|
639
|
+
|---|---|
|
|
640
|
+
| `succeeded` | Handler completed within `maxRuntimeMs`. |
|
|
641
|
+
| `failed` | Handler threw, or the watchdog tripped (`errorTag: timeout`). |
|
|
642
|
+
| `skipped` | An overlapping firing under `onOverlap: 'skip'`. |
|
|
643
|
+
| `missed` | A firing skipped during downtime, recorded by `backfill: 'latest'`. Visible evidence of a gap, not a silent hole. |
|
|
644
|
+
| `running` | In flight right now. |
|
|
645
|
+
|
|
646
|
+
`missed` and `skipped` rows matter: they're the system telling you a firing *didn't* run and why. A wall of `missed` after a deploy means your downtime crossed firing instants — expected with `backfill: 'skip'`, a signal to consider `latest` if those runs mattered.
|
|
647
|
+
|
|
648
|
+
## "Run now"
|
|
649
|
+
|
|
650
|
+
The **Run now** button triggers the handler out-of-band. The run is tagged `trigger: 'manual'` and always executes regardless of `onOverlap` — operators expect the button to fire. Inside the handler you can branch on `ctx.trigger === 'manual'` to skip schedule-only guards (e.g. a weekday check) during a manual test.
|
|
651
|
+
|
|
652
|
+
It's gated on the same capability as workflow run-control, so read-only dashboard viewers see the history but can't trigger firings.
|
|
653
|
+
|
|
654
|
+
## The endpoints behind it
|
|
655
|
+
|
|
656
|
+
The dashboard is a thin client over the app's inspect API — useful if you're scripting:
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
GET /_voltro/inspect/schedules # discovered schedules + effective coordination
|
|
660
|
+
GET /_voltro/inspect/schedules/runs?name= # firing history for one schedule
|
|
661
|
+
POST /_voltro/inspect/schedules/:name/fire # "Run now"
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
In the Cloud dashboard, firing history is **live** — the cloud API mirrors each app's inspect stream into a reactive cache, so new runs appear without a refresh. In local DevTools the panel polls every few seconds.
|
|
665
|
+
|
|
666
|
+
See [workflow debugging](/docs/workflows/debugging) for the analogous Workflows panel.
|