> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Saved state

> Keep emulator data, tokens, and webhooks across restarts, and save named snapshots to go back to.

By default every emulator keeps its state in memory, and stopping the process loses it. With a state target, Baseline saves each service's data and restores it on the next start: repositories and issues you created, OAuth sessions and the tokens they issued, signing keys (so ID tokens stay valid), webhook subscriptions, and custom emulator state. [Named snapshots](#named-snapshots) save the state of a running server under a name so you can return to it, with or without a state target.

## Turn it on

```bash theme={null}
npx @deploybaseline/cli --state .baseline/state.json
```

Or in `baseline.config.json`:

```json theme={null}
{
  "state": ".baseline/state.json"
}
```

```json theme={null}
{
  "state": {
    "url": ".baseline/state.json",
    "saveDebounceMs": 250
  }
}
```

* `saveDebounceMs`: wait after a change before saving

`--state` overrides the config key. A relative path in the config file resolves against the config file's folder; on the command line, against the current folder. Add `.baseline/` to `.gitignore`.

In tests, `createEmulator` takes the same target:

```typescript theme={null}
import { createEmulator } from '@deploybaseline/cli'

const github = await createEmulator({ service: 'github', port: 4001, state: '.baseline/github.json' })
github.restored   // true when it started from saved state
await github.close()   // saves
```

## Saved state or seed?

Saved state wins. On startup each service either restores or seeds, and the banner says which:

```
  State: .baseline/state.json (restored github, slack; seeded stripe)
```

* A service with saved state restores it and skips its seed config.
* A service with no saved state seeds from config, for example one you just added.
* `--reset-state` (`resetState: true` in `createEmulator`) ignores saved state, seeds every service, and overwrites the file.
* If a service's seed config changed since its state was saved, startup keeps the saved state and prints a note. Use `--reset-state` to apply the new seed.
* State for a service you removed from the config stays in the file, so adding it back restores it.

## When it saves

* 250 ms after a change. Writes (`POST`, `PUT`, `PATCH`, `DELETE`) count, and so does any request that changed state, such as an OAuth page that stores a pending code.
* On exit: `Ctrl+C`, `SIGTERM`, and `Ctrl+Break` on Windows. `close()` in `createEmulator`.
* Writes are atomic: a temporary file is synced and renamed over the old one, so a crash leaves the previous complete file.

Windows does not deliver `SIGTERM` when a process is killed, so the debounced save is what keeps data there. Keep the debounce short if you stop emulators abruptly.

Not saved: webhook delivery history (so Slack's `event-retry` simulation cannot retry a delivery from before a restart), scheduled webhook retries, and rate limit counters.

## Watch mode

With `--watch`, a reload normally resets every service to its seed. With a state target, a reload restores the saved state instead. A service whose seed config changed reseeds, and the reload prints `<service>: seed changed, reseeded`. `--reset-state` applies only to the first start, not to later reloads.

## One process per state file

Baseline locks the file (`state.json.lock`, holding the PID and host). A second `start` on the same file stops with:

```
.baseline/state.json is in use by process 41822 (since 2026-10-18T09:12:03.000Z). Stop it first, or use another state target.
```

If the process that held the lock is gone (a crash or `kill -9`), the next start takes the lock over and prints a warning, so you never clean up by hand. A lock from another machine (a shared folder) is refused instead, since its process cannot be checked.

## SQLite and Turso

A state target can also be a SQLite file or a libSQL database such as [Turso](https://turso.tech). Each needs a small package that the CLI loads only when you use it:

| Target | Package | Notes |
| - | - | - |
| `sqlite:.baseline/state.db` | `@deploybaseline/state-sqlite` | Uses Node's built-in `node:sqlite` (Node 24 or later), so there is nothing to compile |
| `libsql://my-db.turso.io` | `@deploybaseline/state-libsql` | Also `libsql+https://` and `libsql+http://` for a self-hosted server. Uses `@libsql/client` over HTTP |

Run the package next to the CLI with `npx -p`:

```bash theme={null}
npx -p @deploybaseline/cli -p @deploybaseline/state-sqlite baseline start --state sqlite:.baseline/state.db

export BASELINE_STATE_AUTH_TOKEN=...   # or append ?authToken=... to the URL
npx -p @deploybaseline/cli -p @deploybaseline/state-libsql baseline start --state libsql://my-db.turso.io
```

Or install it in your project (`npm install -D @deploybaseline/cli @deploybaseline/state-sqlite`) and run `baseline start` from a `package.json` script. Without the package, start stops and prints both commands.

Everything else works as with a file: restore or seed on start, debounced saves, `--reset-state`, `--watch`, upgrade checks, and named snapshots.

* **One table.** Both targets keep state in a `baseline_state` table, one row per name (`name`, `format`, `data`, `created_at`, `updated_at`). The `current` row is the live state and holds the same JSON as a state file. Every other row is a named snapshot, so snapshots live in the same database instead of a folder.
* **SQLite locking.** A SQLite target is locked like a file, with `state.db.lock` next to it. The database file is created with mode `0600` and uses WAL journaling, so other tools can read it while Baseline runs.
* **libSQL locking.** A libSQL database is shared, so a lock file cannot protect it. Baseline holds a lease row in a `baseline_state_lock` table and renews it every 10 seconds. A second process is refused with the process and host that hold the lease. A lease that is not renewed for 30 seconds (its holder crashed or lost the network) is taken over with a warning.
* **Latency.** Startup reads one row; saves are debounced whole-state writes, so a remote database does not slow requests down.
* **The token.** The `authToken` URL parameter is removed before the URL appears in any message, banner, or snapshot listing. Prefer `BASELINE_STATE_AUTH_TOKEN`, which keeps it out of your shell history and config file.

## Upgrades

The file records a format version and, for each service, its provider's `stateVersion`. When a Baseline release changes how a provider stores data, it raises that number, and older saved state is refused instead of loaded half broken:

```
.baseline/state.json: Saved state for github has stateVersion 1, but this version of baseline expects 2.
The saved shape is not compatible and is not migrated automatically. Either start with --reset-state to discard it and reseed, or run the version that wrote it (@deploybaseline/core@0.16.0).
```

A file written by a newer Baseline is refused the same way and never overwritten. A file that cannot be parsed stops startup too; fix it, delete it, or use `--reset-state`.

## Named snapshots

Save the running server's state under a name and go back to it later, for example before a test that changes a lot:

```bash theme={null}
npx @deploybaseline/cli snapshot save before-checkout
# ... run the flow ...
npx @deploybaseline/cli snapshot restore before-checkout
```

| Command | |
| - | - |
| `snapshot save <name>` | Save the current state (replaces a snapshot with that name) |
| `snapshot restore <name>` | Replace the current state with the snapshot |
| `snapshot list` | Name, time, size, and services of each snapshot (`--json` for scripts) |
| `snapshot export [name]` | Print the current state, or a snapshot, as a state file (`-o file` writes an owner-only file) |
| `snapshot export --redact` | The same with secrets replaced by placeholders |
| `snapshot import <file>` | Replace the current state with a state file (`-` reads stdin) |
| `snapshot delete <name>` | Delete a snapshot |

The commands find the server through `.baseline/run/<pid>.json`, which `start` writes next to the config (or in the current folder). With several servers running there, pick one with `--port` and any of its service ports. From another folder or machine, pass `--url` and `--token`, or set `BASELINE_CONTROL_TOKEN`.

What a restore does:

* A snapshot holds every running service, built-in and custom. Restoring replaces the state of each running service it holds, including tokens and webhook subscriptions.
* A running service the snapshot does not hold is left as it is. A service in the snapshot that is not running is ignored. The command lists both.
* Everything is checked first. A snapshot whose provider `stateVersion` differs from the running one is refused, and a restore that fails part way is rolled back.
* With `--state`, the restored state is saved like any other change.

Where snapshots live: with `--state .baseline/state.json` they are files in `.baseline/state.snapshots/` and survive restarts. Without `--state` they live in memory until the server stops or a `--watch` reload. Names use letters, digits, `.`, `_`, and `-`, up to 64 characters.

### Fixtures

An export is a complete state file. Commit it and start from it in CI, or load it into a running server:

```bash theme={null}
npx @deploybaseline/cli snapshot export > fixtures/checkout.json
cp fixtures/checkout.json .baseline/ci-state.json
npx @deploybaseline/cli --state .baseline/ci-state.json        # or: snapshot import fixtures/checkout.json
```

`--state` writes to its file, so point it at a copy rather than the committed fixture. An export holds the same tokens and private keys as the state file; only commit one made from fake seed data.

### Redacted exports

`snapshot export --redact` replaces tokens, client secrets, passwords, private keys, signing secrets, and OAuth codes with placeholders such as `redacted:3`. The same secret gets the same placeholder everywhere, so you can still follow which session uses which token. Record IDs, names, emails, and scopes stay as they are. The file is marked `redacted` and cannot be loaded, since its keys and tokens would not work. Redaction looks for secret-sounding field names, so check an export before sharing it outside your team.

### Control endpoints

The commands use these endpoints, which every service port answers. Send the control token printed at start in `X-Baseline-Control-Token`.

| Endpoint | |
| - | - |
| `GET /_baseline/state` | Current state. `?redact=1` for placeholders, `?download=1` to download it as a file |
| `PUT /_baseline/state` | Replace the current state with the state file in the body |
| `GET /_baseline/state/snapshots` | `{ store, snapshots: [{ name, createdAt, size, services }] }` |
| `POST /_baseline/state/snapshots` | `{ "name": "before-checkout" }`. 201 when new, 200 when it replaced one |
| `GET /_baseline/state/snapshots/:name` | One snapshot as a state file (`?redact=1`, `?download=1`) |
| `POST /_baseline/state/snapshots/:name/restore` | `{ snapshot, restored, unchanged, ignored }` |
| `DELETE /_baseline/state/snapshots/:name` | Delete it |

Errors are JSON `{ "error": "..." }`: 400 for a bad name or state file (including a redacted one), 404 for an unknown snapshot, 409 for a `stateVersion` or format mismatch.

### Inspector

The inspector link printed at start (`/_baseline/control/inspector?token=...`) has a State tab: where state is saved and when it was last written, each service's record, token, and webhook counts, download links (with and without redaction), and the snapshot list with Restore, Download, and Delete buttons and a form to save a new one.

### In tests

`createEmulator` has no control endpoints. It has the same operations as methods:

```typescript theme={null}
const github = await createEmulator({ service: 'github', port: 4001 })
const before = await github.snapshot()
// ... exercise the app ...
await github.restore(before)

await github.saveSnapshot('seeded')     // next to `state` (a file, or a row in a SQLite or libSQL target), otherwise in memory
await github.restoreSnapshot('seeded')
```

`state` takes the same targets as `--state`, such as `'sqlite:.baseline/github.db'`. You can also pass a store you opened yourself, which keeps its snapshots in its database:

```typescript theme={null}
import { sqliteState } from '@deploybaseline/state-sqlite'

const github = await createEmulator({ service: 'github', port: 4001, state: await sqliteState('.baseline/github.db') })
```

A store passed this way is not locked; the `sqlite:` string form is.

## Custom emulators

A custom emulator without its own `persistence` option is saved in the same file, under its service name. One with `persistence` keeps using that.

## Security

The file holds access tokens, client secrets, and private signing keys, and so do snapshots and exports.

* The state file, snapshot files, SQLite databases, and `export -o` files are created with mode `0600` (owner only). On Windows the mode has no effect and files inherit their folder's permissions, so keep them under your user profile.
* Keep `.baseline/` out of version control. Share `snapshot export --redact` output instead of a state file.
* `GET /_baseline/state` returns private keys and tokens. The state endpoints need the control token, accept only loopback or configured hosts, and refuse cross-site requests. With `--host 0.0.0.0` they are reachable from the network, still behind the token.
* The inspector cookie can read state but not change it. The State tab's buttons also send a form token that only the inspector page holds, so a page on another local port cannot restore or delete snapshots.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.