Secret References
Secret references let one secret's value embed the value of another secret. Instead of duplicating a connection string or rebuilding a URL by hand, you compose it from its parts:
DB_USER=app
DB_PASS=s3cr3t
DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db.internal:5432/app
When this environment is read, DATABASE_URL resolves to postgres://app:s3cr3t@db.internal:5432/app.
Syntax
| Token | Meaning |
|---|---|
${KEY} | Reference another secret in the same environment |
${env.KEY} | Reference a secret in another environment of the same project, where env is the target environment slug |
$${KEY} | Escape — produces the literal text ${KEY} and is never treated as a reference |
The secret key must match ^[A-Za-z_][A-Za-z0-9_]*$ and the environment slug must be a valid slug (^[a-z0-9]+(?:-[a-z0-9]+)*$). Tokens that don't match — for example ${ not-a-key } — are left untouched.
Same-environment reference
HOST=api.example.com
PORT=8443
ENDPOINT=https://${HOST}:${PORT}/v1
Cross-environment reference
Pull a value from another environment in the same project by prefixing the key with that environment's slug:
# in the "staging" environment
API_BASE=${prod.API_URL}
Here prod is the slug of the target environment. Cross-environment references require that you have read access to the referenced environment — see Security below.
Cross-project references are not supported. A reference can only point at the same project's environments.
Escaping
If you need a literal ${...} in a value (for example, a shell template that should be expanded later), double the dollar sign:
PROMPT=$${USER}@$${HOST} # resolves to the literal: ${USER}@${HOST}
The escape is processed first, so $${KEY} never matches a reference token.
When references are resolved
References are stored verbatim — the raw value, tokens and all, is what lives encrypted at rest. Resolution happens only when a value is consumed:
| Read path | Resolves references? |
|---|---|
GET /api/v1/secrets/.../{env} (CLI pull, run) | Yes |
GET /api/v1/secrets/.../{env}/{key} (single secret) | Yes |
Export to a .env file from the dashboard | Yes |
| Promote secrets to another environment | No — token stored as-is |
| Copy secrets to another environment or project | No — token stored as-is |
Because promote and copy preserve the literal token, a reference like ${DB_HOST} keeps pointing at whatever DB_HOST is in the destination environment after it lands there. This is intentional: copying a composed DATABASE_URL to production should rebind to production's DB_HOST, not freeze staging's value.
References can nest: a resolved value may itself contain references, and those are expanded too (up to a depth limit). Cycles (A → B → A) and overly deep chains are detected; the offending token is left literal rather than looping forever.
The CLI (pull, run) needs no special handling — it receives already-resolved values from the API.
Security
Cross-environment references respect the caller's read access. They never let you read a value you couldn't already read directly:
- A same-environment reference is always allowed — you already have access to that environment.
- A cross-environment reference (
${env.KEY}) is resolved only if you have read access to the referenced environment. For a user, that means an org owner/admin role or explicit project/environment access. A service token scoped to one environment cannot read another environment through a reference.
When a reference cannot be resolved — because access is denied, the environment doesn't exist, or the key isn't found — the token is left literal in the returned value and reported. It is never silently replaced with an empty string, and a value you can't access is never leaked.
Reference errors
Unresolved references are surfaced in the API response alongside the secrets, in a referenceErrors array. Each entry is the unresolved reference, formatted as env.key for cross-environment references or just key for same-environment ones:
{
"secrets": {
"API_BASE": "${prod.API_URL}"
},
"referenceErrors": ["prod.API_URL"],
"version": "..."
}
A token reported in referenceErrors keeps its literal ${...} form in the value, so you can see exactly what failed to resolve.
Examples
Compose a database URL from parts:
DB_USER=app
DB_PASS=s3cr3t
DB_HOST=db.internal
DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@${DB_HOST}:5432/app
Reuse a shared base URL across environments:
# production environment
API_URL=https://api.example.com
# staging environment
API_BASE=${prod.API_URL}
Keep a literal template untouched for a downstream tool:
LOG_FORMAT=[$${LEVEL}] $${MESSAGE} # resolves to: [${LEVEL}] ${MESSAGE}