Deployment

PreviousNext

Ship a Convex-backed Expo app — dev versus prod deployments, EAS build profiles, the two-job CI workflow both starters include, migration safety, and the production checklist.

Two things deploy independently: your Convex functions (npx convex deploy) and your app (EAS build, or an over-the-air update). Unlike Supabase there is no separate migration step — pushing your functions also pushes the current schema.ts.

Dev vs. prod deployments

Every Convex project has (at least) two deployments: the dev deployment npx convex dev talks to while you work, and a separate prod deployment your shipped app should point at. Environment variables are set per deployment — npx convex env set NAME value targets dev, --prod targets prod. The auth installation guide covers pointing SITE_URL and EXPO_URL at the right one before you ship.

Environment variables

EXPO_PUBLIC_ variables are inlined into the JavaScript bundle at build time — they are not secret, and changing one needs a new build or OTA update, not a server restart.

eas.json
{
  "build": {
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "env": {
        "EXPO_PUBLIC_CONVEX_URL": "https://your-dev-deployment.convex.cloud"
      }
    },
    "production": {
      "autoIncrement": true,
      "channel": "production",
      "env": {
        "EXPO_PUBLIC_CONVEX_URL": "https://your-prod-deployment.convex.cloud"
      }
    }
  }
}

Building

eas build --platform all --profile preview
eas build --platform all --profile production
eas submit --platform ios --profile production

Over-the-air updates

eas update --branch production --message "Fix the reset-password redirect"

OTA updates ship JavaScript only. Changing scheme in app.json needs a new build, and it also means updating EXPO_URL on your Convex deployment — see the redirect allow-list.

The shipped workflow

.github/workflows/ci.yml in both starters has two jobs — fewer than Supabase's four, and deliberately so.

verifytsc --noEmit, expo lint, jest. Runs on every PR, needs no live deployment because convex/_generated/ is checked in.

build — kicks off an EAS build on merge to main.

There is no types-are-current job. Supabase's works because supabase db start spins up a throwaway local Postgres from checked-in migration files — nothing needs to be logged in. Convex has no equivalent: _generated/ is produced by npx convex dev or npx convex codegen talking to an actual deployment, and there is no way to regenerate it from schema and function source alone without an authenticated session against somewhere. There is nothing to regenerate-and-diff in an unauthenticated CI job, so this job is dropped rather than faked.

There is also no default deploy job. Supabase's runs non-interactively off an access token every Supabase-scaffolded user already has from creating their project. The Convex CLI's own npx convex dev --once — what bna-ui convex itself runs — opens a browser and cannot run in CI at all.

Deploying for real

pnpm dlx convex deploy

Pushes your current schema.ts and functions to your prod deployment. Run it from your machine, or wire up the optional job below.

Adding a CI deploy job (optional)

npx convex deploy can run non-interactively, with a deploy key from your dashboard's Settings → Deploy Keys:

.github/workflows/ci.yml
deploy:
  name: Deploy Convex functions
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  needs: [verify]
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with: { node-version: 22, cache: npm }
    - run: npm ci
    - run: npx convex deploy
      env:
        CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}

This is not in the shipped workflow because nothing in the scaffold prompts for or sets up that key — add it once you have one, rather than have every fresh git push fail on a secret that does not exist yet.

Migration safety

npx convex deploy pushes your current schema and functions. It does not roll back.

  • Add fields as v.optional(...), backfill with a mutation, then tighten it. A required field on a table with existing documents fails the deploy until every row has one.
  • Do not remove a field or function the deployed app still reads. Deploy the app that stops reading it first.
  • Deploy functions before the app that needs them. A new query with old clients is harmless; a client calling a query that no longer exists is a crash.

Assume old versions of your app are live for weeks — your users decide when they update, unlike a web deploy.

Production checklist

Point SITE_URL and EXPO_URL at production

pnpm dlx convex env set SITE_URL https://your-site.com --prod
npx convex env set EXPO_URL your-scheme:// --prod

Set every provider's credentials against --prod too

AUTH_RESEND_KEY, AUTH_GOOGLE_ID/SECRET, AUTH_APPLE_ID/SECRET — each one you configured for dev, again with --prod.

Re-read every query and mutation as an attacker

There is no RLS backstop here. Confirm every function that returns or changes user data checks getAuthUserId(ctx) against the right owner. See database.

Confirm the production EAS profile points at prod

EXPO_PUBLIC_CONVEX_URL in the production build profile must be your prod deployment's URL, not the dev one you have been testing against.

Monitoring

The Convex dashboard's Logs and Health sections cover function errors, execution time and scheduled job failures. For the app side, expo-updates and EAS Insights cover adoption and crashes.

Next