Docs / Branches

Branches

Disposable, isolated Postgres databases cut from a privacy-safe copy of your production data. A branch is ready in seconds, costs your production database nothing, and is gone when you delete it.

Overview

A branch is a real PostgreSQL server with its own connection string, created from a Safe Copy of your production database. Use one wherever you'd rather not touch anything that matters:

  • Migrations and schema changes: run db:migrate against real data before you merge.
  • Debugging: reproduce a bug that only shows up with production-shaped data.
  • Coding agents: give Claude Code (or any agent) its own database for a task, then throw it away.
  • CI and previews: one fresh branch per job or pull request.

Branches are copy-on-write: a new branch shares storage with its parent, so it's ready in seconds whatever the size of your data, and it only uses extra disk for the rows you change. Every branch runs in its own Postgres process, so changes in one never show up in another.

🧪
Private beta. Branches are enabled per account. If Branches doesn't appear in your dashboard sidebar, get in touch and we'll turn it on.

How it works

Four terms come up throughout:

TermWhat it is
ProjectA connected production database and everything branched from it. A project's name is its slug in URLs, the CLI and the API.
Safe CopyThe protected copy pgrun makes of production: schema and approved rows, with sensitive columns faked, hashed or removed. Branches are always created from the Safe Copy, never from production directly.
Base branchThe branch that fronts the Safe Copy, usually called main. It has no connection string and can't be deleted; you create branches from it.
BranchA writable, point-in-time clone of the base branch or of another branch, with its own credentials and an optional expiry.

Quickstart

Setting up a project takes one visit to the dashboard. Open Set up in the sidebar and follow the four steps:

  1. Connect Postgres: paste a connection string, or connect a database you already run on pgrun.
  2. Protect Data: review what pgrun couldn't decide on its own, then approve the policy.
  3. Create Safe Copy: one click. Production is only read.
  4. Create a branch: from the dashboard, the CLI, or let your coding agent do it.

After that, a branch is one command away:

curl -fsSL https://pgrun.dev/install | sh
pgrun auth login                     # paste a token from the dashboard's Tokens page
pgrun project use my-project         # remember the project for this repo

pgrun branch create --name try-index --ttl 1h --wait
# DATABASE_URL=postgresql://…@br-….us.db.pgrun.dev:5432/postgres?sslmode=require

pgrun branch delete try-index

Or run one command against a fresh branch and clean up automatically:

pgrun branch exec --create try-migration --ttl 1h --delete-after -- bin/rails db:migrate

1 · Connect Postgres

In Set up → Connect Postgres, either click Connect next to a database you already run on pgrun, or open Connect existing Postgres and paste a connection string. RDS, Aurora, Cloud SQL, Azure, Supabase, Neon and self-hosted Postgres all work.

postgres://readonly_user:PASSWORD@db.example.com:5432/myapp?sslmode=require
  • A read-only role is enough. It needs to connect and to SELECT from every table you want copied. pgrun forces the session read-only before it runs any SQL.
  • Allow pgrun through your firewall. The connect form lists the IP addresses to allow.
  • PostgreSQL 16, 17 or 18 is required to create a Safe Copy. Branches run the same major version as your production database.
  • One database per project. pgrun copies the database named in the connection string. Connect each database you want to branch as its own project.

Connecting runs a read-only check: the Postgres version, database size, tables, extensions, and which columns look sensitive. No rows are copied at this step. The connection string is stored encrypted and is never shown again, never returned by the API, and never copied into a branch.

🔌
"Connection check failed"? The project page shows the error and a form to update the connection string. Check the password, sslmode, and that your firewall allows the listed addresses.

2 · Protect Data

Before any production row is copied, you approve a protection policy covering every column. pgrun handles the obvious cases and asks you about the rest. Nothing is copied until every column is decided and the policy is approved.

What pgrun decides on its own

  • Protects columns whose names say they're personal or secret: emails, phone numbers, passwords, tokens, addresses, dates of birth, card and bank numbers, and similar.
  • Copies a column only when its structure shows it can't hold personal data: non-text types, enums, columns limited to a fixed list by a CHECK constraint, keys, and a small set of conventional status-style names confirmed by table statistics.
  • Never copies free text on its name alone. A text column called notes or diagnosis waits for you.

Step 1: tables

pgrun suggests excluding the data of tables that look like logs, job queues or session storage. Exclude data keeps the table's structure, so migrations and tests still work, but copies no rows. One decision resolves every column in the table.

Step 2: columns

Columns that still need a decision are grouped, largest risk first, so one answer can cover many similar columns. For each, pick:

DecisionIn your branchesAvailable for
CopyThe production value, unchanged.Any column
FakeA synthetic placeholder such as pgrun_fake_3f9a…. The same input always gives the same output.Text columns
HashA keyed digest of the value. Equal values stay equal, so joins and uniqueness keep working.Text columns
RemoveNULL, or a placeholder when the column is NOT NULL text.Nullable or text columns

Emails that pgrun protects automatically become addresses like user_3f9a…@example.test, so email-shaped constraints still pass. Copying a column pgrun flagged as sensitive needs an explicit acknowledgment. When every column is resolved, approve the policy. That creates policy version v1, then v2 on the next approval, and so on.

Optional: production data access

By default pgrun decides from the schema and catalog statistics alone, and never reads your values. Two permissions on the project page let it decide more columns for you. Both are off until an owner or admin turns them on:

PermissionWhat it allows
Allow bounded production samplingReads a small sample of text values in memory to recognize closed formats, such as country codes, that are safe to copy. Values are never stored, shown or returned.
Allow deep verification scansRead-only, time-limited full-column scans that prove a format holds on every row before pgrun copies the column. Requires sampling. Scans may add temporary read load, and pgrun backs off when the database is busy.

Declining either permission never makes protection weaker. Columns that would have needed it stay in your review list.

🛡️
Weakening a policy needs a reason. If you change an approved policy so it protects less, for example Remove → Copy or including an excluded table again, re-approving asks you to write down why. The reason is stored with the new policy version.

3 · Create the Safe Copy

Once the policy is approved, click Create Safe Copy on the project page (or run pgrun source copy --wait). pgrun then:

  1. checks that production's schema still matches the approved policy, and refuses if it doesn't;
  2. copies the schema;
  3. copies the rows of every table you kept, with protection applied inside the query that reads production, so a protected value never leaves your database;
  4. brings sequences up to their production values;
  5. scans the result for known sensitive values and fails the copy if any got through.

It takes about as long as reading your database once: roughly a minute for a 100 MB database. When the badge reads Ready, the project appears under Branches with a base branch called main.

What gets copied

Copied

  • Every schema, table, index, constraint, view, function and trigger in the database
  • Rows of tables you kept, with your protection rules applied
  • Sequence positions
  • Extensions your schema uses (the connection check lists them and flags any that need review)
  • Migration bookkeeping (schema_migrations, ar_internal_metadata), always, so db:migrate knows where it is

Not copied

  • Roles, passwords, ownership and grants
  • Rows of excluded tables (their structure is kept)
  • Publications and subscriptions
  • Materialized view contents: the views exist but stay empty until you run REFRESH MATERIALIZED VIEW
  • Other databases on the same server

In a branch, everything lives in the database called postgres, whatever your production database is called.

When production changes

A Safe Copy is a point-in-time snapshot. It doesn't follow production, and changes to production never reach the Safe Copy or its branches.

  • Schema changed before the copy? pgrun won't copy under an out-of-date policy. It compares production's live schema with the approved one right before copying and refuses on any difference. When the project shows Action required, Protect Data lists the added, removed and retyped columns, classifies what it can, and asks you to decide the rest and re-approve. If the Safe Copy shows Failed instead, email support.
  • Schema changed after the copy? The Safe Copy and your branches keep the schema they were made with. Run the new migrations on a branch; that's what branches are for.
  • Need fresher data? Refreshing a Safe Copy isn't self-serve yet. Email support@postgresrun.com. A refresh requires deleting the project's branches first.

Create a branch

From the dashboard: Branches → your project → Create branch, then choose:

FieldNotes
Branch fromThe Safe Copy (main) or any ready branch in the project.
Branch name3–63 characters: lowercase letters, numbers and hyphens, starting with a letter and ending with a letter or number. Names are unique within a project and are never reused, even after a branch is deleted.
Time to liveNever, 1 hour, 6 hours, 24 hours or 7 days. See Expiry & deletion.

The page shows progress while the branch is created (usually a few seconds) and then its connection details. The same thing from the CLI:

pgrun branch create my-project --name feature-auth --ttl 24h --wait

Connect to a branch

A ready branch shows its host, port, database and a copyable connection string:

postgresql://USER:PASSWORD@br-<id>.us.db.pgrun.dev:5432/postgres?sslmode=require

Use it anywhere that accepts a DATABASE_URL. With the CLI:

pgrun branch url my-project feature-auth          # DATABASE_URL=postgresql://…
eval "$(pgrun branch env my-project feature-auth)" # exports DATABASE_URL in this shell
pgrun branch exec my-project feature-auth -- psql   # runs one command with DATABASE_URL set
  • Use the URL as given. Every branch has its own stable hostname on port 5432. Don't swap in an IP address or a different port.
  • TLS is required. Unencrypted connections are refused. The certificate is publicly trusted, so sslmode=verify-full works with your system's root certificates.
  • Don't set channel_binding=require. TLS ends at pgrun's gateway in front of the branch, so the server can't offer channel binding. Password authentication (SCRAM) still runs end to end.
  • Full control. The branch's role is a superuser on that branch only, so you can create extensions, roles and schemas freely.
  • Reset password on the branch page sets a new password; the old connection string stops working.
🔐
Checking TLS? Check from the client, e.g. \conninfo in psql. Inside the branch, pg_stat_ssl reports false because the last hop from the gateway to Postgres stays on the same machine. Your connection is still encrypted all the way to that machine.
🔑
A branch's connection string is a live credential for as long as the branch exists. Keep it out of commits, logs and PR comments. The API only returns it for a single ready branch, never in lists.

Branch from a branch

Click Create branch from this branch on any ready branch, or pass --parent:

pgrun branch create my-project --name orders-v2-try2 --parent orders-v2 --wait

The child starts as an exact copy of the parent at the moment you create it; later changes on either side stay separate. A branch with live children can't be deleted, so delete the children first.

Expiry & deletion

  • Time to live is set once, at creation: 1h, 6h, 24h or 7d, or none. Expired branches are deleted automatically by a sweep that runs every 10 minutes. There's no way to extend a TTL; create a new branch instead.
  • Delete from the branch page, with pgrun branch delete, or with DELETE on the API. Deletion is asynchronous and permanent. The branch's data and credentials are destroyed.
  • The base branch can never be deleted, and neither can a branch that still has children.
  • Deleted and expired branches stay in the project's list, dimmed, so you can see what existed.
⏱️
Always set a TTL for automated branches. If a CI job or agent crashes before it cleans up, the TTL does it for you.

Statuses

StatusMeaning
creatingRequest accepted.
snapshottingTaking a point-in-time snapshot of the parent.
provisioningCloning the snapshot into the new branch.
startingStarting Postgres and creating the branch's credentials.
readyConnectable. The connection string is available.
failedCreation failed. The branch page shows why. Delete it and create a new branch under a new name; a failed branch doesn't recover. A branch that doesn't finish within 45 minutes is marked failed.
deletingBeing removed.
deletedGone. The name stays reserved.

ready and failed are the only final outcomes of a create. Anything else means it's still in progress.

Limits

LimitValue
Live branches per project10, not counting the base branch. Failed branches count until you delete them.
Branch names3–63 characters, a-z, 0-9, -; never reused within a project.
Time to liveNone, 1h, 6h, 24h or 7d.
PostgreSQL versions16, 17 and 18, matching production.
Databases per projectOne: the database in the connection string.
RegionSafe Copies and branches are hosted in the US.
Safe Copy refreshSnapshot only; refresh through support.
API rate limit120 requests per minute per token.

Claude Code

The fastest way to use branches is to let your coding agent create them. The pgrun-branching skill teaches Claude Code when a task needs a real database, how to hand the branch's DATABASE_URL to only the commands that need it, and to delete the branch when it's done.

curl -fsSL https://pgrun.dev/install | sh
pgrun auth login              # prompts for a token, verifies it, saves it
pgrun skill install           # installs the skill into ~/.claude/skills
cd your-app
pgrun projects list
pgrun project use my-project  # writes .pgrun/project so Claude branches the right project

Then ask for a database change as usual:

Add an index to users.email, run the migration and tests using a pgrun database branch.

Claude creates a branch with a TTL, runs your migration against it with DATABASE_URL set for that command only, checks the result, and deletes the branch. Rails, Django, Prisma and anything else that reads DATABASE_URL work without config changes. The skill never edits database.yml or .env, never asks you to paste a token into the chat, and never approves a protection policy for you.

🪪
Use a token from the account that owns the project. Tokens are account-scoped: a token from another account gets 404 for every project.

CLI

pgrun is a single static binary. On macOS and Linux, install or upgrade with curl -fsSL https://pgrun.dev/install | sh. The installer verifies the download's checksum and uses /usr/local/bin when it's writable, otherwise ~/.local/bin; set PGRUN_INSTALL_DIR to choose. Windows builds and the source are at github.com/pgrundev/pgrun-cli.

Branch commands

CommandWhat it does
branch create [project] --name NCreate a branch. --ttl 1h|6h|24h|7d, --parent NAME, --wait (prints DATABASE_URL=… when ready), --timeout 300s, --json.
branch list [project]Name, status, parent, version, created, expires. Never shows credentials.
branch get [project] NAMEOne branch's status (alias status).
branch url [project] NAMEPrints DATABASE_URL=… for a ready branch.
branch env [project] NAMEPrints export DATABASE_URL="…" for eval, or JSON with --format=json.
branch exec [project] NAME -- CMDRuns CMD with DATABASE_URL in its environment and exits with its exit code.
branch exec [project] --create N -- CMDCreates a branch, waits, runs CMD. Add --ttl, --from PARENT, and --delete-after to delete it afterwards, even when CMD fails.
branch delete [project] NAMEStarts deletion.

Projects, auth and setup

CommandWhat it does
projects listProjects this token can branch.
project use SLUGWrites .pgrun/project. Every command can then omit [project] in this directory and below.
auth login / logout / statusSave, clear or check credentials. The token is read with echo off and verified before it's saved.
source status|protect|copyThe Safe Copy steps from the terminal. source protect prints the review table; --set table.column=copy|fake|null|remove decides columns and --approve activates the policy.
skill installInstalls the Claude Code skill (--project for ./.claude/skills).
mcp serveRuns the MCP server. See MCP server.

Configuration, highest priority first: --url/--token flags, then PGRUN_API_URL/PGRUN_API_TOKEN, then ~/.config/pgrun/config.json. Exit codes: 0 success (for branch exec, the command's own code), 1 operation failed, 2 not authenticated, 64 bad usage. --json prints the API's response unchanged.

CI

Create a token on the dashboard's Tokens page, store it as a CI secret named PGRUN_API_TOKEN, and wrap the database step in branch exec. A GitHub Actions example:

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # … set up Ruby / Node / Python as usual
      - name: Install pgrun
        run: |
          curl -fsSL https://pgrun.dev/install | PGRUN_INSTALL_DIR="$HOME/.local/bin" sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - name: Migrate a disposable branch
        env:
          PGRUN_API_TOKEN: ${{ secrets.PGRUN_API_TOKEN }}
        run: |
          pgrun branch exec my-project \
            --create "ci-${{ github.run_id }}-${{ github.run_attempt }}" \
            --ttl 1h --delete-after -- bin/rails db:migrate

Include the run attempt in the name. Branch names are never reused, so re-running a job with the same name would fail.

MCP server

pgrun mcp serve exposes branches as tools to any MCP client over stdio. It reads its credentials from the environment only:

{
  "mcpServers": {
    "pgrun": {
      "command": "pgrun",
      "args": ["mcp", "serve"],
      "env": {
        "PGRUN_API_URL": "https://app.pgrun.dev",
        "PGRUN_API_TOKEN": "pgrun_…"
      }
    }
  }
}

Tools: pgrun_create_branch (waits for ready by default and returns database_url), pgrun_list_branches, pgrun_get_branch and pgrun_delete_branch. Put the block in Claude Desktop's config or a project's .mcp.json.

REST API

Everything the CLI does is plain HTTPS. Authenticate with a bearer token from the Tokens page. Base URL: https://app.pgrun.dev/api/v1.

EndpointReturns
GET /projectsThe account's projects, with status ready, preparing or failed.
POST /projects/:project/branches201 with the branch. Body: name (required), ttl, parent_branch_id (an id or a branch name; defaults to the base branch). 422 on an invalid or taken name, a bad TTL or a full quota.
GET /projects/:project/branchesLive branches, without connection strings.
GET /projects/:project/branches/:nameOne branch. Includes connection_url once it's ready.
DELETE /projects/:project/branches/:name202 deleting. 409 for the base branch or a branch with children.
GET /usage, GET /projects/:project/usageThe numbers on the Usage page.
/sources/…The Safe Copy flow: connect, protect, copy.
API=https://app.pgrun.dev/api/v1
AUTH="Authorization: Bearer $PGRUN_API_TOKEN"

curl -sS -X POST "$API/projects/my-project/branches" -H "$AUTH" \
  -H "Content-Type: application/json" -H "X-PGRun-Client: ci" \
  -d '{"name":"pr-482","ttl":"24h"}'
# 201 {"id":"branch_91","name":"pr-482","status":"creating","is_base":false, …}

curl -sS "$API/projects/my-project/branches/pr-482" -H "$AUTH"
# poll every ~2 s until "status" is "ready" (with "connection_url") or "failed"

curl -sS -X DELETE "$API/projects/my-project/branches/pr-482" -H "$AUTH"
# 202 {"id":"branch_91","name":"pr-482","status":"deleting"}

The optional X-PGRun-Client header (cli, mcp, agent, ci or github_pr) only labels your Usage page. A 404 can mean an unknown project, a token from another account, or branches not being enabled for the account. The complete reference, with every field and error, is agents.md, written to be pasted straight into an agent's context.

Test suites on a branch

A branch holds real data, and most test suites expect an empty database they're free to wipe. Branches are great for migrations, console work and querying real data. Running a fixture-based suite is a different job. If you do run one, know that:

  • Fixtures truncate their tables first. Rails' fixtures :all empties every fixture table, and foreign keys don't stop it because the branch role is a superuser.
  • Schema maintenance purges the database. Set config.active_record.maintain_test_schema = false for the run.
  • Parallel tests create extra databases on the branch and load the schema into each. Run with PARALLEL_WORKERS=1.
⚠️
If a migration fails on a branch, don't repair it by loading the schema. db:schema:load, db:reset, db:setup and db:prepare wipe the branch's data. Create a fresh branch instead, and tell us if schema_migrations is ever empty on a new branch.

Roles & permissions

Decisions about production data belong to the account's owners and admins. Everyone on the account can see them and work with branches.

ActionOwner / adminMember
View projects, Safe Copy status and protection decisions
Create, connect to, reset and delete branches
Connect a production database or change its connection
Turn production data access (sampling, deep verification) on or off
Exclude tables, approve the policy, request a review
Create the Safe Copy
Create or revoke API tokens

An API token acts with the authority of the person who created it, checked on every request. If that person stops being an owner or admin, the token immediately loses the privileged actions above, while branch endpoints keep working.

Usage

The Usage page shows how your account uses branches over the last 24 hours to 90 days: active and peak concurrent branches, branches created by day, week or month, compute time, storage, Safe Copies, startup times, and a per-project breakdown. It also shows which surface created each branch (dashboard, CLI, MCP, agent, CI or API). The same numbers are available from GET /api/v1/usage. A value pgrun didn't measure shows as unknown, never as zero.

Security model

  • Production is only read. Every session pgrun opens against production is read-only, and nothing is written to it.
  • Protection happens at the source. Masking runs inside the query that reads production, so a protected value never leaves your database. The finished copy is scanned for known sensitive values, and the copy fails if any are found.
  • Fail closed. No copy without an approved policy covering every column. No copy if the schema changed since approval. No automatic Copy for free text.
  • Credentials stay secret. The production connection string is encrypted at rest and never displayed or returned. Each branch gets its own credentials, which are destroyed with the branch.
  • Encrypted in transit. Branch connections require TLS through pgrun's gateway. Branch Postgres ports aren't reachable from the internet.
  • Audited. Connecting a database, consent changes, policy approvals and branch creation and deletion are recorded with who did them, whether a person or a token.

Troubleshooting

SymptomWhat to do
No Branches in the sidebarBranches aren't enabled for this account yet. Contact us.
API or CLI returns 404Check the project slug with pgrun projects list and that the token belongs to the same account.
pgrun exits with code 2Not authenticated. Run pgrun auth login.
"branch quota reached"The project has 10 live branches. Delete some, including failed ones.
"Name has already been taken"Names are never reused, including names of deleted branches. Pick a new one.
"parent branch is not ready"Wait for the parent to reach ready, or branch from main.
Branch status failedRead the error on the branch page, delete the branch, and create a new one. Don't retry in a loop. If it keeps failing, email support.
Can't delete a branchIt has child branches (delete those first), or it's the base branch, which can't be deleted.
SSL or connection errorsUse the connection string exactly as shown, with sslmode=require or stricter. Remove channel_binding=require if your client adds it.
password authentication failedThe password was reset, or the branch was deleted. Fetch the current URL with pgrun branch url.
pg_stat_ssl shows falseExpected. See Connect to a branch.
materialized view … has not been populatedRun REFRESH MATERIALIZED VIEW on the branch.
db:migrate fails with "relation already exists"The branch's migration history is missing. Don't load the schema; create a new branch and let us know.
Project shows Action requiredProduction's schema or the protection rules changed. Open Protect Data, decide the new columns, and re-approve.
Safe Copy won't startEvery column must be decided and the policy approved. If production changed since approval, re-approve first.
Safe Copy failedEmail support@postgresrun.com with the project name.

Still stuck? Email support@postgresrun.com with your project and branch name.