ZeroLock Quick Start
ZeroLock is a concurrency-correctness substrate: a small stateful gateway that sits between your agents and their shared state, so that two agents touching the same piece of state can't silently clobber, duplicate, or contradict each other. This guide gets a gateway running locally and walks through the two operations everything else in ZeroLock is built on — reading a versioned value, and committing a change to it — in under five minutes.
If you just want the shape of the idea before touching a terminal: every piece of state you coordinate through ZeroLock is a shard. Every shard has a version number. A write has to say which version it's building on, and the gateway only accepts it if that version still matches reality. That one rule — check the version, then write — is what everything downstream (task claiming, conflict resolution, approval gates) is built out of.
Prerequisites
- Node.js 20+ — the gateway and both build tooling and the TypeScript SDK target Node 20's LTS runtime. Older versions may build but aren't tested.
- pnpm (
npm install -g pnpm) — the repository is a pnpm workspace; the packages (gateway,sdk-ts,sdk-py,harness,schema) link against each other locally, which plainnpm/yarnwon't resolve correctly. - Optional but useful: Docker if you'd rather skip local Node/Postgres setup entirely and jump straight to Docker Deployment below, and Python 3.10+ if you plan to follow the Python SDK example.
Verify your toolchain before cloning anything:
node -v # expect v20.x or newer
pnpm -v # expect 8.x or newerInstallation
# Clone the repository
git clone https://github.com/shalwin04/zlock.git
cd zerolock
# Install dependencies across the whole workspace
pnpm install
# Build all packages (gateway, both SDKs, schema, harness)
pnpm buildpnpm build is Turbo-orchestrated, so it builds packages in dependency order — schema first (both SDKs validate against it), then the SDKs and gateway. If this is your first build, expect it to take a minute or two; subsequent builds are incremental.
Start the Gateway
# Start with default settings (SQLite, port 8080)
cd packages/gateway
pnpm devThe gateway is now running at http://localhost:8080, backed by a local SQLite file (zerolock.db, created on first write) — no external database required to get started. Confirm it's alive:
curl http://localhost:8080/health
# { "status": "ok", "timestamp": "...", "websocket": { "connections": 0, "subscriptions": 0 } }This is the same gateway code you'll point production traffic at eventually, with a couple of flags added (--auth-required, --rate-limit). For production you can also point the gateway at PostgreSQL with --postgres (see Deployment — Storage). See Deployment when you get there.
Your First Shard
Before running the SDK examples, it's worth seeing the underlying HTTP calls once, since everything the SDKs do is a thin wrapper over exactly this:
# Create a decision shard — a piece of state with exactly one current value
curl -X POST http://localhost:8080/shards \
-H "Content-Type: application/json" \
-d '{ "id": "counter", "type": "decision", "initial_value": { "count": 0 } }'
# → { "id": "counter", "version": 1 }
# Read it back
curl "http://localhost:8080/shards/counter?agent_id=cli-test"
# → { "value": { "count": 0 }, "version": 1 }
# (note: no "id" or "type" in the response - you already know the id, and a
# plain read doesn't echo the type back; see API Reference for why)
# Commit a new value, referencing the version you just read
curl -X POST http://localhost:8080/shards/counter/commit \
-H "Content-Type: application/json" \
-d '{ "agent_id": "cli-test", "expected_version": 1, "new_value": { "count": 1 } }'
# → { "version": 2, "audit_entry_id": "ae_..." }That expected_version field is the whole trick: if another agent had already committed a change between your read and your write, expected_version: 1 would no longer match the shard's current version, and the gateway would reject the commit with a 409 STALE_VERSION instead of silently overwriting the other agent's work. Try it — commit with expected_version: 1 a second time and watch it fail. That rejection is the entire point of the system: a plain key-value store would have let the second write through and quietly erased the first.
Test with TypeScript SDK
import { ZeroLockClient } from '@zlock/sdk-ts';
async function main() {
const client = new ZeroLockClient({
gateway: 'http://localhost:8080',
agentId: 'my-agent',
});
// Create a shard
await client.createShard('my-shard', {
type: 'decision',
initialValue: { count: 0 },
});
// Read the shard
const shard = await client.getShard('my-shard');
console.log('Value:', shard.value, 'Version:', shard.version);
// Commit an update
await client.commitShard('my-shard', { count: 1 }, shard.version);
console.log('Updated successfully!');
}
main();Install the SDK first (pnpm add @zlock/sdk-ts@beta or npm install @zlock/sdk-ts@beta) if you're running this outside the monorepo. The client is a stateless wrapper — all correctness logic lives in the gateway, so there's nothing to configure beyond the gateway URL and an agent identifier. See TypeScript SDK for the full API, error types, and retry configuration.
Test with Python SDK
import asyncio
from zerolock_sdk import ZeroLockClient, CreateShardOptions
async def main():
async with ZeroLockClient(gateway="http://localhost:8080") as client:
# Create a shard
await client.create_shard("my-shard", CreateShardOptions(
type="decision",
initial_value={"count": 0}
))
# Read the shard
shard = await client.get_shard("my-shard")
print(f"Value: {shard.value}, Version: {shard.version}")
# Commit an update
await client.commit_shard("my-shard", {"count": 1}, shard.version)
print("Updated successfully!")
asyncio.run(main())Install with pip install --pre zerolock-sdk (or poetry add zerolock-sdk --allow-prereleases). The Python client is async-first with a synchronous wrapper (ZeroLockSyncClient) for non-async codebases — see Python SDK.
Seeing a Real Conflict
The single most useful thing to do before writing any real agent code is to watch a conflict happen on purpose. Open two terminals:
Terminal A — read the shard and hold the version, but don't commit yet:
curl "http://localhost:8080/shards/counter?agent_id=agent-a"
# note the version, say it's 2Terminal B — read and commit immediately:
curl "http://localhost:8080/shards/counter?agent_id=agent-b"
curl -X POST http://localhost:8080/shards/counter/commit \
-H "Content-Type: application/json" \
-d '{ "agent_id": "agent-b", "expected_version": 2, "new_value": { "count": 99 } }'
# → succeeds, version is now 3Back in Terminal A, commit using the now-stale version 2:
curl -X POST http://localhost:8080/shards/counter/commit \
-H "Content-Type: application/json" \
-d '{ "agent_id": "agent-a", "expected_version": 2, "new_value": { "count": 1 } }'
# → 409 { "error": "STALE_VERSION", "stale_shard_id": "counter", "current_version": 3, "message": "..." }Agent A's write is rejected instead of silently overwriting agent B's. Note the 409 body gives you the current version, not the current value — if your retry logic needs the value, issue a fresh GET (see API Reference). In your own agent code this is exactly the shape of StaleCommitError (TypeScript) / StaleCommitError (Python) — catch it, re-read, and retry. Both SDKs' "Best Practices" sections show the standard read-modify-commit retry loop.
Appending to a Shared Log (CRDT, No Conflicts Possible)
Not every piece of shared state should reject concurrent writers the way counter just did above. A findings log, an event stream, or anything where "every contribution survives" is the correct outcome belongs on an additive shard instead — and appending to one uses a different operation entirely, not commit:
# Create an additive array shard
curl -X POST http://localhost:8080/shards \
-H "Content-Type: application/json" \
-d '{ "id": "findings", "type": "additive", "config": { "crdt_type": "array" } }'
# Append to it - note the "append" field, not "new_value"
curl -X POST http://localhost:8080/shards/findings/commit \
-H "Content-Type: application/json" \
-d '{ "agent_id": "researcher-1", "expected_version": 0, "append": { "question": "market-size" } }'
# → { "version": 1, "audit_entry_id": "ae_..." }Run that same append command again with a different agent_id and payload, even "concurrently" from two terminals — both survive; there's no version to conflict over, and expected_version is ignored entirely on this path. This is client.appendItem() / client.append_item() under the hood in both SDKs (see TypeScript SDK / Python SDK) — it was verified live at up to 80 concurrent appenders with zero items lost.
Docker Deployment
If you'd rather not install Node/pnpm locally, or you want Postgres, Prometheus, and Grafana running alongside the gateway from the start, use the provided Docker Compose stack:
# Build and start all services
docker-compose up -d
# Check status
curl http://localhost:8080/health
# View metrics
curl http://localhost:8080/metrics
# View Prometheus dashboard
open http://localhost:9090
# View Grafana dashboard
open http://localhost:3000 # admin / zerolockThis starts four services — gateway (8080), postgres (5432), prometheus (9090), and grafana (3000). Metrics and dashboards work out of the box, and the compose file points the gateway at the postgres service through ZEROLOCK_POSTGRES_URL. See Deployment for configuring these services for production use.
Troubleshooting
pnpm build fails on a fresh clone. Delete node_modules at the repo root and in each package, then re-run pnpm install && pnpm build. Turbo caches build outputs; a corrupted cache from an interrupted build is the most common cause.
Port 8080 already in use. Set PORT=8081 pnpm dev (or any free port) and adjust the gateway URL in your client config to match.
curl to /health hangs or connection-refuses. Confirm the gateway process is actually still running in its terminal (it logs "listening on..." on startup) and that nothing else bound port 8080 first.
Node version mismatch errors during install. Check node -v against the Prerequisites above — the workspace's engines field will refuse to install cleanly on Node versions older than 20.
Try the Examples
With the gateway running, the fastest way to see ZeroLock in action is to run the quickstart examples:
# Hello ZeroLock - OCC conflict prevention
cd examples/quickstart/01-hello-zerolock
pip install -r requirements.txt
python main.py
# Task Queue - claim-process-complete pattern
cd examples/quickstart/02-task-queue
python main.py
# Shared Notepad - CRDT append (no conflicts possible)
cd examples/quickstart/03-shared-notepad
python main.pyEach example runs twice — once without coordination (showing the problem) and once with ZeroLock (showing the fix). The output clearly labels what went wrong and what was preserved.
For more complex scenarios with real LLM agents, see the Examples Overview which covers:
- Framework integrations — LangGraph, CrewAI, AutoGen, LlamaIndex
- Real-world use cases — PR review, codebase migration, research coordination
- Advanced patterns — approval gates, file locking, cross-language cooperation
What's Next?
- Examples Overview — working examples from quickstart to production patterns, with code you can run today.
- Architecture — the five-layer design: why shards split into CRDT vs. OCC, how conflict resolution and approval gates fit together, and what's actually tested at what scale.
- API Reference — every HTTP endpoint and WebSocket message type, with request/response shapes and error codes.
- TypeScript SDK — the full client API: caching, task claiming, approvals, retry logic, and typed errors.
- Python SDK — the same surface, async-first with a sync wrapper.
- Deployment — moving from this local SQLite setup to a production Postgres deployment with auth, rate limiting, and monitoring.
- Benchmarking — what's actually been measured against a live gateway (adversarial scenarios, scale sweeps, framework integrations) and what's still an open gap.
