Variables and secrets#
Set configuration and secrets for a service in an environment, safely, without putting values into shell history.
Before you start#
- An active human session.
- The exact service and environment identifiers. Human variable commands require them and use tenant-scoped nested routes.
The server derives workspace and project ownership. Do not pass
--workspace-id or --project-id with a human session.
Scopes#
| Scope | Behaviour |
|---|---|
plain |
Ordinary configuration |
secret |
Stored encrypted |
sealed |
Write-only — the value cannot be read back |
Choose sealed for anything you never need to read again, such as a database
password. Choose secret when an operator may legitimately need to retrieve it.
Naming rules#
New human-created names must match [A-Za-z_][A-Za-z0-9_]* and are limited to
255 bytes. List, update and delete remain compatible with legacy names created
through recovery credentials.
List#
midnight variable list --service-id svc_api --environment-id env_prod
Responses contain metadata only. The table includes the current version, which you need for compare-and-swap.
Create#
set is create-only for human sessions. Pipe the value on standard input so it
never enters shell history:
printf %s 'postgres://user:password@db/app' | midnight variable set --service-id svc_api --environment-id env_prod --scope sealed --stdin DATABASE_URL
printf avoids appending an unintended newline. --stdin preserves the bytes
it reads exactly, including a trailing newline if one is present.
--value still exists for compatibility but exposes the secret
through shell history and process listings. Use --stdin.
Update#
Update addresses the exact opaque identifier returned by list or set, and
requires the current version:
printf %s 'postgres://user:new-password@db/app' | midnight variable update --service-id svc_api --environment-id env_prod --expected-version 1 --stdin sealed_database_url
Compare-and-swap means a stale editor cannot overwrite a newer change: if the version does not match, the write is refused.
Delete#
midnight variable delete --service-id svc_api --environment-id env_prod --expected-version 2 sealed_database_url
Saving does not deploy#
Saving variable desired state does not deploy or restart the service. The new value takes effect on the next deployment or restart.
This is deliberate — it lets you stage a configuration change and apply it when you choose — but it surprises people who expect an immediate effect.
Empty values#
The API and CLI accept an explicitly empty value. An empty variable is not the same as an absent one.
Verify#
midnight variable listshows the name with the expected scope and a version.- After a deploy or restart, the workload sees the new value.
- A second update with a stale
--expected-versionis refused.
If it does not work#
| Symptom | Check | Recovery |
|---|---|---|
| The write is refused with a version error | The current version from list |
Re-read and retry with the current version |
set fails on an existing name |
set is create-only for humans |
Use update with the exact identifier |
| A name is rejected | Whether it matches [A-Za-z_][A-Za-z0-9_]* and 255 bytes |
Rename it |
| The application still sees the old value | Whether it has been deployed or restarted since | Saving does not deploy |
| A sealed value cannot be read | Sealed is write-only by design | Replace it rather than trying to read it |
| A token is rejected before any request | Non-human tokens are rejected for these routes | Use a human login, or the stored recovery credential for legacy compatibility |