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.
With branches (recommended)
Section titled “With branches (recommended)”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.
With CLI fork (older pattern)
Section titled “With CLI fork (older pattern)”- 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 }} --forceForks are not idempotent — re-running the create step on a later commit returns 409. The branches API is preferred for that reason.
Why this is useful
Section titled “Why this is useful”- Migration safety. Your
ALTER TABLEruns 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 semantics
Section titled “TTL semantics”- TTL is in whole days, capped at 30. Wider ranges aren’t supported.
- The daily 04:00 UTC cron deletes any database whose
expiresAtis 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
PUTresets the TTL (or clears it if you omitttlDays). For raw forks, TTL is set at creation and can’t be renewed.
Variations
Section titled “Variations”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.
Caveats
Section titled “Caveats”- 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
Managepermission and rotate it periodically.
See also
Section titled “See also”- Branches — the underlying API
- Forking — the underlying primitive
- Migrations — running schema changes per-PR
- API tokens — scoping tokens for CI use