Skip to content

Deployment

Configure Vercel, Turso, Resend, and GitHub for the Concourse application

This runbook configures the existing concourse Vercel project. It does not create a staging environment or a custom domain.

CLI deployment commands use supplied configuration and never load local files. Ordinary CLI commands select production when NODE_ENV=production, or when NODE_ENV is absent and CI=true or CI=1. Explicit NODE_ENV=test still selects test files in CI. See the CLI mode policy.

Initial database migration

The repository has one baseline migration. It creates the current schema in an empty database. It does not upgrade a database that applied the old migration history or an earlier baseline.

Before the first successful Production migration, create an empty database as Turso resources describes. Inspect its schema and migration ledger before deployment. Do not reset an existing Production database to apply this baseline. Until the baseline is applied, each schema change regenerates the baseline under the same folder name and adds no migration. The database package guide gives the procedure. After the first successful Production migration, keep the migration history and add incremental migrations for schema changes.

Recreate each active Preview database after its branch contains the baseline migration or a regenerated baseline. To recreate a Preview database:

  1. Close the trusted pull request.
  2. Wait until the Clean up Preview job succeeds and prints Deleted concourse-pr-<number>.
  3. Reopen the pull request.

If you reopen the pull request earlier, the new run cancels the cleanup and reuses the old database. If that database applied the old migration history, the baseline migration fails with table ... already exists. If it applied an earlier baseline, it keeps its old schema, and the Preview health check still passes.

A local development database has the same problem. If it applied the old migration history, pnpm db:migrate and pnpm dev report table ... already exists. If it applied an earlier baseline, pnpm db:migrate reports no error, but the database keeps its old schema. In both cases, run pnpm db:reset once, then run pnpm dev:setup to create the local accounts again.

Requirements

Make sure that you have:

  • Vercel team access to Conveyal and the concourse project.
  • A Turso Marketplace installation connected to that project.
  • A Resend account with mail.tredis.dev verified for sending.
  • GitHub administrator access to conveyal/concourse.
  • Node.js 24.21.0, pnpm 12 (the packageManager field in package.json pins the exact version), the GitHub CLI, and the Vercel CLI.

Vercel project

Keep these settings:

SettingValue
Projectconcourse
Root directoryapps/concourse
FrameworkNext.js
Node.js24.x
Function regioniad1
Production URLhttps://concourse.tredis.dev
Production branchmain

Vercel Git deploys every commit on main. GitHub Actions decides when a trusted pull request needs a Preview. It deploys through the same project after the branch database values are ready. The project has no Vercel ignore command, so Vercel builds every deployment that it receives.

The other Vercel apps use one ignore command. It always builds Production. It builds a Preview only when turbo query affected finds a change to the app's build task since the last successful deployment of that branch. Turbo includes the app's dependencies and the extra build inputs in the app's turbo.json, such as docs/tredis. A branch's first Preview always builds. When the shallow Vercel clone does not contain the previous commit, Turbo treats all files as changed, so the Preview builds.

Enable the Vercel system environment variables for Production and Preview. The application requires VERCEL_DEPLOYMENT_ID, VERCEL_GIT_COMMIT_SHA, and VERCEL_REGION=iad1. It uses VERCEL_PROJECT_PRODUCTION_URL as the production origin and VERCEL_BRANCH_URL as the stable Preview origin.

Turso resources

  1. Create one production database through the Vercel Marketplace integration.
  2. Keep its default 24-hour recovery window.
  3. Connect it only to the Production environment of the concourse project. Vercel stores and injects TURSO_DATABASE_URL and TURSO_AUTH_TOKEN.
  4. Keep the Marketplace installation on its free Starter plan.
  5. Record the Vercel installation ID as the GitHub variable VERCEL_TURSO_INSTALLATION_ID.

The preview workflows use the Vercel Marketplace API to create and remove resources. They do not need a Turso account or a Turso Platform API token.

GitHub variables and secrets

Set these repository variables:

  • VERCEL_PROJECT_ID
  • VERCEL_TEAM_ID
  • VERCEL_TURSO_INSTALLATION_ID

Set these repository secrets:

  • VERCEL_TOKEN
  • AUTH_AUDIT_FINGERPRINT_SECRET
  • BETTER_AUTH_SECRETS
  • PREVIEW_ADMIN_EMAIL
  • PREVIEW_ADMIN_NAME
  • PREVIEW_ADMIN_PASSWORD

The Vercel token must manage Marketplace resources, environment variables, and preview redeployments for the Conveyal team. Store it only in GitHub. No database credential is stored in GitHub.

Use preview-only values for the two authentication secrets. Configure the same two values in the Preview environment of Vercel. Do not reuse the production authentication secrets.

Resend resource

  1. Add mail.tredis.dev to the existing Resend account.
  2. Verify the domain with the Vercel DNS setup that Resend provides.
  3. Create a sending-only API key restricted to mail.tredis.dev.
  4. Store the key as the sensitive Vercel variable RESEND_API_KEY in Production and Preview.

Configure Production with:

EMAIL_PROVIDER=resend
AUTH_EMAIL_FROM=concourse@mail.tredis.dev
RESEND_TEST_RECIPIENT=<approved test address>

The application rejects every To, CC, and BCC recipient that does not match RESEND_TEST_RECIPIENT. Keep general-user email disabled until this allowlist policy is deliberately replaced.

Local development and browser tests continue to use Maildev.

Application environment

Configure the Production environment with the Turso values, the Resend values, and:

AUTH_TRUSTED_PROXY_IP_HEADERS=x-forwarded-for

Set strong values for AUTH_AUDIT_FINGERPRINT_SECRET and BETTER_AUTH_SECRETS.

CAUTION: Never copy a production secret into a preview, a log, an issue, a pull request, or a local environment file.

Configure the Preview environment with separate strong values for those two secrets, the Resend values, and the same AUTH_TRUSTED_PROXY_IP_HEADERS value. The preview database workflow adds only the branch-specific Turso values. Every Preview deployment also receives the Rackspace QA values below, so an administrator can run the audited connection check.

Rackspace values

Before you enable Rackspace access, read the Rackspace security and operations pages. Schema generation runs only on an operator laptop through the approved VPN. A Vercel deployment reads merged snapshots. It does not receive Drizzle Kit, generation configuration, or schema publication credentials.

Configure these sensitive values with the same shared account in Preview and Production:

RACKSPACE_MYSQL_HOST=<reviewed host>
RACKSPACE_MYSQL_PORT=3306
RACKSPACE_RUNTIME_USERNAME=<shared account>
RACKSPACE_RUNTIME_PASSWORD=<shared password>

Configure Preview with:

RACKSPACE_ENVIRONMENT=qa
RACKSPACE_RUNTIME_DATABASE=mi_property_value_qa

Configure Production with:

RACKSPACE_ENVIRONMENT=production

Do not set RACKSPACE_RUNTIME_DATABASE in Production. The project Static IPs apply to both scopes.

Preview exposes only an audited fixed-QA connection check. Production exposes only the durable full-registry verifier. Development exposes neither.

Preview lifecycle

.github/workflows/preview-database.yml accepts only pull requests whose head repository is conveyal/concourse. A fork pull request never receives credentials or a database.

The workflow runs for every trusted pull request update. Its required Preview health aggregate is always created. Before it changes any resources, it checks whether the Concourse build or Preview provisioning inputs changed:

  • With no preview database, it compares the complete pull request against its merge base.
  • With an existing database, it checks the latest Preview for that branch. The deployment must be ready and identify the current database, pull request, branch, and commit. Both branch database variables must exist, and the deployment must pass its readiness check. The workflow compares the new commit directly against that deployed commit, so reversions and canceled runs cannot hide a required update. If rewritten history removes the deployed commit from the branch, any changed files require a deployment because Turbo cannot safely compare that history. Identical trees can reuse the Preview.
  • Missing database variables, a pending or failed latest Preview, or unavailable comparison history require a deployment. The workflow does not reuse an older ready Preview while a newer one can still finish. API and Turbo query errors fail the check.

Turbo checks the Concourse build and its dependencies. Changes to the Preview scripts, Preview workflows, or database migrations also require an update. An unaffected update completes Preview health successfully without changing resources. It keeps any existing database and Preview. Keep the workflow active for all trusted pull requests so this required check can complete even when a deployment is unnecessary. Documentation-only updates use the same safe no-op decision, unless an existing Preview requires the exact-commit safety checks.

The workflow also supports a manual Preview rebuild. Enter an open pull request number for a trusted internal pull request. The workflow reads the pull request before it changes resources and rejects missing, closed, invalid, or fork pull requests before checkout. It checks out the exact head commit before provisioning. A manual rebuild bypasses the unchanged-build decision and creates a fresh deployment. It does not bypass trust or checkout validation.

When an update is required, the workflow:

  1. Creates or reuses the free Marketplace resource concourse-pr-<number>.
  2. Connects that resource to the Development environment of Vercel with a unique prefix, pulls the credentials into an isolated temporary file, and disconnects the resource again.
  3. Applies the migrations, then runs the Auth native Turso proof process against that database with its private CI credentials. This proof checks remote rollback, audit atomicity, foreign keys, and Better Auth coordination. It removes its test records before closing the connection.
  4. If the proof passes, seeds the fixtures and creates the preview system administrator with pnpm auth:bootstrap-preview.
  5. Stores the branch-specific Turso values in Vercel.
  6. Creates a fresh Preview from the exact pull request branch and commit.

A failed proof stops the workflow before seeding, administrator bootstrap, or deployment. Local development still uses SQLite and requires no Turso credentials.

The deployment records its database, pull request, branch, and commit in Vercel metadata. A Preview from before this metadata was added requires one new deployment before it can be reused. Reopening a pull request after database cleanup uses the complete pull request comparison again.

When it creates a deployment, the Preview health check waits for that exact deployment to become ready. Then it calls the /api/health/ready endpoint of the deployment. The endpoint checks database access and the migration ledger. Require this check on main, so that a build that cannot use its preview database cannot merge.

PREVIEW_ADMIN_PASSWORD is the only source of the Preview administrator password. Preview has no default password. The bootstrap also gives the administrator an owner membership in Template Organization, and creates passwordless organization admin and member users for impersonation tests.

When the pull request closes, the workflow removes the branch values and the Marketplace resource. Missing values or resources count as successful cleanup, including a pull request that never needed a Preview. The daily preview-orphans.yml workflow removes any resource that was missed.

Production migration gate

Add these two GitHub results as Vercel Deployment Checks that block production alias assignment:

  • The CI / verify job.
  • The Concourse production migration commit status.

Keep automatic production aliasing enabled. The app's Vercel build command runs the uncached deployment:migrate task before the application build. Only VERCEL_ENV=production applies migrations. Preview and local builds skip this task before reading database configuration.

The task uses the Turso credentials injected by Vercel. It rejects missing credentials, redacted placeholders, and local database URLs. It applies pending committed migrations and checks the migration ledger. The database connection closes after success or failure. A failed migration stops the build. The task runs even when Turbo restores the application build from cache. Every commit on main builds, so a migration-only commit also applies its migrations.

After the build succeeds, Vercel sends vercel.deployment.ready to GitHub. The production workflow then:

  1. Validates the repository, project, branch, and exact commit.
  2. Calls the readiness endpoint of that exact deployment, without following redirects.
  3. Requires HTTP 200 and the expected ready response.

The workflow reads no database credentials. The existing Concourse production migration status name remains unchanged so configured Deployment Checks continue to match it.

Vercel assigns the production alias only after both checks pass.

The Marketplace integration does not provide Turso Platform API access. The migration therefore uses the same database credential that Vercel injects into the application build. When a rotation is required, rotate that credential through the Marketplace resource.

A migration can commit before the application build or a later check fails. Keep each migration compatible with the currently serving application and any rollback target. A build failure does not roll back a completed migration. Preview keeps its existing migration, remote proof, fixture seed, and administrator bootstrap sequence before deployment.

See ADR 0013 for migration ownership.

First production administrator

After the first production migration succeeds, load the production environment and run:

pnpm auth:create-admin

Create the first systemAdmin through the prompts. Do not put its password on a command line or in a workflow.

Recovery

Open the Turso dashboard through Vercel Marketplace to do a recovery. The Marketplace workflow cannot automate a point-in-time restore without Turso Platform API access.

If the dashboard permits a temporary restore:

  1. Apply no writes to the active production database.
  2. Make sure that the restored schema and the administrator record are correct.
  3. Erase only the temporary restore database.

Use expand-contract migrations for ordinary releases. A rollback target must support the current database schema. See Rackspace rollback compatibility.

Completion checks

The setup is complete when:

  • pnpm install --frozen-lockfile && pnpm verify passes on Linux CI.
  • A trusted pull request completes create, redeploy, reuse, health verification, and removal.
  • An unaffected pull request passes Preview health and closes without creating a database.
  • The required Preview health check passes for the exact pull request commit, either through a verified deployment or a successful decision to skip it.
  • The production migration Deployment Check passes for the exact Vercel commit.
  • Liveness, readiness, authentication, organization access, and member administration pass.
  • One Resend message reaches only the configured test recipient.
  • A manual Turso recovery check is recorded, when the Marketplace dashboard permits a temporary restore.
  • The Vercel logs contain no database, authentication, or email errors.