seekrit
Docs/Branch configs

Branch configs

A branch is a disposable environment forked from a real one: pr-142 inherits everything from dev, overrides only the values that differ for that pull request, and deletes itself when its lifetime runs out.

It exists for the config problem every preview deploy has. Without it you either point every preview at the shared dev environment — so no PR can change a value without changing it for everyone, and one leaked preview token exposes all of dev — or you hand-create a real environment per PR and accumulate stale ones nobody deletes.

Inherit, don't copy

A branch is not a snapshot. It stores only what you override, and everything else resolves from its parent at read time:

group secrets  <  app-env secrets  <  branch overrides  <  .env  <  process env

Two things follow, and both matter:

  • Creating a branch is instant, whatever the environment holds. Nothing is copied or re-encrypted — a branch of an environment with 200 secrets costs the same as a branch of one with 2.
  • Branches track their parent. Rotate STRIPE_KEY on dev and every open branch picks it up on the next read. A snapshot would have gone stale.
note

A branch can't out-reach what it forked. Reading one requires a key grant on every layer it resolves, including the parent — so branch access is always a subset of parent access, and revoking someone from dev ends their access to its branches too.

Opening and closing one

# Fork dev. Lives 7 days unless you say otherwise.
seekrit branch create pr-142 --app api --from dev

# Override just what differs for this PR.
seekrit secrets set DATABASE_URL 'postgres://…pr-142…' --app api --env dev --branch pr-142

# Run against it — everything else comes from dev.
seekrit run --app api --env dev --branch pr-142 -- pnpm dev

seekrit branch list --app api
seekrit branch delete pr-142 --app api

--ttl takes 30m, 12h, 7d, 2w, or never (max 30 days). Expired branches are deleted automatically, along with their overrides, key grants, and any service token bound to them — so forgetting to clean up is safe, not a leak.

In CI

A service token stays bound to its environment; --branch (or SEEKRIT_BRANCH) picks a branch of that environment, so the token you already use for dev needs no changes and no new grants.

env:
  SEEKRIT_TOKEN: ${{ secrets.SEEKRIT_TOKEN }}   # bound to api/dev
  SEEKRIT_BRANCH: pr-${{ github.event.number }}

steps:
  - run: seekrit branch create "$SEEKRIT_BRANCH" --app api --from dev --ttl 3d
  - run: seekrit secrets set DATABASE_URL "$NEON_BRANCH_URL" --app api --env dev --branch "$SEEKRIT_BRANCH"
  - run: seekrit run -- pnpm test        # picks up SEEKRIT_BRANCH automatically

Then on PR close:

  - run: seekrit branch delete "pr-${{ github.event.number }}" --app api

The TTL is the backstop for when that step never runs — a cancelled workflow, a force-pushed branch, a runner that died.

seekrit-run takes the same --branch flag and SEEKRIT_BRANCH variable, so containers get branch configs without the Node CLI:

seekrit-run --branch pr-142 -- ./server

Who can branch

Anyone who can read the environment — creating a branch is not an admin action, because it grants no access the creator doesn't already have. The new branch's data key is wrapped to everyone who already holds a grant on the parent, so a branch is usable by the same people the moment it exists, with no grant paperwork. Pass --no-share to keep a branch to yourself.

note

That fan-out happens once, at creation. A principal added to the parent after a branch exists — a service token minted later, a teammate granted later — won't hold that branch's key and gets no key grant for branch …. Grant it explicitly, or just re-create the branch; they're cheap and short-lived. Branches created after the principal are unaffected.

Branches are one level deep (you can't branch a branch) and live under application environments, not groups. Their names share the application's environment namespace, so a branch can never collide with a real environment.

Seeing where a value came from

--explain marks inherited values against their source layer and overridden ones against the branch:

seekrit run --app api --env dev --branch pr-142 --explain -- pnpm dev
DATABASE_URL      branch:api#pr-142
STRIPE_KEY        app:api/dev
SHARED_CA         group:common@dev