Skip to main content
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 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

Or in baseline.config.json:
  • 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:

Saved state or seed?

Saved state wins. On startup each service either restores or seeds, and the banner says which:
  • 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:
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. Each needs a small package that the CLI loads only when you use it: Run the package next to the CLI with npx -p:
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:
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:
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:
--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. 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:
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:
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.