CI/CD database workflow

Every PR gets
its own database

Automatically spin up an isolated, production-data database branch for every pull request. No shared staging conflicts. No stale data. Deleted on merge.

The shared staging problem

Most teams test database changes against a single shared staging environment. This works when there's one developer. It breaks down the moment multiple PRs are open simultaneously.

Shared staging conflicts

Two developers test simultaneously and overwrite each other's data. QA can't reproduce a bug because the data was changed by someone else.

Slow environment setup

Spinning up a separate test database takes 30+ minutes with pg_dump/restore. So teams skip it and test on shared staging instead — which leads to the problem above.

Stale test data

The staging database was last refreshed 3 weeks ago. Features that depend on recent production data fail in staging but work in prod — or the reverse.

How branch-per-PR works

Each step happens automatically in your CI pipeline

1

PR opened

Developer opens a pull request. CI pipeline starts.

2

Database branch created

Pipeline calls Vela API to create a CoW branch from the latest production snapshot. Available in < 30 seconds regardless of DB size.

3

Migrations applied

Any schema migrations in the PR are applied to the branch database — not to staging or production.

4

Tests run

Integration tests, E2E tests, and QA reviews all point to the isolated branch database. No conflicts with other PRs.

5

PR merged or closed

Pipeline deletes the branch database. Storage freed. No cleanup scripts needed.

GitHub Actions example

A minimal workflow that creates a Vela database branch for each PR, runs tests, and cleans up on completion:

name: PR Database Branch

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

jobs:
  test-with-db-branch:
    if: github.event.action != 'closed'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Create database branch
        id: db-branch
        run: |
          BRANCH_NAME="pr-${{ github.event.pull_request.number }}"
          RESPONSE=$(curl -s -X POST https://api.vela.run/v1/branches \
            -H "Authorization: Bearer ${{ secrets.VELA_API_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{"source": "production", "name": "'"$BRANCH_NAME"'"}')
          echo "db_url=$(echo $RESPONSE | jq -r .connection_string)" >> $GITHUB_OUTPUT

      - name: Run migrations
        env:
          DATABASE_URL: ${{ steps.db-branch.outputs.db_url }}
        run: npm run db:migrate

      - name: Run integration tests
        env:
          DATABASE_URL: ${{ steps.db-branch.outputs.db_url }}
        run: npm test

  cleanup-db-branch:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Delete database branch
        run: |
          BRANCH_NAME="pr-${{ github.event.pull_request.number }}"
          curl -s -X DELETE https://api.vela.run/v1/branches/$BRANCH_NAME \
            -H "Authorization: Bearer ${{ secrets.VELA_API_TOKEN }}"

Actual API shape may differ — see Vela docs for current reference.

Branch-per-PR vs. the alternatives

Dimension Branch per PR Shared staging Separate env per PR
Data freshness Production snapshot (always current) Manual refresh — often weeks stale Manual setup — depends on team
Isolation Each PR fully isolated Shared — conflicts between PRs Isolated but expensive
Setup time < 30 seconds (CoW branch) Already running (but problematic) 30+ min (pg_dump/restore)
Storage cost Near-zero (shared blocks) One DB (but always conflicted) N × full DB size
Scales to 50 PRs Yes No Very expensive
Deleted automatically Yes, on merge/close N/A Manual cleanup

Frequently Asked Questions

What does 'branch per PR' mean for databases?

Branch per PR (or database preview environments) means that each pull request automatically gets its own isolated database copy — created at the start of the CI pipeline and deleted when the PR is merged or closed. This gives every PR a consistent, isolated, production-like database without conflicts with other open PRs or with the shared staging environment.

How do I connect my CI pipeline to Vela's branching API?

Vela exposes a REST API for database branch management. In your CI pipeline (GitHub Actions, GitLab CI, CircleCI, etc.), you add steps to call the API at the start of the job (create branch) and at the end (delete branch). The API returns a connection string for the new branch database that your tests can use.

Does this work for large production databases?

Yes — this is a key advantage of storage-level copy-on-write branching. The branch creation time is constant regardless of database size because no data is copied. A 500 GB database creates a branch in the same time as a 1 GB database.

What happens to migrations in a branch-per-PR workflow?

Schema migrations included in the PR are applied to the branch database during the CI job, not to production or shared staging. This lets you test your migration against real production data safely. If the migration causes issues, only the branch is affected — it's discarded.

How is this different from just using Docker Compose for local database testing?

Docker Compose gives you an empty database (or one seeded with synthetic fixtures). Branch-per-PR gives you a full copy of production data — real customer records, real data distributions, real edge cases. Tests that pass against empty or synthetic data can still fail against production data due to unexpected data shapes, constraint violations, or volume-related performance issues.

Stop sharing staging. Branch instead.

Try Vela's database branching in the sandbox — no infrastructure required.

Try Vela Sandbox Free