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_KEYondevand every open branch picks it up on the next read. A snapshot would have gone stale.
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.
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