Skip to content

PR preview databases

For every pull request, run your tests against a fresh fork of prod — schema, data, indexes, the lot — that disappears on its own when the PR merges or rots. No “shared staging that everyone steps on,” no empty-DB tests that miss real-data bugs, no manual cleanup.

The cleanest way is the Branches API, which gives you idempotent create-or-reset by ref. The older fork-and-delete pattern still works.

.github/workflows/preview-db.yml
name: Preview DB
on:
pull_request:
types: [opened, reopened, synchronize, closed]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Create or reset PR branch
if: github.event.action != 'closed'
run: |
curl -fsS -X PUT \
-H "Authorization: Bearer ${{ secrets.PERSQL_TOKEN }}" \
-H 'Content-Type: application/json' \
-d '{"ttlDays": 7}' \
https://api.persql.com/api/namespaces/acme/databases/main/branches/pr-${{ github.event.number }}
- name: Run tests against the preview
if: github.event.action != 'closed'
env:
DATABASE_URL: https://api.persql.com/v1/db/acme/main-pr-${{ github.event.number }}
DATABASE_TOKEN: ${{ secrets.PERSQL_TOKEN }}
run: |
npm ci
npm run migrate
npm test
- name: Tear down on close
if: github.event.action == 'closed'
run: |
curl -fsS -X DELETE \
-H "Authorization: Bearer ${{ secrets.PERSQL_TOKEN }}" \
https://api.persql.com/api/namespaces/acme/databases/main/branches/pr-${{ github.event.number }}

The PUT is idempotent: first push creates the branch from a fresh parent dump; every later push resets it. The TTL is a belt-and- braces fallback in case the close webhook is missed — the daily cron sweeps any branch whose expiresAt has passed.

- name: Fork prod for this PR
if: github.event.action != 'closed'
run: |
persql login --token ${{ secrets.PERSQL_TOKEN }}
persql db fork acme/app pr-${{ github.event.number }} --ttl 7d
- name: Tear down on close
if: github.event.action == 'closed'
run: |
persql login --token ${{ secrets.PERSQL_TOKEN }}
persql db delete acme/pr-${{ github.event.number }} --force

Forks are not idempotent — re-running the create step on a later commit returns 409. The branches API is preferred for that reason.

  • Migration safety. Your ALTER TABLE runs against real data shapes, not fixtures. You catch “this took 12 seconds on prod-sized rows” before merging.
  • Reproducing prod bugs. “User 12345 reports X” — branch prod, reproduce, fix, verify. The branch dies in a week.
  • Performance. 100-row test fixtures don’t tell you a query is N², 5M-row prod data does.
  • No shared staging conflicts. Each PR gets its own DB; nobody’s WIP migration breaks anybody else’s tests.
  • TTL is in whole days, capped at 30. Wider ranges aren’t supported.
  • The daily 04:00 UTC cron deletes any database whose expiresAt is in the past — at most 50 per tick so a backlog can’t starve the worker.
  • Deletion is a hard drop: the Durable Object is destroyed and the registry row is removed. Tokens that referenced the database return 404 from the next call.
  • For branches, every PUT resets the TTL (or clears it if you omit ttlDays). For raw forks, TTL is set at creation and can’t be renewed.

Schema-only previews. If your tests don’t need real data, branch an empty database that you keep around as a “schema source of truth” and run your migrations on the branch. Faster, smaller, free- tier friendly.

Per-feature branches. Same pattern but use a feature branch name (feat/checkout-redesign) as the ref instead of a PR number. Add your own teardown step when the branch is deleted.

Local mirror. persql db export <branch> dumps SQL you can apply to a local SQLite. Handy when CI fails and you want to debug interactively.

  • A branch pays its own storage. Big production databases (multi-GB) branched per PR will show up on the bill — see Pricing & billing.
  • Creating a branch briefly pauses writes on the parent while we read; in practice this is hundreds of milliseconds.
  • API tokens are namespace-scoped, so the same token authenticates the PR branch as authenticates prod. Most teams use a CI-only token with Manage permission and rotate it periodically.