CapyDB/ docs
GuidesIntegrations

GitHub Action

A disposable preview database for every pull request - idempotent create, masked outputs, TTL backstop, schema diffs and generated types.

What it does

capydatabase/github-action@v1 creates a CapyDB preview database for a pull request, waits for it to be ready, and exports the connection strings into the job environment. It talks to the API with curl + jq (preinstalled on GitHub runners) - nothing to install, nothing to cache.

Usage

.github/workflows/preview-database.yml
name: preview-database

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

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - name: Create preview database
        if: github.event.action != 'closed'
        id: capydb
        uses: capydatabase/github-action@v1
        with:
          api-key: ${{ secrets.CAPYDB_API_KEY }}
          project-id: ${{ vars.CAPYDB_PROJECT_ID }}

      - name: Run migrations + tests against the preview
        if: github.event.action != 'closed'
        run: |
          pnpm db:migrate
          pnpm test
        # DATABASE_URL / DATABASE_URL_UNPOOLED are already exported by the
        # action (write-env defaults to true).

      - name: Delete preview database
        if: github.event.action == 'closed'
        uses: capydatabase/github-action@v1
        with:
          api-key: ${{ secrets.CAPYDB_API_KEY }}
          project-id: ${{ vars.CAPYDB_PROJECT_ID }}
          command: delete

Inputs

InputDefaultDescription
api-key- (required)CapyDB API key. Use a project-scoped key stored in repo secrets.
project-id- (required)The CapyDB project the preview belongs to.
commandcreatecreate (idempotent), reset, delete, schema-diff, or generate-types.
github-token-Token for schema-diff / generate-types to post and update their PR comment (secrets.GITHUB_TOKEN with pull-requests: write).
namepr-<number> / branch slugPreview name; re-running with the same name reuses the database.
modecloneclone copies production data; empty starts blank.
ttl-hours72Auto-delete after this many hours (1–168).
api-urlhttps://capydb.dev/api/capydbOverride for self-hosted control planes.
wait-timeout600Seconds to wait for the preview to become ready.
write-envtrueExport DATABASE_URL / DATABASE_URL_UNPOOLED into the job env.
types-languagetypescriptgenerate-types: typescript, zod, or drizzle.
types-styleAPI default (capydb)generate-types, TypeScript only: capydb or supabase.
types-pathgenerator's file namegenerate-types: file to write, e.g. src/db/database.types.ts.
types-sourcepreviewgenerate-types: preview (this PR's preview) or project (production).
types-commitfalsegenerate-types: commit and push the file when it changed.

Outputs

OutputDescription
preview-idID of the preview database.
preview-nameResolved preview name.
database-urlPooled connection string (masked).
database-url-unpooledDirect connection string (masked).
schema-changedschema-diff only: true when the preview's schema differs from production.
types-pathgenerate-types only: the file that was written.
types-changedgenerate-types only: true when the generated file differs from the one on disk (or is new).
types-committedgenerate-types only: true when the file was committed and pushed.

Schema diff on pull requests

After your migrations run against the preview, a schema-diff step compares the preview's schema to production and keeps one PR comment updated with the unified diff, plus a step summary and the schema-changed output. Connection strings never appear in it.

permissions:
  pull-requests: write

steps:
  - uses: capydatabase/github-action@v1
    with:
      api-key: ${{ secrets.CAPYDB_API_KEY }}
      project-id: ${{ vars.CAPYDB_PROJECT_ID }}

  - run: pnpm drizzle-kit migrate # or your migration tool, against DATABASE_URL_UNPOOLED

  - uses: capydatabase/github-action@v1
    with:
      api-key: ${{ secrets.CAPYDB_API_KEY }}
      project-id: ${{ vars.CAPYDB_PROJECT_ID }}
      command: schema-diff
      github-token: ${{ secrets.GITHUB_TOKEN }}

Generated types on pull requests

generate-types calls the same generator as capydb generate and writes TypeScript interfaces, Zod schemas, or a Drizzle schema into the workspace - generated from the preview's schema after your migrations ran, or from production with types-source: project. It reports whether the file changed as the types-changed output, in the step summary, and - with a github-token - in its own PR comment, with the diff against the file on the branch.

Fail the job when the committed types lag the schema:

- uses: capydatabase/github-action@v1
  id: types
  with:
    api-key: ${{ secrets.CAPYDB_API_KEY }}
    project-id: ${{ vars.CAPYDB_PROJECT_ID }}
    command: generate-types
    types-path: src/db/database.types.ts
    github-token: ${{ secrets.GITHUB_TOKEN }}

- if: steps.types.outputs.types-changed == 'true'
  run: |
    echo "::error::src/db/database.types.ts is out of date; see the PR comment"
    exit 1

Or commit the regenerated file to the PR branch with types-commit: true. That needs contents: write and a branch checkout: on pull_request events actions/checkout checks out a detached merge commit by default, and the action refuses to commit on top of it.

permissions:
  contents: write
  pull-requests: write

steps:
  - uses: actions/checkout@v7
    with:
      ref: ${{ github.head_ref }}

  # ... create the preview and run your migrations ...

  - uses: capydatabase/github-action@v1
    with:
      api-key: ${{ secrets.CAPYDB_API_KEY }}
      project-id: ${{ vars.CAPYDB_PROJECT_ID }}
      command: generate-types
      types-path: src/db/database.types.ts
      types-commit: true
      github-token: ${{ secrets.GITHUB_TOKEN }}

The commit contains only the generated file and is authored by github-actions[bot]. A push made with GITHUB_TOKEN starts no new workflow run, so it cannot loop; it also cannot push to a fork, so leave types-commit off for PRs from forks. Both schema-diff and generate-types need an API key with schema:read (or projects:read, which project-scoped keys carry by default).

Behavior worth knowing

  • create is idempotent: pushing more commits to the same PR reuses the existing preview and extends its TTL instead of recreating it. Use command: reset if you want a fresh clone on every push.
  • Masked outputs: connection strings are registered with ::add-mask:: so they never appear in logs.
  • TTL backstop: previews expire via their TTL even if the closed job never runs (force-pushed branch, cancelled workflow), so a missed cleanup cannot leak databases forever.
  • Key scoping: create the API key in the dashboard under Settings → API keys with a project_id, scoped to the single project the workflow uses. A leaked project-scoped key cannot touch sibling projects or the org. See Authentication.
  • Ordering: unlike the Vercel/Netlify deploy-webhook flow, the Action is synchronous - database first, then your steps. Use it when build steps need the database to exist.