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. 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 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, 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

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

  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
$ 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.