Env templates
Env templates
Section titled “Env templates”Preview resolves ${namespace.field} refs server-side against the forked Snap’s endpoints — so your compose services get concrete, per-PR DB coordinates with no client-side string-building.
Why server-side?
Section titled “Why server-side?”The snap fork is created after you submit the preview, so the DSN isn’t known when you write the workflow. Template strings declared in your preview’s env list are resolved on the API just before the runner boots — unresolved refs surface as EnvError, never as empty strings.
Where the env list lives
Section titled “Where the env list lives”Two sources, in precedence order:
.destesi.ymlat the repo root — when present withpreview.enabled: true, its env list is authoritative. File ships with the PR; reviewers see env changes in the same diff. Full reference.- Dashboard — the existing per-preview env editor. Used when no file is committed (or when the file’s
preview:block is absent / disabled).
File-wins-strict — committing a file to a repo whose previews are dashboard-managed today will overwrite the dashboard env on the next build. Migrate by copying entries into the file in one PR.
Syntax
Section titled “Syntax”- Template is
${namespace.field}. Dotted names, not underscores. - Literals and multiple refs compose:
${postgres.user}:${postgres.password}@${postgres.host}. - Resolved values are not rescanned — no recursive templating.
- Unknown namespace or field →
EnvError, preview fails fast. - Mark a value
secret: true(or--env-secretvia CLI) to redact it from the web UI + API responses.
Available references
Section titled “Available references”| Reference | Resolves to |
|---|---|
${postgres.dsn} |
Full snap DSN (scheme+user+pass+host+port+db). |
${postgres.host} |
Snap host. In-cluster Service address for pods; localhost for host-reach. |
${postgres.port} |
Snap port. |
${postgres.user} |
Snap user. |
${postgres.password} |
Snap password. |
${postgres.database} |
Snap database name. |
${postgres.sslmode} |
sslmode carried from the remote’s DSN (empty when the DSN doesn’t set one). |
${s3.*} |
Reserved; currently unresolved (future release). |
${preview.*} |
Reserved; currently unresolved (future release). |
Examples
Section titled “Examples”Via CLI
Section titled “Via CLI”dst preview create \ --env 'DATABASE_URL=${postgres.dsn}' \ --env 'PGHOST=${postgres.host}' \ --env-secret NEXTAUTH_SECRET=$(openssl rand -hex 32) \ --from-dir ./app --from-remote conn_abcVia API (POST /v1/previews)
Section titled “Via API (POST /v1/previews)”{ "identity_workspace_id": "ws_...", "source_kind": "github", "third_party_app_id": "...", "repo_ref": "owner/repo", "branch": "feat/x", "env": [ { "name": "DATABASE_URL", "value": "${postgres.dsn}" }, { "name": "PGHOST", "value": "${postgres.host}" }, { "name": "NEXTAUTH_SECRET", "value": "...", "secret": true } ]}Gotchas
Section titled “Gotchas”- Don’t set
NEXTAUTH_URLfor Auth.js apps. The overlay forwardsX-Forwarded-Host/X-Forwarded-Proto; Auth.js withtrustHost: truereads those. HardcodingNEXTAUTH_URLpins one callback host and breaks preview URLs. - Host rewriting is automatic. The DSN the runner sees is rewritten from
localhost:<nodeport>to the in-cluster Service address (snap-s-<id>.destesi-snaps.svc…) so DinD-nested containers can reach it. You get the right value either way. - Secrets are stored server-side.
--env-secret NAME=valueposts the value encrypted at rest and redacts it in all API/UI responses. Rotate by redeploying with a new value. - Declared env wins over compose’s
environment:section. The runner generates adocker-compose.override.ymlthat injects declared env into every service, so you don’t have to${VAR}-thread values through your compose file.