Governed config that drives every deploy

Environments & secrets

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.

i
Every project has a 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 key within one environment (e.g. STRIPE_KEY in production).
  • 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 set or 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 stale DATABASE_URL is never user-restorable. The public connection string (for connecting from your own machine) is shown in the dashboard and flicker branch sql; it isn't written into your app's environment.
i
Managed keys track infrastructure automatically. Branch a database and the fork's 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

A 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:

  1. Parent env — what the app runs with on the branch's parent (e.g. main), so a branch only diverges where it must.
  2. User overrides — values set on this app.
  3. 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-set DATABASE_URL can 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.