MainSDK
Search⌘K
Interfaces

Reliability and operations

Deploy config and serverless schedules

deploy

Write the platform's own deploy files from main.yaml for Vercel, Cloudflare Workers, Fly.io and Docker, wake schedules through one tick route, check the build before it ships and diff two environments.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec deployFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

deploy writes your platform's own deploy files from main.yaml, wakes schedules through one tick route, checks the build before it ships and diffs two environments.

At a glance

deploy:
  target: vercel
  regions: [fra1]
  port: 3000
  tick:
    secret: secret:CRON_SECRET
    late: 10m
  vercel: { plan: pro }
  docker: { base: node:24-slim }
jobs:
  schedules:
    nightly-report: "0 3 * * *"
seal:
  mode: warn
  exclude: ["**/*.map"]
  publish: { sbom: false }

Calls

  • createTick(app)
  • tick.handle(req)
  • tick.scheduled(event)
  • deploySection(config)
  • staticSchedules(config)
  • slotOf(expr, now)

Guarantees

Whatever the driver.

  • MainSDK writes and checks deploy files; it never pushes a deploy, holds a platform token or hosts the app
  • Generated blocks are owned and checked; every other key and line of the file is kept byte for byte
  • Regions follow the data regions, and a region outside them is a finding
  • A schedule runs once per slot, however many times the platform calls the tick
  • The build fails before anything is served when a required env var name is missing
  • deploy config, preflight and diff make no network call and read no platform credential

Drivers

4 drivers. Select one to see its line in main.yaml.

main.yamlVercel (default)
deploy:
  driver: vercel

Change this one line to swap providers; your calls stay the same.Vercel integration

Codes

CodeWhere it appearsMeaning
MAIN-DEPLOY-0001Checkmainsdk check, before deployA schedule can't be deployed: its expression is computed at runtime and the target has no long-running process, or jobs.schedules and a jobs.schedule call give one name two expressions
MAIN-DEPLOY-0002Checkmainsdk check, before deploydeploy.regions names a region outside the app's data regions, or the target can't run in a data region
MAIN-DEPLOY-0003Checkmainsdk check, before deploydeploy preflight found required env var names missing or empty in the build; the names are listed, never a value
MAIN-DEPLOY-0004Checkmainsdk check, before deployA schedule or region breaks the declared platform plan's limit (for example a cron more often than daily on Vercel Hobby, or two regions on Hobby)
MAIN-DEPLOY-0005Checkmainsdk check, before deployA Next.js app targets Docker or Fly without output: "standalone" in its Next.js config
MAIN-DEPLOY-0010Checkmainsdk check, before deployA generated block is stale against main.yaml, or was edited by hand; the fix is the deploy config command that rewrites it
MAIN-DEPLOY-0011Checkmainsdk check, before deployA secret would reach the image or the build log: a Dockerfile ARG or ENV named after a secret: reference, or .dockerignore not excluding .env*
MAIN-DEPLOY-0012Checkmainsdk check, before deploydeploy diff --fail-on widen found a change that widens what the target environment may do: a new allowed host, a new secret, a new region or a looser grant
MAIN-DEPLOY-0006RuntimeError responses and logsSee the spec
MAIN-DEPLOY-0007RuntimeError responses and logsSee the spec
MAIN-DEPLOY-0008RuntimeError responses and logsSee the spec

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Deploy section and jobs.schedules in main.yaml with region and plan-limit rulesPhase 10.4deploy#contract
Schedules from jobs.schedules and literal jobs.schedule callsPhase 10.4deploy#schedules
vercel.json regionsPhase 10.4deploy#vercel
wrangler.toml with cron triggers and the in-process scheduled handlerPhase 10.4deploy#cloudflare
fly.toml and a non-root Dockerfile with probes and a shutdown deadlinePhase 10.4deploy#fly-docker

Quick start

main.yaml
deploy:
  target: vercel
  regions: [fra1]
  port: 3000
  tick:
    secret: secret:CRON_SECRET
    late: 10m
  vercel: { plan: pro }
  docker: { base: node:24-slim }
jobs:
  schedules:
    nightly-report: "0 3 * * *"
app/example.ts
// The platform cron calls this route with the bearer secret; Workers calls scheduled in process
import { createTick, type ScheduledEvent } from "mainsdk/deploy";
import { app } from "mainsdk";

const tick = createTick(app());

export default {
  fetch: (req: Request) => {
    const { pathname } = new URL(req.url);
    return tick.match(pathname) ? tick.handle(req) : new Response("Not found", { status: 404 });
  },
  scheduled: (e: ScheduledEvent) => tick.scheduled(e),
};

Configuration

The Quick start shows the whole deploy section.

KeyTypeWhat it does
deploy.targetstringvercel (default), cloudflare, fly, docker or node
deploy.regionslistFunction or machine regions; they must follow the data regions
deploy.portnumberThe port the server listens on
deploy.tick.secretsecret referenceThe bearer secret the platform sends to the tick route
deploy.tick.latedurationA tick later than this after its slot still runs and logs MAIN-DEPLOY-0008
deploy.vercel.planstringhobby or pro; hobby limits cron and regions (MAIN-DEPLOY-0004)
deploy.docker.basestringThe base image of the generated Dockerfile
jobs.schedulesmapSchedule name to cron expression; each becomes a platform cron entry
sealmapThe build seal the deploy ships with: mode (off, warn or refuse), exclude globs and publish.sbom. Its checks are in Seal your build

What you get

Write the platform's own deploy files from main.yaml for Vercel, Cloudflare Workers, Fly.io and Docker, wake schedules through one tick route, check the build before it ships and diff two environments.

Calls

CallWhat it does
createTick(app)Builds the tick route and the Workers scheduled handler for an app. Mount tick.match(path) before your own routes.
tick.handle(req)Answers POST /_mainsdk/jobs/tick?schedule=<name>: 200 { schedule, slot, ran }, or a problem 401, 404 or 405. Never throws.
tick.scheduled(event)Runs every schedule whose expression equals event.cron in process, with no HTTP call, for a Cloudflare cron trigger.
deploySection(config)Reads the deploy section of a config with its defaults applied.
staticSchedules(config)The jobs.schedules of a config as name to cron expression.
slotOf(expr, now)The latest minute at or before now (UTC) that a cron expression matches, as epoch milliseconds, or null.

What mainsdk deploy config writes

TargetFiles it ownsWhat is generated
vercelvercel.jsonThe keys regions, crons (one per schedule, calling the tick route) and buildCommand (mainsdk deploy preflight --env <env> && <your build>)
cloudflarewrangler.tomlA block with compatibility_flags = ["nodejs_compat"], triggers.crons (one per distinct expression), a name, a main entry and a compatibility date
flyfly.toml, Dockerfile, .dockerignoreprimary_region, kill_timeout (shutdown deadline plus 5 s), an http_service with a check on health.ready, and no cron
dockerDockerfile, .dockerignoreA multi-stage image whose runtime stage runs as node, with STOPSIGNAL SIGTERM and a node -e health check on health.live
nodenothingOnly deploy/env.<env>.example

Every target also writes deploy/env.<env>.example, which lists each required env var name with where it is used and no value, and mainsdk.deploy.json, which records each file and a hash of the part MainSDK owns. Commit both.

MainSDK owns the lines between # mainsdk:generated:begin and # mainsdk:generated:end and, in vercel.json, the three keys above. Everything else in the file is yours and is kept byte for byte. A key that you define outside the block wins and the block leaves it out. mainsdk check reports a stale or hand-edited block as MAIN-DEPLOY-0010 with the command that rewrites it.

Regions and plan limits

The function or machine region follows the data regions of db.regions and files.regions.

Data regionVercelFly.ioCloudflare Workers
usiad1iadglobal; info finding only
eufra1fraglobal; info finding only
inbom1bomglobal; info finding only

A deploy.regions entry outside the data regions is MAIN-DEPLOY-0002. With deploy.vercel.plan: hobby, a cron more often than once a day, more than two cron entries or more than one region is MAIN-DEPLOY-0004.

Schedules on serverless targets

On vercel and cloudflare there is no process to keep a timer, so the in-process scheduler of jobs.schedule is off and each schedule becomes a platform cron entry. The platform calls POST /_mainsdk/jobs/tick?schedule=<name> with Authorization: Bearer <deploy.tick.secret>. The tick answers as soon as the job is queued and runs each schedule once per slot, so a duplicate call answers ran: false. A tick later than deploy.tick.late after its slot still runs and logs MAIN-DEPLOY-0008. The tick is left out of the SLO request counts.

Schedules are the jobs.schedules of main.yaml plus every jobs.schedule("name", "expr", …) call whose name and expression are string literals. A computed one can't be deployed to a serverless target: MAIN-DEPLOY-0001, with its file and line.

Preflight and environment diff

mainsdk deploy preflight --env prod runs inside the platform build, where the secrets are. It checks that each required env var name is present and non-empty, the region and plan rules, and mainsdk check --env prod, and fails the build before anything is served. It prints names, never values.

mainsdk deploy diff --from staging --to prod lists what changes between two environments. With --fail-on widen it fails on a new allowed host, a new secret, a new region or a looser grant. MainSDK never pushes a deploy, holds a platform token or hosts your app; the platform's Git integration or CLI does that.

What it always does

  • MainSDK writes and checks deploy files; it never pushes a deploy, holds a platform token or hosts the app
  • Generated blocks are owned and checked; every other key and line of the file is kept byte for byte
  • Regions follow the data regions, and a region outside them is a finding
  • A schedule runs once per slot, however many times the platform calls the tick
  • The build fails before anything is served when a required env var name is missing
  • deploy config, preflight and diff make no network call and read no platform credential

Errors and checks

CodeWhenWhat to do
MAIN-DEPLOY-0001A schedule cannot be deployed: its expression is computed at runtime, or one name has two expressionsUse a string literal expression
MAIN-DEPLOY-0002deploy.regions is outside the data regions, or the target cannot run in onePick a region from the table above
MAIN-DEPLOY-0003deploy preflight found required env var names missing or emptySet the names it lists on the platform
MAIN-DEPLOY-0004A schedule or region breaks the declared plan's limitLower the cron rate or regions, or change the plan
MAIN-DEPLOY-0005A Next.js app targets Docker or Fly without output: "standalone"Set it in the Next.js config
MAIN-DEPLOY-0006A tick arrived without the right bearer secret (401)Send deploy.tick.secret
MAIN-DEPLOY-0007A tick named a schedule the app does not declare (404)Declare it under jobs.schedules
MAIN-DEPLOY-0008A tick ran later than deploy.tick.late after its slotCheck the platform's cron delay
MAIN-DEPLOY-0010A generated block is stale or hand-editedRun the deploy config command the finding names
MAIN-DEPLOY-0011A secret would reach the image or the build logRemove the ARG or ENV; exclude .env* in .dockerignore
MAIN-DEPLOY-0012deploy diff --fail-on widen found a widening changeReview the new host, secret, region or grant

Each code's cause and fix is in check codes.

Swapping drivers

main.yaml
deploy:
  target: fly   # was vercel

Then run mainsdk deploy config again. Your app code stays the same; the files it owns change to the new platform's. The drivers are:

DriverProvider
vercelVerceldefault
cloudflareCloudflare Workers
flyFly.io
dockerDocker

Testing

The tick, the file writers and the preflight run against fake platforms, with no network call (packages/sdk/test/deploy-tick.test.ts, packages/cli/test/deploy.test.ts). Check your own files in CI with mainsdk deploy config --check and mainsdk deploy preflight --env <env>.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet