Tenant isolation and per-tenant usage metering for LangGraph checkpointers and stores
Project description
langgraph-tenancy
Tenant isolation for LangGraph persistence — as a drop-in wrapper.
LangGraph's own threat model says it plainly:
Checkpoint savers index by
thread_id. Without application-level auth, any caller with a valid thread_id can access that thread's state. [...] Users embedding LangGraph directly must implement their own access controls.
If you run a multi-tenant product on open-source LangGraph, the only thing between Customer A's agent state and Customer B's is a query filter in your application code. This package replaces that convention with enforcement.
Install
pip install langgraph-tenancy
Usage
Wrap your existing checkpointer and store. Nothing else changes.
from langgraph_tenancy import (
TenantScopedCheckpointer,
TenantScopedStore,
InMemoryUsageLedger,
)
ledger = InMemoryUsageLedger()
checkpointer = TenantScopedCheckpointer(PostgresSaver(...), usage_ledger=ledger)
store = TenantScopedStore(InMemoryStore())
graph = builder.compile(checkpointer=checkpointer, store=store)
# tenant_id is now REQUIRED on every invocation
graph.invoke(
{"messages": ["hello"]},
config={"configurable": {"thread_id": "t1", "tenant_id": "acme"}},
)
# free per-tenant token metering, extracted from checkpointed messages
ledger.totals("acme") # TenantUsage(input_tokens=..., output_tokens=..., by_model={...})
What it enforces
| Raw LangGraph behavior | With langgraph-tenancy |
|---|---|
Any caller with a thread_id reads that thread |
Threads are physically keyed tenant::thread; wrong-thread_id bugs cannot cross tenants |
| Missing filter → silent unscoped query | Missing tenant_id → TenantRequiredError, nothing read or written |
checkpointer.list(None) enumerates every tenant's threads |
Refused with UnscopedAccessError |
| Store namespaces are convention; any node can read any namespace | Every op is rooted at the tenant segment, resolved from the run config automatically |
delete_thread("t1") deletes whoever owns t1 |
Requires an explicit for_tenant("acme").delete_thread("t1") handle |
usage_metadata buried in checkpoint blobs, unqueryable |
Aggregated per tenant (and per model), deduped by message id |
No magic
The entire mechanism is key prefixing plus mandatory-context checks, in two small files you can audit in ten minutes:
- thread ids become
"{tenant_id}::{thread_id}"before reaching your database; the prefix is stripped from everything returned. - store namespaces
("memories",)become("{tenant_id}", "memories"). - tenant ids containing the separator are rejected, so
acmecan never craft a key that collides with another tenant's space.
It composes with any BaseCheckpointSaver / BaseStore implementation —
Postgres, SQLite, Redis, MongoDB, in-memory — because it never touches
storage itself.
What it is not
- Not authentication. You decide which tenant a request belongs to; this package guarantees that decision is enforced everywhere downstream.
- Not encryption. Combine with
EncryptedSerializerfor at-rest encryption. - Not a replacement for database-level controls in high-assurance setups (RLS, schema-per-tenant) — it's the layer that makes your application unable to leak, whatever the database allows.
Tested
The adversarial test suite — every test attempts a cross-tenant access the
raw LangGraph API allows — runs against InMemorySaver and a real
PostgresSaver in CI. The isolation guarantees are proven on actual SQL
storage, not just the in-memory reference.
Development
uv venv && uv pip install -e ".[test]"
uv run pytest # postgres tests skip if no server is reachable
# to run the postgres leg locally:
export LG_TENANCY_PG_URI=postgresql://user@localhost:5432/langgraph_tenancy_test
uv run pytest
Status
Early (0.1.x). Covered today: sync + async checkpointer paths, sync store
paths, in-memory and Postgres backends. Not yet covered: subgraph
checkpoint_ns edge cases, AsyncPostgresSaver, PostgresStore, store TTL
ops. Issues and PRs welcome.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langgraph_tenancy-0.1.0.tar.gz.
File metadata
- Download URL: langgraph_tenancy-0.1.0.tar.gz
- Upload date:
- Size: 12.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43b3f90d46f223c50c3e57b6b33c382ce1a34370fa7b5ca672d0b4a4f84bf227
|
|
| MD5 |
a6f765ad8382e1f6308402fac0e54970
|
|
| BLAKE2b-256 |
e33e34711fa067e3cf6092f35074c701bb7168b125cf412534513ea7585d942c
|
Provenance
The following attestation bundles were made for langgraph_tenancy-0.1.0.tar.gz:
Publisher:
release.yml on ac12644/langgraph-tenancy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_tenancy-0.1.0.tar.gz -
Subject digest:
43b3f90d46f223c50c3e57b6b33c382ce1a34370fa7b5ca672d0b4a4f84bf227 - Sigstore transparency entry: 1787456298
- Sigstore integration time:
-
Permalink:
ac12644/langgraph-tenancy@38c34b303962cd7b0a8a4800095ff89293c892f5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ac12644
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@38c34b303962cd7b0a8a4800095ff89293c892f5 -
Trigger Event:
release
-
Statement type:
File details
Details for the file langgraph_tenancy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: langgraph_tenancy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b52f426b83527b4a7d5f20c147592c9dddcc47645d4d79f6845cc815f8926e1
|
|
| MD5 |
c6035ed82a6ad0c056d5c4210ab22a59
|
|
| BLAKE2b-256 |
c7365e67b3ca1b87a8ea959c763e65a39691501b81f1a0d6b9344cf3b23c7970
|
Provenance
The following attestation bundles were made for langgraph_tenancy-0.1.0-py3-none-any.whl:
Publisher:
release.yml on ac12644/langgraph-tenancy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_tenancy-0.1.0-py3-none-any.whl -
Subject digest:
8b52f426b83527b4a7d5f20c147592c9dddcc47645d4d79f6845cc815f8926e1 - Sigstore transparency entry: 1787456400
- Sigstore integration time:
-
Permalink:
ac12644/langgraph-tenancy@38c34b303962cd7b0a8a4800095ff89293c892f5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ac12644
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@38c34b303962cd7b0a8a4800095ff89293c892f5 -
Trigger Event:
release
-
Statement type: