Run StateMirror locally
Submit one evidence snapshot, retrieve it and verify its integrity. You need Git, Node.js 20 or later, npm, Docker with Compose and curl. Commands below use Bash (macOS, Linux or WSL).
On this page
1. Clone and configure
git clone https://github.com/simplestates/statemirror-core.git
cd statemirror-core
cp .env.example .env
npm installSet these values in the cloned Core repository’s .env. These published test credentials are for local development only.
PORT=8080
READ_API_KEYS=smr_test_read_key_123
WRITE_API_KEYS=smw_test_write_key_456
DATABASE_URL=postgres://statemirror:statemirror@localhost:5432/statemirror2. Start PostgreSQL and Core
Start only the postgres service from the repository’s Compose file. Run Core through npm in this terminal.
docker compose up -d postgres
docker compose exec postgres pg_isready -U statemirror -d statemirror
npm run migrate
npm run build
npm run startWait for pg_isready to report “accepting connections” before migrating. With older Compose installations, use docker-compose in place of docker compose.
In a second terminal:
curl http://localhost:8080/v1/healthConfirm status: healthy and database: connected.
3. Submit evidence
StateMirror accepts application-defined JSON; this site uses one consistent example shape for demonstration. The outer envelope below uses Core’s API fields; Evidence Lanes remain optional.
curl -X POST http://localhost:8080/v1/snapshots \
-H 'Authorization: Bearer smw_test_write_key_456' \
-H 'Idempotency-Key: website-local-demo-001' \
-H 'Content-Type: application/json' \
-d '{
"evidence_ref": "access:customer_123:request_001",
"evidence_type": "access_decision",
"captured_at": "2026-06-15T18:22:31.000Z",
"state_payload": {
"subject": "customer_123",
"inputs": {
"plan_status": "active",
"seats_used": 10,
"seat_limit": 10,
"read_at": "2026-06-15T18:22:30.000Z"
},
"rule_version": "access-rules@2026.06.12",
"decision": "deny",
"reason": "seat_limit_reached",
"correlation_id": "request_001"
}
}'Save the returned snapshot_id. The receipt also includes sequence_num, received_at, payload_hash and chain_hash. Use the same idempotency key only to retry the same submission; choose a new key for a new evidence moment.
Receipt
Abbreviated successful response shape. These values are illustrative; use your returned ID, sequence and hashes.
{
"snapshot_id": "71be5886-a779-4c28-ae90-c389165c90a2",
"sequence_num": 1,
"received_at": "2026-06-15T18:22:31.120Z",
"payload_hash": "PbQTKH7/sGHUg+GjB0acqsNluk4DuU42HAZaHUwQ5r4=",
"chain_hash": "NZpNkbVJ5z7OzkbmqpiGM89RqzAA9A0+eUVvAyUPkOg="
}4. Retrieve the snapshot
Replace PASTE_RETURNED_ID with the ID from the response.
SNAPSHOT_ID='PASTE_RETURNED_ID'
curl "http://localhost:8080/v1/snapshots/$SNAPSHOT_ID" \
-H 'Authorization: Bearer smr_test_read_key_123'Inspect the returned state_payload: the consulted seat counts and declared denial remain the submitted values, regardless of changes in your application database.
5. Verify integrity
curl "http://localhost:8080/v1/snapshots/$SNAPSHOT_ID/verify" \
-H 'Authorization: Bearer smr_test_read_key_123'Verification result
Abbreviated successful response for the example snapshot:
{
"snapshot_id": "71be5886-a779-4c28-ae90-c389165c90a2",
"sequence_num": 1,
"valid": true,
"message": "Preserved evidence integrity verified."
}This checks the preserved payload and its chain linkage. It does not establish that the seat counts were true or that denying access was correct. For a wider chain check, use the documented sequence-range verification endpoint.