---
title: "Ship with deploy config"
description: "Generate Vercel, Cloudflare, Fly or Docker files from main.yaml, run schedules on a serverless platform, fail the build on a missing secret and review a staging to production change."
section: "guides"
status: "Building for 0.4"
spec: "deploy"
canonical: "https://mainsdk.dev/docs/guides/ship-with-deploy-config"
updated: "2026-10-28"
---

# Ship with deploy config

> Generate Vercel, Cloudflare, Fly or Docker files from main.yaml, run schedules on a serverless platform, fail the build on a missing secret and review a staging to production change.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/ship-with-deploy-config · Updated: 2026-10-28

You will generate the platform's deploy files from `main.yaml`, wake a schedule through the tick route, and review what a promotion to production changes. It takes about 20 minutes. MainSDK writes and checks the files; the platform's Git integration or CLI still deploys.

## You need

- A MainSDK app with a `main.yaml` and a production overlay `main.env.prod.yaml` (see [Deploy on Vercel](https://mainsdk.dev/docs/guides/deploy-vercel)).
- The app's data regions declared in `db.regions`.

## 1. Pick a target and a schedule

Set the target in the production overlay and declare the schedule in `main.yaml`:

```yaml title="main.env.prod.yaml"
deploy:
  target: vercel
  vercel: { plan: pro }
```

```yaml title="main.yaml"
jobs:
  schedules:
    nightly-report: "0 3 * * *"
```

## 2. Write the files

```bash
npx mainsdk deploy config --env prod
```

This writes `vercel.json` (regions, one cron for `nightly-report`, a build command that runs the preflight), `deploy/env.prod.example` and `mainsdk.deploy.json`. Commit all three. Keys you added to `vercel.json` yourself are not touched. Run it again after any change to `main.yaml`; `mainsdk deploy config --check` and `mainsdk check` fail when a generated block is stale.

## 3. Serve the tick route

On `vercel` and `cloudflare` the platform's cron calls one route. Mount it before your own routes and set `CRON_SECRET` in the platform:

```ts title="app/api/mainsdk/route.ts"
import { app } from "mainsdk";
import { createTick } from "mainsdk/deploy";

const tick = createTick(app());
export const POST = (req: Request) => tick.handle(req);
```

On Cloudflare Workers export `scheduled: (event) => tick.scheduled(event)` next to `fetch`, and the same job is queued in process with no HTTP call.

## 4. Let the build refuse a missing secret

The generated build command runs `mainsdk deploy preflight --env prod` first. Try it with a name unset:

```bash
CRON_SECRET=x npx mainsdk deploy preflight --env prod
```

It exits 1 with `MAIN-DEPLOY-0003` and lists the missing names, never a value.

## 5. Review the promotion

```bash
npx mainsdk deploy diff --from staging --to prod --fail-on widen
```

It lists changed config, env names, schedules, regions and grants, and exits 1 with `MAIN-DEPLOY-0012` for each change that lets production do more than staging.

## Check it worked

Run `npx mainsdk check --env prod`. It passes when the generated files match `main.yaml`, the regions sit inside the data regions and the schedules fit the plan.
