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:
- Close the trusted pull request.
- Wait until the
Clean up Previewjob succeeds and printsDeleted concourse-pr-<number>. - 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
concourseproject. - A Turso Marketplace installation connected to that project.
- A Resend account with
mail.tredis.devverified for sending. - GitHub administrator access to
conveyal/concourse. - Node.js 24.21.0, pnpm 12 (the
packageManagerfield inpackage.jsonpins the exact version), the GitHub CLI, and the Vercel CLI.
Vercel project
Keep these settings:
| Setting | Value |
|---|---|
| Project | concourse |
| Root directory | apps/concourse |
| Framework | Next.js |
| Node.js | 24.x |
| Function region | iad1 |
| Production URL | https://concourse.tredis.dev |
| Production branch | main |
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
- Create one production database through the Vercel Marketplace integration.
- Keep its default 24-hour recovery window.
- Connect it only to the Production environment of the
concourseproject. Vercel stores and injectsTURSO_DATABASE_URLandTURSO_AUTH_TOKEN. - Keep the Marketplace installation on its free Starter plan.
- 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_IDVERCEL_TEAM_IDVERCEL_TURSO_INSTALLATION_ID
Set these repository secrets:
VERCEL_TOKENAUTH_AUDIT_FINGERPRINT_SECRETBETTER_AUTH_SECRETSPREVIEW_ADMIN_EMAILPREVIEW_ADMIN_NAMEPREVIEW_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
- Add
mail.tredis.devto the existing Resend account. - Verify the domain with the Vercel DNS setup that Resend provides.
- Create a sending-only API key restricted to
mail.tredis.dev. - Store the key as the sensitive Vercel variable
RESEND_API_KEYin 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-forSet 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_qaConfigure Production with:
RACKSPACE_ENVIRONMENT=productionDo 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:
- Creates or reuses the free Marketplace resource
concourse-pr-<number>. - 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.
- 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.
- If the proof passes, seeds the fixtures and creates the preview
system administrator with
pnpm auth:bootstrap-preview. - Stores the branch-specific Turso values in Vercel.
- 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 / verifyjob. - The
Concourse production migrationcommit 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:
- Validates the repository, project, branch, and exact commit.
- Calls the readiness endpoint of that exact deployment, without following redirects.
- Requires HTTP 200 and the expected
readyresponse.
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-adminCreate 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:
- Apply no writes to the active production database.
- Make sure that the restored schema and the administrator record are correct.
- 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 verifypasses on Linux CI.- A trusted pull request completes create, redeploy, reuse, health verification, and removal.
- An unaffected pull request passes
Preview healthand closes without creating a database. - The required
Preview healthcheck 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.