Docs / Secrets / env

Secrets / environment

Encrypted per-site secrets after claim. Set them in the project UI, aft env CLI, or API. MCP does not set secrets.

Model

Secrets are a vault on a claimed site. Values are encrypted at rest (AES-GCM); only names are ever listed back — there's no "reveal value" anywhere, including the API. MCP does not set secrets — claim first, then use the project UI, CLI, or API.

Project UI

  1. Claim the site (see Claim).
  2. Open the project → Secrets.
  3. Add name + value → Save.

CLI

aft login
cd your-project          # aft.json / .aft/state.json present
aft env list
aft env set ANTHROPIC_API_KEY=sk-…
aft env set OTHER_KEY "value with spaces"
aft env unset ANTHROPIC_API_KEY

Requires login and ownership of the claimed site. Same vault as the dashboard — aft secrets is an alias for aft env.

API

# names only
curl https://api.aft.page/v1/sites/{slug}/secrets \
  -H "Authorization: Bearer …"   # or Cookie session

curl -X PUT https://api.aft.page/v1/sites/{slug}/secrets/ANTHROPIC_API_KEY \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer …" \
  -d '{"value":"sk-…"}'

curl -X DELETE https://api.aft.page/v1/sites/{slug}/secrets/ANTHROPIC_API_KEY \
  -H "Authorization: Bearer …"

Owner or editor session, or a valid deploy edit token, can call these. GET only ever returns names.

aft.json capabilities

Declare intended names so approve-on-deploy can show them to the owner before anything is granted:

{
  "name": "my-app",
  "runtime": "next",
  "capabilities": {
    "secrets": ["ANTHROPIC_API_KEY"],
    "egress": ["api.anthropic.com"]
  }
}

This is a declaration, not a grant — see Capabilities for the full approve-on-deploy flow.

Where secrets actually go

Values live in the encrypted aft vault. For next / worker sites with a live upstream Worker, aft env set also syncs to that Worker via the Same names as process.env in your app). Static sites never see secrets at runtime.

Set secrets after the site Worker exists (post-deploy). If you set them earlier, they stay in the vault and sync when the upstream registers.

Try URLs: sqlite only

Paste-repo on Run does not provision Postgres or MySQL. The try box can use a local sqlite file when the app already has an ORM engine switch (Django DATABASES, Flask/FastAPI + SQLAlchemy via DATABASE_URL / SQLALCHEMY_DATABASE_URI).

Apps that talk the Postgres/MySQL wire protocol (pg, mysql2, Prisma postgresql, raw psycopg without Django, Phoenix/Ecto with postgrex / Ecto.Adapters.Postgres, Rails with gem pg and no sqlite3) fail honestly. We do not rewrite those adapters. Self-healing runtime installs missing language toolchains (Ruby, Elixir, pip) — not databases. D1 is a Worker binding for apps built on Code — not a drop-in for pg.

After you claim the URL, add a real DATABASE_URL under Secrets and re-run.

Next: Custom domains — put a claimed site on your own hostname.