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 Postgres

advisoryLock 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=external

Example 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: OnFailure

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.

What to deploy alongside

  • 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.
  • 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.
  • 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.