Values built from other values

DATABASE_URL is a host, a port, a user and a password with punctuation between them. All four usually exist already as their own keys. Writing the composed one out by hand means remembering to change it in two places, which nobody does — so it drifts, and is wrong in the way that takes an afternoon to find.

A value can name another key instead:

postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/app

Read that key and you get the assembled string. Change DB_PASSWORD and the assembled string changes, including the copy already sitting in a cluster.

The two forms

${KEY} names a key in the same environment.

${project.environment.KEY} names a key anywhere in your organisation — a shared SENTRY_DSN, a licence key every project needs, a staging credential production is deliberately built from.

Start typing ${ in the editor and it offers the keys you can reference. Keys in the environment you are editing come first, in the short form.

$${ when you mean the characters

Plenty of real values contain ${...} and mean it literally — shell fragments, Compose files, CI templates. Doubling the dollar says so:

exec $${HOME}/bin/serve

reads back as exec ${HOME}/bin/serve, with nothing substituted.

Who can read what

References resolve with your permission, not the key's. Reading a value built from ${payments.production.STRIPE_KEY} requires that you could have read STRIPE_KEY in payments/production yourself.

The consequence is deliberate, and worth knowing before you rely on it: the same key can resolve for one reader and be refused for another. A service token scoped to one environment cannot read a value in it that is built from another.

When a reader is refused, they are told which environment they would have needed. They are never given a partly-assembled value, and never given the template.

Every key a read touches gets its own audit record. Revealing one value built from four produces five.

What a consumer sees

A consumer — the Kubernetes connector, the API, anything holding a service token — receives the assembled value. Resolution happens on the server, so nothing downstream needs access to the keys a value is built from.

A composed key gains no new version when the thing it names changes. The template did not change; a history full of writes nobody made could not answer "who changed this". What moves instead is the key's last-changed time, which is what a consumer watches. A projected Kubernetes Secret updates within one change-feed cycle of the referenced key being written, with nothing written to the composed key at all.

Editing one

The editor shows you the template, not what it resolves to. If it showed the assembled string, saving the row would replace the composition with a frozen copy of its own output.

A key built from others carries a link mark in the list, so you can see which values have moving parts before you open one.

What is refused

A loop. A naming B naming A is refused when you save it, naming the loop. Refusing at read time would let you save a value that can never be read and find out from a consumer.

A reference to somewhere you cannot see. Refused when you save it. Stored and failed later, writing templates would be a way to discover which projects and environments exist.

Deleting a key others are built from. You are told which ones would break and asked whether to go ahead. Deleting it anyway is allowed — the refusal is a question, not a wall.

A chain more than eight deep. Cycles are refused going in, but a chain assembled one key at a time by several people can be acyclic and still absurd.

What it does not do

A reference names a key, not a project-wide value: there is no scope above the environment yet. And a reference is resolved when the value is read, every time — nothing is cached, so a revoked credential stops resolving on the next request rather than the next interval.