Flicker secrets live in environments, not on individual
apps. An environment is a branchable slice of a project — like main, staging, preview/pr-42, or
dev/giovanni
— holding a bundle of config that the project's apps and database
branches resolve at deploy time. Every deploy resolves its environment
into an immutable snapshot and injects the result. This page is the deep
dive.
main environment.
It is the root (≈ production) — the parent that every other environment
branches from. staging
is a long-lived branch for config that isn't tied to a single feature;
preview/pr-42
is created with a PR and torn down with it; dev/<you>
is your personal slice for local work. Branching an environment forks the
project's database(s) copy-on-write and clones the secret set in sync, so
each environment is a self-consistent copy of the whole project.
The model
Three record types make up a secret:
-
Secret — a stable identity: one
keywithin one environment (e.g.STRIPE_KEYinproduction). - Secret version — an encrypted value. Every change adds a new version and moves the secret's pointer; old versions stay for history and rollback.
- Secret bundle — an immutable, fully-resolved snapshot of an environment for a single deploy or run. The bundle is what the running workload actually sees.
Values are encrypted at rest. List and history endpoints return key names and metadata only — a value never reaches a log, an event, or an API response unless you deliberately read it (and that read is governed and audited — see below).
User vs. managed secrets
Every secret has a provider:
-
User — values you set. API keys, tokens, feature flags,
anything you type with
flicker secrets setor in the UI. -
Managed
— values Flicker computes and keeps in sync. When
you wire a database to an environment, the platform injects and maintains
DATABASE_URL(the fast internal endpoint your app uses),FLICKER_ENV, and — when a connection pooler is provisioned —DATABASE_POOLER_URL. You never set these by hand, and you can't roll them back: a staleDATABASE_URLis never user-restorable. The public connection string (for connecting from your own machine) is shown in the dashboard andflicker branch sql; it isn't written into your app's environment.
DATABASE_URL is rewired to point at the fork — no
manual edit, no chance of a copied-over URL shadowing the branch's own
database. The same mechanism powers dependency auto-rewire.
Visibility: masked vs. restricted
User secrets carry a visibility tier:
- masked (default) — the value is readable through the governed read path (and shown masked in the UI until revealed).
- restricted — write/rotate only. No read API ever returns the value, to anyone, regardless of token scope. The running app still gets it at deploy time — restriction governs reads, not delivery.
restricted
is one-way for a given value: you can't flip it back
to masked
(that would expose a value written under a
never-readable promise). Rotating to a brand-new value may set masked
— the new value never carried the restricted promise.
Inheritance
Environments form a single-parent chain via inheritance. Resolution walks from the target environment up to the root, and more-specific environments override their ancestors. Set a key once on a shared parent and every child inherits it; override it on a child and only that child diverges.
The clearest example is personal configs. Each developer
gets a dev/<user> environment that inherits from the
org-wide dev environment. Override a key and you break
inheritance for that one key; delete the override and it re-inherits. Your
first flicker secrets pull creates your personal config
automatically.
Preview and dev can't inherit from production
preview or development environment may not
inherit (transitively) from production. This is a platform
policy: it stops production secrets from leaking into a throwaway preview
or a developer's laptop. Attempting it is refused at create/update time.
How a deploy consumes secrets
At deploy time, Flicker resolves the app's environment:
it walks the inheritance chain ancestors-first, flattens user and managed
secrets into a single key → value map (closer environments
winning), and persists that map as an immutable bundle.
The deploy injects the bundle; the bundle is the audit record of exactly
what that deploy ran with.
For an app running on a branch, one more layer applies. The effective env is composed lowest-precedence-first:
-
Parent env — what the app runs with on the branch's
parent (e.g.
main), so a branch only diverges where it must. - User overrides — values set on this app.
-
Auto-rewiring
(Flicker-owned, highest precedence) —
connection strings repointed at this branch's own
resources:
DATABASE_URL→ the forked database, dependency keys → the depended-on app's connection. Applied last on purpose, so a stale user-setDATABASE_URLcan never shadow the branch's own database.
Note the ordering: auto-rewire beats user overrides. That's the whole point — a Flicker-owned key always points at the branch's own resource and cannot be silently shadowed by a value copied from the parent. It only affects keys that are both user-set and the target of a dependency edge.
Restricted secrets are included in the resolved bundle (the running app needs the value); they're only excluded from the read APIs.
Managing secrets from the CLI
flicker secrets
operates on one environment — by default your
personal dev/<user>, or pass --environment. Each
set
or unset
is
one versioned change (one bundle, at most one redeploy of bound apps), and
values are never echoed back:
$ flicker secrets set STRIPE_KEY=sk_live_123 --environment production $ flicker secrets list --environment production # names + visibility + version + updated_at — never values $ flicker secrets unset OLD_FLAG --environment production
secrets get
is the one deliberate value read. It's governed —
only the org-wide key, or an environment-scoped key bound to this exact
environment, may read — and every read is access-logged server-side.
Restricted secrets return nothing to anyone:
$ flicker secrets get DATABASE_URL --plain # governed + access-logged; restricted secrets never return a value
Syncing to a local .env
flicker secrets pull downloads an environment's fully-resolved
snapshot — inheritance flattened, restricted keys excluded — into a local .env. By default it targets your personal dev/<user>, created on first pull:
$ flicker secrets pull # dev/<user> → ./.env, created on first pull $ flicker secrets pull --environment production --out .env.prod $ flicker secrets diff # what would change locally — key names only, never values
Pulled files start with a provenance header
(# managed by flicker · env: dev/giovanni · pulled: …). A .env
without that header is presumed hand-maintained, and pull
refuses to overwrite it unless you pass --force. Values are never printed to stdout —
pull
writes the file and reports a count; diff
prints key names and
status only.