MidnightDocs

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.

Warning

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

Important

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 list shows 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-version is 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