Variables and secrets
How variables layer from organization to service, how secrets are stored, and how they reach a running service.
Variables are the environment a service runs with. You set them in layers, and each key takes its value from the most specific layer that sets it.
Layers
From least to most specific:
- Organization, every environment
- Organization, one environment
- Project, every environment
- Project, one environment
- Service, every environment
- Service, one environment
So a service-level LOG_LEVEL for production beats a project-wide one, which beats the organization's. Setting a key that a layer already has for that environment is an error, so edit the existing one instead.
Keys start with a letter or underscore and hold letters, digits and underscores. Organization-wide variables need the admin role. Project and service variables need developer or above.
Set them
loomctl vars my-project
loomctl vars set my-project LOG_LEVEL=debug --env preview
loomctl vars set my-project STRIPE_KEY=- --secret --env production
loomctl vars set my-project FEATURE_X=on --service svc:api:go:cmd/serverKEY=- reads the value from standard input, which keeps a secret out of your shell history. --env limits a variable to one environment, and --service limits it to one service by its graph ID. In the dashboard, the project's Variables tab does the same, and the organization's settings hold the organization-wide layer.
Secrets
Mark a variable secret and its value is write-only. Loomplane encrypts it in Vault (AES-256-GCM, versioned) and the API never returns it, to anyone, whatever their role. A database constraint keeps secret values out of the variables table entirely.
When a service deploys, its secrets go to the cloud's own secret store and reach the service by reference, readable by that service's runtime identity only:
| Target | Where secrets go |
|---|---|
| Cloud Run | Secret Manager |
| ECS on Fargate | Secrets Manager |
| Vercel | Sensitive environment variables |
| DigitalOcean App Platform | App Platform secrets |
| Azure Container Apps | App secrets |
They never pass through Ctrl Plane, which stores what it's given as plain JSON. A secret never appears in a task definition, a list response or the CLI's output, and the integration tests check that.
Addresses Loomplane adds
A service gets a variable for each thing it needs. Resource addresses hold credentials, so they're secrets and arrive through the secret store.
| Needs | Variable |
|---|---|
| Postgres, MySQL, MariaDB | DATABASE_URL |
| Redis, Valkey | REDIS_URL |
| Memcached | MEMCACHED_SERVERS |
| MongoDB | MONGODB_URI |
| AMQP brokers | AMQP_URL |
| Kafka | KAFKA_BROKERS |
| NATS | NATS_URL |
| Object storage | STORAGE_URL |
| Search | SEARCH_URL |
| ClickHouse | CLICKHOUSE_URL |
| Another service | Its name in capitals plus _URL, such as API_URL |
When two needs would share a name, each gets its own name's instead, such as ANALYTICS_URL. A variable you set yourself wins over a generated one.
MariaDB and Valkey resources used to get their name in capitals plus _URL (DB_URL). A service deployed that way keeps the old variable beside the new one, and the plan warns you to move off it.
The secret key
Every key Loomplane needs is derived from LOOMPLANE_SECRET_KEY with HKDF under a label of its own. That includes Vault's encryption, authsome's tokens and the GitHub install state's signature. Keep the secret key out of your repository and out of your backups. Losing it loses every credential and secret; leaking it along with a database copy exposes them. The security model has the whole picture.