Branches

A branch is a separate copy of a project's search data, like a database branch for a preview environment. Every project starts with one branch, main. Indexes, documents and keys on one branch are invisible to every other branch, even when two branches have an index with the same name.

A branch starts either empty or as a fork. An empty branch has no indexes: create them and load its documents yourself. A fork starts with a copy of its parent's indexes, documents and settings, made in seconds whatever their size. See Fork a Branch.

Create a Branch

  1. Open the project and its Branches page.
  2. Click New branch and give it a name, such as preview/pr-1.

A branch is ready within a few seconds. Names are 1 to 63 characters: lower-case letters, digits, ., _, / and -, starting and ending with a letter or digit. Your plan sets how many branches a project can have, counting main; the dialog says so when you reach it.

Fork a Branch

A fork copies every engine of its parent, main unless you name another, with all its indexes, documents and settings. That includes indexes you created directly on an engine's native endpoint. Nothing is copied byte for byte: the fork shares its parent's stored data and keeps only what it changes, so it is ready in seconds and costs little until you write to it. Writes to the fork never reach the parent, and writes to the parent never reach the fork.

terminal
curl -X POST https://headband.dev/v1/branches \
  -H "Authorization: Bearer hb_adm_YOUR_LOGIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"projectId": "YOUR_PROJECT_ID", "name": "preview/pr-1", "expiresAt": "2026-11-03T00:00:00Z"}'

Send "empty": true for an empty branch instead. The answer lists the fork's engines and their endpoints in instances, and so does GET /v1/branches/:name.

  • The fork holds the parent's data as of its last save to storage, which happens within about a minute of a write and whenever an idle engine parks.
  • Each forked engine is serverless: it parks when idle and wakes on the next request. A Meilisearch fork wakes in well under a second. An Elasticsearch fork boots its engine to wake, which takes seconds, so it stays up for 30 minutes after its last request.
  • Meilisearch and Elasticsearch engines fork. Typesense, Visor and your own engine (BYOE) do not yet. A parent with a Headband index on any of them answers 409 fork_not_supported, listing each index and why, and no branch is made. Create an empty branch for those. An engine of the parent with no Headband index that cannot fork is left out of the fork.
  • A synced index forks as a plain index with the data it had. Sync keeps writing to the parent only.
  • Forking is rolling out. Until it is on for your organization, a create without empty answers 409 fork_not_available.

Preview Deploys on Vercel

Give every Vercel preview its own fork of production search. A command in the build step forks a branch named after the git branch, mints keys for it and writes them where your app reads its environment. The branch goes away 7 days after the last push.

  1. Run headband login once. Its key is in ~/.config/headband/credentials.json.
  2. In Vercel, open the project's Settings, then Environment Variables. For the Preview environment only, add HEADBAND_API_KEY (that login key, as a Secret) and HEADBAND_PROJECT (your project id).
  3. Set the build command, in vercel.json or the project's Build settings:
vercel.json
{
  "buildCommand": "if [ \"$VERCEL_ENV\" = preview ]; then npx -y @headband/cli@0.7 branch ensure \"preview/$VERCEL_GIT_COMMIT_REF\" --project \"$HEADBAND_PROJECT\" --expires-in 7d --env-out .env.production; fi && next build"
}

On a preview build, the command forks main into preview/<git branch> the first time, and reuses that branch on every later push. It writes these variables to .env.production, replacing any HEADBAND_* lines already there. Next.js reads that file during the build and on the server.

VariableValue
HEADBAND_URLThe Headband API.
HEADBAND_SEARCH_KEYA search key for the preview branch only.
HEADBAND_ADMIN_KEYAn admin key for the preview branch only. Keep it on the server.
HEADBAND_BRANCHThe branch name. HEADBAND_BRANCH_ID and HEADBAND_PROJECT_ID come with it.
HEADBAND_ELASTICSEARCH_URLOne per engine, such as HEADBAND_MEILISEARCH_URL: the fork's native endpoint.

In production, set the same names in Vercel's Production environment to your main keys, so the app reads one set of names everywhere. The production build skips the command.

Cleanup needs nothing more: each push moves the expiry 7 days out, and an expired branch is deleted with its engines and keys. To delete it as soon as the pull request closes, add a GitHub Actions job:

.github/workflows/headband-preview-cleanup.yml
on:
  pull_request:
    types: [closed]
jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - run: npx -y @headband/cli@0.7 branch delete "preview/${{ github.head_ref }}" --project "${{ vars.HEADBAND_PROJECT }}" --yes
        env:
          HEADBAND_API_KEY: ${{ secrets.HEADBAND_API_KEY }}

The job deletes by the git branch name as is. ensure lowers capitals and replaces characters a branch name cannot hold, so for such a git branch the job finds nothing to delete, and the expiry removes the branch instead.

Each Branch Gets Its Own Engine

The first index you create on a branch places it on a new dedicated engine for that branch, separate from the engine behind main. So each branch with indexes has one dedicated engine of its own.

Like any serverless engine, a branch's engine parks when it is idle and wakes on the next request, so a preview branch nobody is using costs little. Deleting the branch deletes its engine too.

Keys and Branches

Every project-scoped key belongs to exactly one branch.

  • The project's default keys, and keys made in Settings, then Keys, are main keys.
  • To get a key for another branch, switch the branch picker in the project header to that branch, then open the project's Keys tab. Keys for a branch other than main are test keys: hb_adm_test_ and hb_src_test_.
  • A branch key needs no header. It already points at its branch, so the same requests you send to main reach the branch.
  • A branch key cannot reach any other branch. Naming another branch, even main, answers 403 branch_not_allowed ("This key reaches only its own branch.").
  • Creating an index on a branch needs that branch's admin key, or the dashboard with the branch picked. An org key cannot create indexes: it answers 403 project_scope_required.
terminal
curl -X POST "https://headband.dev/v1/documents?index=products" \
  -H "Authorization: Bearer hb_adm_test_YOUR_BRANCH_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '[{"id": 1, "title": "Only on this branch"}]'

The Headband-Branch Header

An org-scoped key reaches main by default. To send it to another branch, add the Headband-Branch header. It takes a branch name, a branch id, or <project id>/<branch name> to pick one project's branch.

terminal
curl -X POST https://headband.dev/v1/search \
  -H "Authorization: Bearer hb_adm_YOUR_ORG_KEY" \
  -H "Headband-Branch: preview/pr-1" \
  -H "Content-Type: application/json" \
  -d '{"index": "products", "q": "widget"}'
AnswerWhen
400 invalid_branch_nameThe header is not a branch name, a branch id or <project id>/<branch name>.
403 branch_not_allowedA branch key named a branch other than its own.
404 branch_not_foundNo live branch has that name in your organization, or it was deleted.

Delete a Branch

On the Branches page, open the branch's menu and click Delete, then type the branch name to confirm. Within a few seconds the branch, its indexes, its keys and its engine are gone. Its keys then answer 401, and the header answers 404 branch_not_found. Nothing on main changes.

The same menu can Protect a branch, which blocks deleting it, or Set expiry so it is deleted on a date. A branch cannot be both protected and expiring.

main, the primary branch, serves production. It is always protected: it cannot be unprotected, expired or deleted, and the API answers each attempt with 409 branch_is_primary.

Branches through the API

/v1/branches lists, creates, updates and deletes branches. Listing needs an admin key. Creating and deleting need the key from headband login; any other key answers 403 branches_manage_required. The CLI wraps it as headband branch (requires CLI 0.4.0), which also has branch checkout to point a directory at a branch. See Branches in the CLI.