---
title: Environment variables
description: Set variables for a service. Changes apply on the next deploy.
---

Environment variables are the process environment a **service** is started with. Names must match `^[A-Z][A-Z0-9_]*$` — `DATABASE_URL`, `PORT`, `S3_BUCKET`. The app is read from `./tori.json`.

## Managed Postgres

Managed Postgres is not generally available. `tori db create` provisions a database in the app's zone. When `tori db status` reports **ready**, the platform writes `DATABASE_URL` onto this environment (masked in `tori env list`). It is not printed by `tori db`. The running revision still uses the snapshot it started with — deploy again for the process to see the URL. Query-parameter edits of that URL (for example `pgbouncer=true`) are kept while the database exists. A URL to another host is left alone. When the database is deleted and the zone confirms it is gone, a URL for that pooler role is removed.

Each plan caps concurrent sessions and database size (`limits.database` on `GET /api/account`). Statements longer than 60 seconds are cancelled. Crossing the size cap makes `tori db status` report that writes are read-only; existing reads keep working. `DATABASE_URL` is transaction-mode PgBouncer, so recovery is one transaction: `BEGIN; SET TRANSACTION READ WRITE; TRUNCATE the_table; COMMIT;`. A plain `DELETE` does not shrink the database. Writes resume once usage is under 90 % of the cap.

### Dumps and restore

Shared-tier databases take a nightly custom-format dump (`pg_dump -Fc`) into the cell object store. Seven successful daily dumps are kept. This is **not** PITR: the recovery point is the last successful dump (up to about 25 hours), and recovery time is provisioning a restore database plus `pg_restore`. Dedicated-cluster point-in-time recovery is out of scope.

`tori db dumps` lists dumps for the owner. `tori db restore --from <dump-id>` restores into a **separate** Postgres database. It never overwrites the live app database and never rewrites `DATABASE_URL`. Only one restore may be ready at a time. `tori db restore-status` prints the restore Postgres database and role names; the password is not printed (same as `tori db create`). Connecting from a laptop is not in this slice — the copy exists so an operator can inspect it in the zone, or so a throwaway service can be pointed at it later.

This dump and restore path is not generally available yet.

See [tori db](/cli/db/).

```sh
npx tori.host env set API_TOKEN=secret
npx tori.host env list
npx tori.host env unset API_TOKEN
```

See [tori env](/cli/env/) for flags. The same keys can be set in the console on the app → **env** tab.

## Release-phase migrations

A service deploy can run a command in a Job **after** the image is in the zone and **before** the new revision takes traffic. Set `"release"` in `tori.json` or pass `--release`. A Prisma / Alembic / Drizzle tree without that setting only prints a hint; it does not run a Job. Set `"release": false` to silence the hint.

```sh
npx tori.host deploy --image registry.example.com/acme/api@sha256:<64 hex> --release "npx prisma migrate deploy"
```

The Job is bounded at **10 minutes** (`activeDeadlineSeconds`, including image pull). That bound is not adjustable. A `--release` string is split on whitespace with no quoting (`npx prisma migrate deploy`); use a JSON array in `tori.json` for arguments that contain spaces. A building deploy is still closed by the control plane after 20 minutes from insert, so a wait-for-previous-Job plus a full 10-minute command can lose to that deadline — cancel and redeploy promptly rather than after a long migration. A successful Job then switches the serving revision. A failing or timed-out Job does **not**: the previous revision keeps serving. That is not an automatic rollback of database changes — a migration that already committed stays committed. Schema changes must stay readable by the revision that is still live until you deploy a matching app.

A console or API rollback of a deploy that had a release command runs **that same command** as a Job before traffic moves. If the Job fails, the rollback does not switch the revision. Forward-only migrations that the older image cannot apply will fail the rollback; keep schema changes backward-compatible, or deploy a matching image instead of rolling back.

Cancel of a building deploy does not SIGKILL a still-running release Job: the command is allowed to finish, then the Job is collected. The cancelled revision is not switched live. A following deploy waits for that Job to finish before starting its own — two release commands of one app do not run at once. The wait still counts against the 20-minute building deadline.

Retrying the same deploy reuses the Job named after that deploy id; a new deploy is a new Job. Prisma, Alembic and Drizzle apply heads idempotently. The cell-agent image in the zone must include this field; an older agent ignores `release` and would pin the revision without running the command.

See [tori deploy](/cli/deploy/).

## Next deploy, not now

A change applies on the **next deploy**, not immediately. Each deploy takes a snapshot of the environment, so a revision that is already running keeps the one it started with. After `env set` or `env unset`, deploy again from the directory that holds `tori.json`.

For a source-built service:

```sh
npx tori.host
```

For an image you brought, pass the same digest:

```sh
npx tori.host deploy --image registry.example.com/acme/api@sha256:<64 hex>
```

That is enough: the new snapshot differs, so a new revision is created. `--restart` is a different switch — it forces a revision when image **and** environment are already live — and it is only valid together with `--image`. Do not run `tori deploy --restart` on its own.

The CLI says so after every write. Treat a successful `env set` without a following deploy as “stored, not yet running”.

## Values never come back

The API returns values masked and there is no endpoint that does otherwise. `env list` can show that a variable is set, and not what it is set to:

```
API_TOKEN  sec••••••••
```

The listing goes to stdout and the commentary to stderr, so piping gets the rows without the paragraph. Rotate a leaked secret by setting a new value and deploying; you cannot read the old one back.

## Static sites

A static site is files served from object storage: nothing runs, so nothing reads a variable. The CLI refuses `env set` and `env unset` on a static site. The HTTP API and the console env tab have no such guard: they store the row, and every static deploy still snapshots no environment, so the values are inert. `env list` still works and says so.

Values for a static build belong in the build that produced `dist/`, not here.

## HTTP

Use these against a **service**. A PUT on a static app still stores the row (201 on create, 200 on update); that write is never read.

```sh
curl -sS -H "Authorization: Bearer $TORI_TOKEN" \
  https://api.tori.host/api/apps/<slug>/env
```

```sh
curl -sS -X PUT -H "Authorization: Bearer $TORI_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"key":"API_TOKEN","value":"secret"}' \
  https://api.tori.host/api/apps/<slug>/env
```

Shapes and status codes: [API reference](/api/reference/) (`listEnvVars`, `setEnvVar`, `removeEnvVar`). Do not copy tables from there into this page.

Download this page as [Markdown](/downloads/markdown/guides/environment.md).
