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.
While a new environment is still forking, its page and the project hub
show each step — the database clone, and each app's image build and
rollout — as waiting, running, done, or failed, with elapsed time. A
failed step names the kind of failure, not the raw error text. An app
that uses one of the environment's databases waits for that database's
clone before starting its rollout (up to 10 minutes; a failed clone is
not waited on).
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, and inheritance is keys-only: at fork every parent key exists in the
child, but values never flow after fork. What a fork carries depends
on the child's
inherit policy — copy_values
snapshots the parent's values into the child's own rows at fork time
(later edits on either side do not cross), keys_only
takes the keys WITHOUT values. A key added to the parent later
appears in every child as present-but-unset, never with the parent's
value. A key is never hidden from a child.
The clearest example is personal configs. Each developer
gets a dev/<user> environment forked from the
org-wide dev environment, snapshotting its keys at
creation. Set a value and only that environment diverges.
Your first flicker secrets pull creates your personal config
automatically. Creating a project makes only main; these
org-level environments are not created with it, and a project's page lists
them only once a branch of its own database backs them.
Deploying an app bound to an environment with unset keys is refused, listing the keys — set them first, then deploy.
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:
the environment's OWN valued secrets (user plus managed) are flattened
into a single key → value map — ancestor values never
flow, so there is nothing to merge — and that map is persisted as an
immutable bundle. The deploy injects the bundle; the
bundle is the audit record of exactly what that deploy ran with.
First the deploy is refused when the environment has present-but-unset
keys, naming them.
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 $ flicker secrets copy --from production # seed personal dev/<user> from production (generate mints fresh; managed URLs skipped)
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 own resolved
values — restricted keys excluded — into a local .env. Present-but-unset keys are reported after the pull and left as
# KEY is unset
comment lines, never a parent env's values. 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.