Undo, for production configuration

Publish an environment and every variable is snapshotted as ciphertext in one locked transaction. Roll it back and every variable touched gets an attributed history entry, written in the same transaction as the change.

What a version is

Publishing an environment copies seven fields for every live variable into a numbered snapshot: name, value, type, secret flag, visibility, required flag and default value. The value is copied verbatim as ciphertext, which is why publishing never needs the workspace key and the server never learns anything by doing it. Labels are derived server-side from a count, so the first publish is v1, and a Postgres advisory lock keyed to the environment means two concurrent publishes cannot race to the same number. Rolling back replays a snapshot in one more transaction and tells you exactly how many variables it added, updated and removed.

The guarantees, precisely

Including the limits, because a history feature you have wrong assumptions about is worse than none.

1 transaction

Per publish and per rollback

Both run inside a single Postgres transaction under an advisory lock keyed to the environment, so a publish cannot interleave with a rollback and neither can half-apply.
4 event types

Per-variable history

created, updated, deleted and restored. Each event denormalizes the actor name and email next to the user id, and the foreign key nulls on delete, so history keeps naming the person after their account is gone.
1000

Versions per environment

A hard cap. The publish that would be the 1001st is refused rather than silently dropping the oldest, so you find out at publish time instead of when you need the snapshot.
8192 chars

Previous-value retention limit

A single-variable update stores the previous ciphertext in its event when it fits under that length, which is what powers "copy old value" in the history drawer. Bulk applies and rollbacks record no previous value.
3 permissions

Gating versions

versions:read, versions:create and versions:rollback are checked server-side on every version controller. The built-in Viewer role carries only the read one.
no delete

Route, command or SDK method

There is no path anywhere that deletes a version row, so labels are never recycled while the environment exists and a published label stays a stable address.

Publishing, inspecting, rolling back

The same lifecycle from the CLI, the REST API and the SDK. Take a snapshot before anything risky, because rollback does not take one for you.

  1. 1

    Publish before a risky change

    The note is optional, trimmed and capped at 200 characters. Publish accepts several environments in one run, producing one snapshot and one event per environment, and it emails every other active member that a new version landed.

    envless publish "before the Stripe migration" --env production
    envless version publish --note "before the migration" --env staging,production
  2. 2

    Look at what you captured

    Listing supports free-text search across label and note plus latest, oldest and label sort orders, with server-side pagination. Revealing a version decrypts it locally, exactly like reading a live variable.

    envless version list --env production
    envless version get v12 --env production --reveal
  3. 3

    Roll back in one transaction

    A live variable counts as changed when any of six fields differs from the snapshot. Every variable the rollback touches gets a history event stamped with the version it came from, written inside the same transaction as the change.

    envless version restore v11 --env production --yes
  4. 4

    Read one variable's story

    Per-variable history is the surface you actually reach for during an incident: who changed this, when, and what was it before. It is queryable from the CLI, the API and the SDK.

    import { envless } from '@goenvless/sdk'
    
    const events = await envless.variables.listAllHistory('api', 'production', 'DATABASE_URL')
    
    for (const event of events) {
        console.log(event.type, event.actor?.email, event.createdAt)
    }
  5. 5

    Ship the events somewhere permanent

    Workspace events are delivered as signed webhooks, which today is how you build a durable, queryable record outside Envless. Delivery is at-least-once with a 30s, 2m, 10m, 30m backoff over five attempts, and only a 2xx inside ten seconds counts.

    curl -X POST https://api.envless.cloud/webhooks \
      -H "Authorization: Bearer $ENVLESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name":"audit-sink","url":"https://hooks.acme.inc/envless","eventTypes":["variable_version.created","variable_version.rolled_back"]}'

Versioning and history FAQ

Straight answers about how this works in practice.

No, only to a version someone explicitly published. Changes made between publishes are not recoverable as a set. Individually they often are, because a single-variable update stores its previous ciphertext in the event, but a bulk apply or a rollback records no previous value. Publish before anything you might want to undo, and treat publishing as cheap.

No, and this catches people out. Rollback replays the target version and writes no new version, so if you roll back and then want to get to where you just were, there has to have been a publish. Publish first, then roll back. The SDK documentation says the same thing at the call site for exactly this reason.

Not yet, and we would rather be precise than imply one. Every workspace action is recorded with the actor, the event type and a full payload, and each one is delivered to your webhook endpoint within seconds. What does not exist today is an endpoint or dashboard page that queries those rows back. The two surfaces you can query now are per-variable history and the version list; for a searchable workspace-wide record, point a webhook at a sink you control.

No. Nothing logs reads. The recorded events are created, updated, deleted and restored, which is to say writes. The nearest thing to read telemetry is a lastUsedAt timestamp on each API key, throttled to at most one write a minute, which tells you a key is in use but not what it fetched.

It is append-only in the sense that no route, command or SDK method exposes an update or delete for a version, and that is the honest way to put it. There is no hash chain, no signature over the rows and no write-once storage, and version ciphertext is deliberately rewritten in place when you rotate the workspace passphrase. Treat a version as a frozen set of variables rather than frozen bytes.

No, they are per environment, derived from that environment's version count. v13 in staging and v13 in production are unrelated snapshots. For anything that has to be globally unambiguous, such as pinning a build, use the version uniqueId rather than the label.

No, and this is deliberate. Duplicate copies products, projects, environments and variables including their ciphertext, but nothing in the duplication path touches versions. A duplicated environment starts with a clean history, which is usually what you want when the copy is a new staging branch rather than a clone of the past.

It starts fresh. A rollback soft-deletes variables missing from the snapshot and inserts new rows for names that are absent, and history is keyed to the variable row, so a restored variable begins a new history while the old events stay attached to the retired row. The version snapshots remain the continuous record across that boundary.

Get Started

Ship secrets, not chaos.

Start free today and discover why developers trust Envless for end-to-end encrypted, versioned secrets across every environment.