---
id: platform/credential-rotation
title: Rotate an application environment credential
description: Rehearse issuing a replacement, applying a revision-checked environment update, verifying named consumers, and retiring the old credential without treating rolledOut as readiness.
keywords: [bex, secrets, environment, credential, rotation, expectedEnvRevision, CAS]
last_updated: 2026-09-23
---

Rotate one existing **service-owned runtime environment variable** when you can
issue a replacement that overlaps with the old credential long enough to verify
consumers. This guide pairs with the downloadable
[credential-rotation sample](/examples/credential-rotation-latest.zip).

Source pin: Bex `63c7582729bd604e0cac55d70634358b58f066f8` (secrets
`batch.go` / `service.go` / `rest.go` — `expectedEnvRevision` CAS). The pin
documents source behavior in that checkout; it is **not** evidence of the build
deployed at `api.bex.co`. An installation may differ — treat observed error
codes and response shapes as installation-specific when they diverge.

## What this covers (and what it does not)

| In scope | Out of scope here |
| --- | --- |
| One ordinary env key/value on a service you manage | Database role rotation, issuer-native APIs, build-time `ARG`/`ENV` baked into images |
| Revision-checked `PATCH …/environment` | Whole-list `PUT …/env-vars` replacement |
| Application-owned consumer checks you enumerate | Proof that every load-balanced replica flipped |
| Explicit retirement after `all_new` | Restoring a revoked external credential |

Environment **group** values and service overrides can still supply the key your
process reads — confirm which layer wins before rotating. See
[Environment variables & secrets](./secrets.md).

## Prerequisites

- Node.js ≥ 22.12 for the local sample.
- Manage authorization on the target service (writes) and view-sensitive for
  reading the selected key/revision.
- A versioned secrets store on the platform (CAS requires it).
- An external issuer (or the sample's synthetic issuer) that can leave old and
  new credentials valid during overlap.

## Download and local rehearsal

```bash
curl -fsSL -o credential-rotation.zip \
  https://bex.co/examples/credential-rotation-latest.zip
unzip credential-rotation.zip -d credential-rotation
cd credential-rotation
npm test
npm run rehearse
npm run stage -- happy
npm run scenarios -- stale-revision
npm run scenarios -- partial-activation
npm run scenarios -- premature-revoke
npm run scenarios -- interrupt-uncertain
```

Every local report includes `"evidence":"local_fixture"`. The synthetic issuer
is **not** Bex and **not** your real IdP.

## Supported compare-and-set shape

`PATCH /v1/services/{id}/environment` with `expectedEnvRevision`:

- Exactly **one** ordinary `envVars[]` assignment (key + value).
- No secret files, generate, delete, rename (`fromKey`), or multi-key CAS.
- Key must already exist (`ENVIRONMENT_VARIABLE_NOT_FOUND` otherwise).
- Stale revision → `ENVIRONMENT_REVISION_CONFLICT` — refresh and retry; do not
  overwrite another editor.
- Result names keys and sets `rolledOut` when `saveMode` is `deploy`. That flag
  means a rollout was **requested**, not that consumers adopted the value.

Read the selected key first (`GET …/env-vars/{key}`) and use its opaque
`revision`. If the server omits a revision, stop — do not fall back to an
unprotected write.

## Staged flow

1. **Issue** a replacement at the provider (or `npm run rehearse` locally).
2. **Update** Bex with the observed revision (`node update.mjs` or the sample's
   fixture API). Prefer `save_only` while validating, then `deploy` when you
   intend a rollout request.
3. **Activate** application processes so they load the new value (redeploy,
   restart, or your app's reload path). Platform `rolledOut` alone is not this
   step.
4. **Verify** every named consumer you can observe. Mixed, failed, timed-out, or
   incomplete results must **not** retire the old credential.
5. **Retire** the old credential explicitly at the issuer only after verification
   reports all enumerated consumers on the new id.

### save_only versus deploy

| Outcome | Meaning in the sample |
| --- | --- |
| `saved: true`, `rolledOut: false` | Config stored; consumers may still present the old credential |
| `rolledOut: true` | Deploy save mode completed its rollout request path |
| `consumerReady` | Always false from the update client — readiness is a separate check |

## Recovery scenarios

Run these from the extracted sample:

| Command | Expected lesson |
| --- | --- |
| `npm run scenarios -- stale-revision` | Competing writer wins; recover by re-observing, never by forcing the stale token |
| `npm run scenarios -- partial-activation` | Mixed consumers refuse retirement; keep the old credential active |
| `npm run scenarios -- premature-revoke` | Revoked credentials stay unusable — overlap exists so you can avoid this |
| `npm run scenarios -- interrupt-uncertain` | Ambiguous transport after submit is uncertain; re-read state; do not auto-revoke |

Failure and interruption never automatically revoke the old credential.

## Hosted (reader-authorized) update

```bash
export BEX_API_ORIGIN=https://api.bex.co
export BEX_ACCESS_TOKEN=…   # never commit
export BEX_SERVICE_ID=svc_…
export BEX_ENV_KEY=APP_TOKEN
export BEX_ENV_VALUE=…      # replacement; never commit or log
export BEX_SAVE_MODE=deploy # or save_only
node update.mjs
```

Then verify **your** consumers with application-owned checks before issuer
revocation. If you cannot enumerate consumers, or the provider cannot overlap
credentials, leave verification incomplete and follow that provider's procedure
rather than claiming a certified zero-downtime rotation.

## Related

- [Environment variables & secrets](./secrets.md) — sparse patch, whole-list PUT risk, groups
- [How deploys work](./how-deploys-work.md) — configuration rollout versus image deploys
- [API overview](./api.md) — authentication and service routes
