The graph plane¶
Design authority:
docs/jdlib/capabilities/graph.md— this guide is the developer-facing shape of that page, not a second authority for it. Where the two disagree, the authority page and the code win.
What it is¶
A tenant-bound client for a graph database: a closed vocabulary of labels and relationships, queries built rather than written, and a repository that refuses to touch a node that is not the caller's tenant's.
Why it exists¶
A graph database has no row-level security and no schema to constrain you: a label is a string and a traversal is a program. The library's job is to make the tenant a property of the client and the vocabulary a closed set, so neither is a call site's decision.
When to use it¶
When a tenant's data is genuinely a graph (relationships that are traversed, not joined) and the graph is a derived plane beside the relational record of truth.
When not to use it¶
As the system of record -- the relational plane stays the record -- and for anything a relational query answers in one join. If a deployment has no graph, the plane is simply absent.
How it works¶
GraphConfig describes the connection; GraphClient binds a tenant to it; GraphQuery builds statements from a closed vocabulary (mapping.py); GraphRepository is the tenant-scoped data access; GraphRunner runs a statement for a tenant that is only known at call time -- which is what a surface serving every tenant needs. GraphPolicy and GraphCircuitBreaker are the shared reliability primitives.
Architecture¶
graph/client.py (the connection), graph/tenant.py (the binding), graph/query.py (the builder), graph/mapping.py (the vocabulary), graph/repository.py (tenant-scoped access), graph/transaction.py (write scope), graph/policy.py and graph/errors.py (refusals and reliability).
Example¶
examples/enterprise/app/graph.py is the example's projection of a relational write into the graph, with the tenant taken from the caller's SecurityContext; its live tests run against a real Neo4j and include a mutation check that a wrong vocabulary declaration fails.
Security¶
The tenant is bound to the client, and GraphTenantViolationError is raised when a statement or a node would cross tenants -- a refusal rather than a filter. The vocabulary is closed: a label or relationship that is not declared is refused, which is what stops an injected string from becoming a traversal.
Reliability¶
The graph is derived, so its availability is not the application's availability: GraphCircuitBreaker opens and the relational path keeps serving. Writes are idempotent by key (GraphConflictError on a real conflict), which is what makes a re-projection safe.
Observability¶
Each statement is a span with the operation, the node count and the outcome; the tenant is an attribute, the statement text is not.
Audit¶
Graph writes that mirror a relational write are audited by the operation that performs both, so the trail names the resource once -- not once per plane.
Configuration¶
URI, database and credentials are configuration (GraphConfig); the vocabulary is code, because it is a claim about the model.
Testing¶
The library's graph suites cover the client, the vocabulary, the errors and the observability; the example's live layer covers projection, reads, the identity constraint and a cross-tenant refusal against a real Neo4j.
Common mistakes¶
Reaching the driver directly; interpolating a label from input; treating the graph as the record of truth; re-projecting without a conflict policy; letting a graph outage fail a request that the relational plane could answer.
Production checklist¶
Declare the identity constraint the example declares; keep the vocabulary closed and reviewed; run the projection idempotently; watch the breaker; and keep the relational plane authoritative.