Deployment
Start local in one command; scale to distributed when you need it.
TemporalStore runs the same engine two ways. Local mode is a single node on local disk — perfect for development, edge, and self-hosting. Distributed mode fans the same serving core across a proxy, a metaserver, and many datanodes, with replicated durability and, at enterprise scale, shared storage.
One process, one data directory, local disk durability. Best for development, CI, edge, demos, and single-tenant self-hosting.
Proxy, metaserver, and datanodes. Best when you need horizontal read/write scale, failover, rolling upgrades, and shard movement.
MatrixObject shared storage separates compute from durable state, so datanodes can scale elastically while retention lives in the object tier.
Choose a topology
Start with the smallest mode that satisfies durability and scale.
Both modes expose the same client API and data models. The topology underneath changes; your app code should not.
| Need | Run this | Why |
|---|---|---|
| Development, CI, demos, edge, or a single-tenant self-host | Local mode | One container or binary, one mounted data directory, no control plane. |
| High availability and failover | Distributed + MatrixRaft | Each shard has co-located replicas and quorum-committed WAL durability. |
| Many concurrent entities, services, or tenants | Distributed mode | Add datanodes for shard capacity and proxies for client fan-in. |
| Compute/storage separation and elastic retention | Distributed + MatrixObject Enterprise | Datanodes become elastic compute over one shared durable object tier. |
Installation
Three ways to get TemporalStore.
Run the prebuilt container, build the Rust engine from source, or install the Python client into your app. Most people start with the Docker image to run the server and add the Python client for their agent code — the two work together.
| Method | Best for | Command |
|---|---|---|
| Docker image | Running the server — dev, edge, production | docker run … ghcr.io/matrixarkai/temporalstore:latest |
| From source (Cargo) | Building the Rust engine, contributing, custom builds | cargo build --release |
| Python client (pip) | Apps and agents that write events and read context | pip install temporalstore |
Prerequisites
- A container runtime (Docker or Podman) for the image path — or a Rust toolchain (stable, 1.75+) to build from source. Nothing else is required to run the server.
- Python 3.9+ if you use the client SDK from your application or agent.
- Optional: Ollama for fully local, open-source embedding and reader models with no API keys. Hosted providers work too.
- Ports:
8080serves the client API in local mode; in distributed mode the metaserver defaults to9100and the proxy exposes8080.
docker run -d --name temporalstore \
-p 8080:8080 \
-v $PWD/ts-data:/var/lib/temporalstore \
ghcr.io/matrixarkai/temporalstore:latest \
--mode local --data-dir /var/lib/temporalstore
git clone https://github.com/matrixarkai/TemporalStore
cd TemporalStore
cargo build --release
# the server binary lands in target/release
./target/release/temporalstore --mode local \
--data-dir ./ts-data --port 8080
pip install temporalstore
# then, in your application
from temporalstore import Client
ts = Client("http://localhost:8080")
Quickstart
From zero to a context pack in five steps.
The minimal end-to-end flow: start the server, point it at local models, write a couple of events, build a summarized context pack, and read it back. This is the “hello world” of agent memory on TemporalStore.
# 1. start the single-node server
docker run -d --name temporalstore -p 8080:8080 \
-v $PWD/ts-data:/var/lib/temporalstore \
ghcr.io/matrixarkai/temporalstore:latest --mode local
# 2. pull open-source models and point the server at them
ollama pull nomic-embed-text
ollama pull qwen2.5:1.5b
export TS_EMBED_URL=http://127.0.0.1:11434
export TS_EMBED_MODEL=nomic-embed-text
export TS_READER_URL=http://127.0.0.1:11434/v1
export TS_READER_MODEL=qwen2.5:1.5b
from temporalstore import Client
ts = Client("http://localhost:8080")
# 3. write a couple of events
ts.put_event(table="agent_memory", entity="workspace_7", ts_ms=now_ms,
attrs={"kind": "decision", "text": "chose Postgres over DynamoDB"})
ts.put_event(table="agent_memory", entity="workspace_7", ts_ms=now_ms,
attrs={"kind": "note", "text": "latency budget is 50ms p99"})
# 4. build a summarized, token-budgeted context pack
pack = ts.context(entity="workspace_7", since="24h",
summarize=True, token_budget=4096)
# 5. read it back
print(pack.text) # the assembled context
print(pack.tokens) # how much of the budget it used
The same five steps work against a distributed cluster — only the endpoint changes (point the client at the proxy). Your application code is identical in both modes.
Open-source models
Local embeddings and a local reader, no API keys required.
TemporalStore needs an embedding model to index and retrieve events by meaning, and it can use a reader model (an LLM) to summarize retrieved history into a compact context pack. Both run fully local through Ollama — nothing leaves the box — or you can drop in a hosted provider through the very same environment variables.
# embeddings for retrieval + a small local reader for summaries
ollama pull nomic-embed-text
ollama pull qwen2.5:1.5b
# the embedding model indexes and searches events
export TS_EMBED_URL=http://127.0.0.1:11434
export TS_EMBED_MODEL=nomic-embed-text
# the reader model condenses retrieved history into the pack
export TS_READER_URL=http://127.0.0.1:11434/v1
export TS_READER_MODEL=qwen2.5:1.5b
| Role | OSS default (Ollama) | Alternatives | Env vars |
|---|---|---|---|
| Embedding indexes & retrieves |
nomic-embed-text |
all-MiniLM-class or bge-small for a lighter footprint; hosted text-embedding-3 or Voyage as OpenAI-compatible drop-ins. |
TS_EMBED_URL, TS_EMBED_MODEL |
| Reader / LLM summarizes packs |
qwen2.5:1.5b |
qwen2.5:7b for higher-quality summaries; any OpenAI-compatible chat endpoint (OpenAI, or your own gateway). |
TS_READER_URL, TS_READER_MODEL |
Picking a size. The embedding model is the one that matters for retrieval
quality; a compact model such as nomic-embed-text is a strong default and keeps
indexing cheap. The reader only shapes the summary, so a small 1.5b model is fine for
most agent memory — step up to 7b when you want richer rollups and can spend the
extra latency. On CPU-only boxes, keep the reader small; on a GPU you can run the larger reader
comfortably.
Hosted is optional. Point TS_EMBED_URL and
TS_READER_URL at any OpenAI-compatible endpoint to use a hosted provider instead —
the store treats local and hosted models identically, so you can start fully local and switch later
without touching application code.
Two modes, one engine
The same API and data models; the topology grows underneath.
Your application code does not change between modes — it writes events and builds context packs the same way. What changes is how many processes serve those calls and where durable data lives.
Same clients, same models. Local mode is one process on local disk; distributed mode spreads shards across datanodes with replicated or shared durable storage.
Local mode
One node, local disk, running in a minute.
Use local mode for development, CI, edge deployments, single-tenant self-hosting, and trying the store on your own data. It needs nothing but a container runtime; add Ollama if you want fully local, open-source embeddings and readers.
docker run -d --name temporalstore \
-p 8080:8080 \
-v $PWD/ts-data:/var/lib/temporalstore \
ghcr.io/matrixarkai/temporalstore:latest \
--mode local --data-dir /var/lib/temporalstore
# embeddings + a small local reader, no API keys
ollama pull nomic-embed-text
ollama pull qwen2.5:1.5b
export TS_EMBED_URL=http://127.0.0.1:11434
export TS_READER_URL=http://127.0.0.1:11434/v1
from temporalstore import Client
ts = Client("http://localhost:8080")
ts.put_event(table="agent_memory", entity="workspace_7",
ts_ms=now_ms, attrs={"kind": "decision", "text": "..."})
pack = ts.context(entity="workspace_7", since="24h",
summarize=True, token_budget=4096)
Inside the single node
One process, one data directory.
In local mode the serving core, the write-ahead log, and the hot cache all live in a single process. Every durable byte lands under one data directory, so persistence is simply a volume mount and a backup is a directory copy.
Writes hit the WAL first, then materialize into blocks, bands, and slabs; snapshots compact the tail. Mount the data dir on a persistent volume and the whole node survives restarts.
# where durable data lives (mount this on a real volume)
export TS_DATA_DIR=/var/lib/temporalstore
export TS_PORT=8080
# open-source models via Ollama -- no API keys
export TS_EMBED_URL=http://127.0.0.1:11434
export TS_EMBED_MODEL=nomic-embed-text
export TS_READER_URL=http://127.0.0.1:11434/v1
export TS_READER_MODEL=qwen2.5:1.5b
# size the hot working set to available RAM
export TS_CACHE_BYTES=2GiB # in-memory block cache
export TS_WAL_FSYNC=batch # batch fsync for throughput
temporalstore --mode local \
--data-dir "$TS_DATA_DIR" --port "$TS_PORT"
tar the data directory (WAL + blocks/bands/slabs + snapshots) while the node is quiesced or from a volume snapshot. Restore is the reverse: drop it in place and start the binary — WAL replay reconstructs any un-materialized tail.
When local mode is enough. Development, CI, demos, edge and on-device context, and single-tenant self-hosting all run comfortably on one node — the API and data models are identical to distributed mode, so nothing about your application code has to change later.
Its limits. One process means no high availability: if the node is down, the store is down, and you scale it only vertically (bigger box, more disk). When you need failover or horizontal throughput across many entities, move to distributed mode below — same API, same models.
Distributed mode
Scale reads and writes across a cluster.
Distributed mode adds a proxy (routing and batching), a metaserver (shard placement and membership), and multiple datanodes that each own a set of shards. Durability is provided by MatrixRaft (consensus-replicated WAL) or, at enterprise scale, by MatrixObject shared storage so compute and storage scale independently.
services:
metaserver:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role metaserver --listen 0.0.0.0:9100
datanode-a:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role datanode --meta metaserver:9100 --shards 0-341
datanode-b:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role datanode --meta metaserver:9100 --shards 342-683
proxy:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role proxy --meta metaserver:9100
ports: ["8080:8080"]
Clients still talk to a single endpoint (the proxy) and use the same API as local mode. Add datanodes to grow capacity; the metaserver rebalances shards. Choose the durability backend below.
Roles & how they interact
A control plane that places shards; a data plane that serves them.
Three roles divide the work. The proxy is stateless — it caches the routing table and forwards each call to the right datanode. The metaserver is the control plane: it owns the shard map, placement, membership, and leases. The datanodes are the data plane: each owns a set of shards, runs the model executors, and appends to its own WAL.
The metaserver is consulted on membership and placement changes, not on every request — the proxy’s cached routing table keeps reads and writes on a direct proxy→datanode path.
Sharding & placement
Keys hash to buckets; buckets group into shards; the metaserver places shards.
Keys hash into a fixed space of buckets; many buckets group into a shard, which is the unit the metaserver places and replicates. Because the bucket space is fixed, adding a datanode does not reshuffle every key — the metaserver moves whole shards to the new node and updates the map, triggering a rebalance that the proxy picks up on its next routing-table refresh.
Replication & scaling
Choose durability, then scale each plane independently.
Two durability models back a shard. With MatrixRaft, each shard is a co-located full replica set that commits by quorum — storage travels with compute. With MatrixObject shared storage, one durable copy lives in the object tier and datanodes stay stateless, so compute and capacity scale apart. The infrastructure deep dive walks through append-structured storage and WAL replay; MatrixObject Enterprise covers the shared store.
Quorum commit; a failed replica is covered by the rest of the set.
Operations
Health, rolling upgrades, backup, and metrics.
| Concern | How it works |
|---|---|
| Health & readiness | Each role exposes liveness and readiness endpoints; a datanode reports ready only once its shards are loaded and WAL replay has caught up. |
| Rolling upgrade | Drain a datanode — the metaserver moves its shards (or promotes replicas) — then upgrade the binary and rejoin. Repeat node by node for a zero-downtime roll. |
| Backup (MatrixRaft) | Back up per-shard from a follower snapshot; the replicated WAL preserves the commit tail. |
| Backup (shared store) | The object tier holds the single durable copy — back up (or version) it centrally; datanodes carry no unique state. |
| Observability | Prometheus-style metrics for shard health, replica lag, WAL append latency, and cache hit ratio; scrape them per role to watch placement and durability. |
services:
metaserver:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role metaserver --listen 0.0.0.0:9100
ports: ["9100:9100"]
datanode-a:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role datanode --meta metaserver:9100 --shards 0-341
environment:
# --- pick ONE durability backend ---
TS_STORAGE_BACKEND: raft # co-located replicas, quorum commit
# TS_SHARED_STORE_DIR: /mnt/matrixobject # shared store: stateless datanodes
volumes: ["dn_a:/var/lib/temporalstore"]
datanode-b:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role datanode --meta metaserver:9100 --shards 342-683
environment:
TS_STORAGE_BACKEND: raft
volumes: ["dn_b:/var/lib/temporalstore"]
proxy:
image: ghcr.io/matrixarkai/temporalstore:latest
command: --role proxy --meta metaserver:9100
ports: ["8080:8080"]
volumes: { dn_a: {}, dn_b: {} }
Set TS_STORAGE_BACKEND=raft for co-located replicated durability, or
point TS_SHARED_STORE_DIR at the shared object tier for stateless datanodes. See the
deep dive for how the backend is resolved and
MatrixObject Enterprise for the
shared store.
Storage backend
Pick the durable tier for your deployment.
| Backend | Mode | Best for | Licensing |
|---|---|---|---|
| Local disk | Local | Single node, dev, edge, self-hosted. | Open source |
| MatrixRaft | Distributed | Replicated high availability without shared storage; fixed replica set. | Open source |
| MatrixObject Enterprise | Distributed | Disaggregated, concurrent read/write, elastic capacity at five-nines. Details | Enterprise |
Configuration reference
The env vars and flags you will actually set.
Every setting has a sensible default; you only touch what your deployment needs. The same variables apply in local and distributed mode — distributed mode simply adds the role and cluster flags.
| Variable / flag | Default | What it does |
|---|---|---|
--mode / --role | local | Run as a single-node local server, or as a metaserver / datanode / proxy in a cluster. |
TS_DATA_DIR / --data-dir | /var/lib/temporalstore | Directory for all durable data (wal, blocks, bands, slabs, snapshots). Mount it on a persistent volume. |
TS_PORT / --port | 8080 | Client API port for local mode and the proxy. |
TS_STORAGE_BACKEND | raft | Distributed durability backend: raft for co-located replicas (quorum commit) or shared for a shared object tier. |
TS_SHARED_STORE_DIR | unset | Path/mount of the shared store when TS_STORAGE_BACKEND=shared; datanodes stay stateless. MatrixObject Enterprise. |
TS_EMBED_URL / TS_EMBED_MODEL | — | Embedding endpoint (Ollama or OpenAI-compatible) and model name used for retrieval. |
TS_READER_URL / TS_READER_MODEL | — | Reader/LLM endpoint and model used to summarize context packs. Optional — retrieval works without it. |
TS_CACHE_BYTES | 2GiB | Size of the in-memory hot block cache. Give it more RAM to keep a larger working set resident. |
TS_WAL_FSYNC | batch | Durability mode for WAL appends: batch for throughput, sync for per-write fsync. |
--meta / --listen | — | Distributed wiring: --meta host:9100 tells a datanode/proxy where the metaserver is; --listen binds the metaserver. |
--shards | — | Shard range a datanode owns (e.g. 0-341). The metaserver assigns ranges as nodes join. |
Verify the install
Confirm the node is healthy and retrieval round-trips.
Two checks tell you everything: a health/readiness probe that the process is up and caught up, and a write→read round trip that proves indexing and retrieval work end to end.
# liveness -- is the process up?
curl -s localhost:8080/healthz
# {"status":"ok"}
# readiness -- shards loaded and WAL replay caught up?
curl -s localhost:8080/readyz
# {"ready":true,"shards_loaded":true,"wal_replayed":true}
from temporalstore import Client
ts = Client("http://localhost:8080")
ts.put_event(table="agent_memory", entity="verify_1", ts_ms=now_ms,
attrs={"kind": "note", "text": "installation smoke test"})
pack = ts.context(entity="verify_1", since="1h")
assert "installation smoke test" in pack.text
print("OK -- write and retrieval round-trip succeeded")
A healthy node returns ok on /healthz immediately and
ready:true on /readyz once shards are loaded. If /readyz stays
not-ready, the node is still replaying its WAL — give it a moment on large data dirs. In a
cluster, probe each role; the proxy reports ready only when it has pulled the routing table.
Which mode should I run?
Start local; go distributed when one node is not enough.
| If you need… | Mode |
|---|---|
| Development, CI, a demo, or a single-tenant self-host | Local |
| Edge or on-device context with no cluster to run | Local |
| High availability and failover for agent memory | Distributed + MatrixRaft |
| Horizontal scale for concurrent reads/writes across many entities | Distributed |
| Disaggregated compute/storage, elastic retention, five-nines | Distributed + MatrixObject Enterprise |
Client SDKs & agent integrations
Talk to the store from your app — or let your agent do it automatically.
Applications use the Python client (pip install temporalstore) to
write events and build context packs against a local node or a cluster proxy. For coding agents,
TemporalStore ships Claude Code and Codex plugins/hooks that ingest an agent’s
turns as events and retrieve the relevant context back automatically — no glue code, no manual
bookkeeping.
The hooks run alongside your agent session: they append each turn to the store and, on the next turn, pull a fresh context pack and inject it — so long-running work carries its own memory. Both plugins fail open (if the store is unreachable the agent keeps running) and use the same local or hosted models described above. Install instructions live in the GitHub repository.
Cloud API Enterprise
Ingest and retrieve over HTTP — your own apps, at high QPS.
Beyond the agent plugins, enterprise customers connect their own applications to a managed, multi-tenant HTTPS endpoint — ingesting resources and skills through APIs rather than hooks. Push events in; get back a ranked, token-budgeted context pack. Every route is authenticated with a per-tenant API key over TLS, and the ingest path is asynchronous so high-QPS producers never block on durability.
◗ Managed endpoint coming soon. api.temporalstore.ai is being
stood up. The same API runs today on a self-hosted cluster — point your
client at the proxy (see Distributed mode) and use the routes below.
| Endpoint | Purpose | Shape |
|---|---|---|
POST /v1/ingest | Write resources, skills, session events | async 202; batch up to 1,000 records |
POST /v1/session/commit | Close a window; extract entities & summaries | one pass over the session |
POST /v1/retrieve | Ranked, token-budgeted ContextPack | read path; p50 < 2 ms |
PUT / GET /v1/blob/<key> | Large attachments | streamed to shared storage |
POST /v1/mcp | Model Context Protocol over HTTP | for MCP-native clients |
GET /v1/healthz · /readyz | Liveness / readiness | probes |
curl -sS https://api.temporalstore.ai/v1/ingest \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"scope":"acme/agent-7/session-42","records":[
{"type":"resource_chunk","uri":"repo://api/handler.rs","text":"pub async fn handle(...)"},
{"type":"skill_section","name":"deploy-runbook","text":"1. drain 2. roll 3. verify"}]}'
# -> 202 Accepted {"accepted": 2}
curl -sS https://api.temporalstore.ai/v1/retrieve \
-H 'authorization: Bearer sk_live_...' \
-H 'content-type: application/json' \
-d '{"query":"current staging build and how to roll it",
"scope":"acme/agent-7","token_budget":1800}'
# -> 200 OK {"pack":[{"text":"staging = 1.9.2","source":"session-42#evt-8"}, ...],
# "tokens":214}
curl -sS -X PUT --data-binary @report-q3.pdf \
https://api.temporalstore.ai/v1/blob/acme/report-q3.pdf # chunked upload
curl -sS https://api.temporalstore.ai/v1/blob/acme/report-q3.pdf # streamed back
Large files stream directly into the MatrixObject
Enterprise shared-storage tier via append_blob, so
they never bloat the hot ingest path. Tenant metadata (accounts, keys, scopes) is stored as KV
inside TemporalStore itself by default; MatrixKV is an optional drop-in
for a transactional metadata plane. Self-hosted clusters expose the same operations through the
proxy — only the endpoint changes.
Auth, rate limits & quotas
Per-tenant keys, token-bucket limits, contract-raised defaults.
Each request carries a per-tenant bearer key over TLS 1.2+, scoped to one or more namespaces
and rotated from the tenant portal — keys never appear in URLs. Regional endpoints
(api.us.temporalstore.ai, api.eu.temporalstore.ai) keep data in-region.
Limits are enforced per key with a token bucket, reported in X-RateLimit-* headers, and
answered with 429 + Retry-After when exceeded. The defaults below are the
enterprise baseline and are raised per contract — throughput scales horizontally as datanodes
are added.
| Limit / quota | Default | Notes |
|---|---|---|
POST /v1/ingest | 5,000 req/s · 10,000 burst | per tenant key |
POST /v1/retrieve | 6,000 req/s · 12,000 burst | read path |
| Mixed ingest + retrieve | ~5,000 ops/s per 8-core node | linear scale-out |
Max attachment (/v1/blob) | 5 GB | streamed — no client-memory cap |
| Max ingest batch | 1,000 records / 16 MB body | whichever first |
| Storage per tenant | 1 TB (expandable) | records + attachments |
Defaults are soft and set per enterprise contract. Exceeding a quota returns a
machine-readable 413 (payload) or 507 (storage) — ingest never silently drops.
Keep reading