Deployment
Run the same schedule on PM2, a Kubernetes fleet, or an external scheduler — and generate the platform manifest with voltro schedule-manifest.
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.
The topology matrix
| Hosting | trigger |
coordination |
Notes |
|---|---|---|---|
Local dev (voltro dev) |
self |
single |
Memory store default. |
| Single VPS / one PM2 process | self |
single |
No coordination needed — one process. |
| PM2 cluster / multiple containers | self |
advisoryLock |
Postgres arbitrates. No extra infra. |
| Kubernetes, N replicas | self |
advisoryLock |
The pragmatic default — pods race the Postgres claim. |
Kubernetes, already running @effect/cluster |
self |
cluster |
Schedules ride the existing sharding fabric. |
| "All cron must be k8s CronJobs" (policy) | external |
— | The cluster fires; generate a CronJob manifest. |
| AWS (EventBridge owns cron) | external |
— | Generate an EventBridge rule. |
| GCP (Cloud Scheduler) / Azure (Functions timer) | external |
— | Generate the matching job. |
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.
Self-hosted, in-process (the common case)
Nothing to deploy beyond your app. Set the coordination that matches your instance count:
// apps/api/app.config.ts
scheduling: { coordination: 'advisoryLock' } // 2+ instances on PostgresadvisoryLock is the default on Postgres, so multi-instance "just works" — see coordination. This is the right answer for most fleets: no orchestrator coupling, no separate scheduler to operate.
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.
Delegating to an external scheduler
When the platform must own the clock (enterprise policy, serverless scale-to-zero), set trigger: 'external' and generate the manifest:
voltro schedule-manifest --provider kubernetes --base-url https://api.internal.example.com--provider |
Emits |
|---|---|
kubernetes |
A CronJob per schedule that curls the fire endpoint |
aws |
EventBridge Scheduler / rule config |
gcp |
A gcloud scheduler jobs create http command |
azure |
An Azure Functions timer-trigger outline |
generic (default) |
A README with the curl shape and the cron table |
Each generated job POSTs to:
POST <base-url>/_voltro/schedule/<name>/fire?trigger=externalExample Kubernetes output (abridged):
apiVersion: batch/v1
kind: CronJob
metadata:
name: dailydigest
spec:
schedule: "0 9 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: trigger
image: curlimages/curl:8.11.0
command:
- curl
- -fsS
- -X
- POST
- "https://api.internal.example.com/_voltro/schedule/dailyDigest/fire?trigger=external"
restartPolicy: OnFailureSix-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 theselftrigger. The generator errors rather than silently dropping the seconds field.
What to deploy alongside
voltro migratecreates_voltro_schedule_runs(and_voltro_schedule_claimsforadvisoryLock) automatically — run it as part of your release, the same as your other tables. You never hand-write these migrations.- Protect the fire endpoint.
/_voltro/schedule/<name>/fireruns a handler — gate it (network policy, ingress rule, or a shared token) so only your scheduler can reach it. - See self-hosting and Voltro Cloud for the broader deployment story.
Switching topologies later
Moving from a single box to a fleet is a config change, not a rewrite:
scheduling: {
- coordination: 'single',
+ coordination: 'advisoryLock',
}Moving cron ownership to Kubernetes:
scheduling: {
- trigger: 'self',
+ trigger: 'external',
}…then voltro schedule-manifest --provider kubernetes --base-url … and apply the output. The *.cron.tsx files don't change.