Turn it on
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: trueincreateEmulator) 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-stateto 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, andCtrl+Breakon Windows.close()increateEmulator. - Writes are atomic: a temporary file is synced and renamed over the old one, so a crash leaves the previous complete file.
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:
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:
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_statetable, one row per name (name,format,data,created_at,updated_at). Thecurrentrow 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.locknext to it. The database file is created with mode0600and 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_locktable 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
authTokenURL parameter is removed before the URL appears in any message, banner, or snapshot listing. PreferBASELINE_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’sstateVersion. 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:
--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
stateVersiondiffers 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.
--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 inX-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:
sqlite: string form is.
Custom emulators
A custom emulator without its ownpersistence 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 -ofiles are created with mode0600(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. Sharesnapshot export --redactoutput instead of a state file. GET /_baseline/statereturns 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.0they 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.