Architecture
Big picture
A SpiceDB node has four layers. The gRPC/HTTP API layer accepts requests and validates them against the schema. The dispatch layer turns one permission question into many smaller sub-checks, caches them, deduplicates them, and (in a cluster) fans them out to other nodes. The graph layer does the actual relationship-graph traversal. The datastore layer reads and writes relationships and schema at a specific revision. The binary entry point is cmd/spicedb/main.go, which builds the cobra command tree whose serve subcommand starts the servers.
Components
API services
The v1 gRPC services live in internal/services/v1/: Permissions, Schema, Watch, and Relationships. permissions.go holds CheckPermission and friends. This layer resolves the request's revision and schema, opens a snapshot reader, and validates that the named object types and relations exist before any graph work begins.
Dispatch layer
internal/dispatch/ distributes the authorization computation. It is a chain: caching/ (memoizes sub-check results), singleflight/ (collapses identical concurrent sub-checks into one), and either graph/ for local traversal or remote/ for redispatch to other nodes in a cluster. combined/combined.go assembles the chain. The local graph dispatcher is wired back into the caching dispatcher so each recursive sub-check is also cached.
Graph engine
internal/graph/ is the traversal engine: check.go for permission checks, expand.go for expanding a relation into its members, and lookupresources*.go / lookupsubjects.go for the reverse queries. ConcurrentChecker in check.go is the core: it either reads tuples directly or recursively evaluates a userset rewrite (a schema rule that derives a permission by combining other relations or permissions with set operations: union, intersection, exclusion).
Datastore
internal/datastore/ and pkg/datastore/ define a common interface with drivers for CockroachDB, PostgreSQL, MySQL, Spanner, and an in-memory store (memdb), plus a proxy. The interface is MVCC-style: reads happen against an explicit revision. The schema DSL lives in pkg/schema/ and pkg/schemadsl/ (lexer, parser, compiler, generator), and core data structures in pkg/tuple/.
How a request flows
A CheckPermission call traces as follows.
(*permissionServer).CheckPermissionatinternal/services/v1/permissions.go:62resolves the revision and schema hash withconsistency.RevisionFromContext(permissions.go:78), opens a snapshot reader (permissions.go:83), builds the Caveat context (permissions.go:85; Caveats are user-defined CEL expressions that attach runtime conditions to relationships, and the context supplies the values they evaluate against), and validates the object types and relations withnamespace.CheckNamespaceAndRelations(permissions.go:95).- It calls
computed.ComputeCheck(permissions.go:124), passingResourceType = tuple.RR(objectType, permission), the subject ObjectAndRelation (ONR), andMaximumDepth = config.MaximumAPIDepth. computeCheckatinternal/graph/computed/computecheck.go:89creates a traversal bloom filter sized to the depth (computecheck.go:113), splits the resource IDs into chunks (computecheck.go:122), and callsd.DispatchCheckper chunk (computecheck.go:123), carrying the bloom filter inMetadata.TraversalBloom(computecheck.go:131).- The dispatch chain runs: a cache hit returns immediately; otherwise singleflight collapses duplicate in-flight checks, and a cluster deployment may redispatch to another node.
- Local traversal runs in
(*ConcurrentChecker).Checkatinternal/graph/check.go:99, thencheckInternal(check.go:165). It first filters resource IDs that match the subject directly (filterForFoundMemberResource,check.go:192). With no userset rewrite it reads tuples incheckDirect(check.go:304); with a rewrite it recurses throughcheckUsersetRewrite(check.go:539), redispatching each child viadispatch(check.go:561). - Results pass through
computeCaveatedCheckResult(computecheck.go:170) for Caveat evaluation and are converted to the API permissionship value before returning.
Key design decisions
- Configurable consistency over always-fresh reads. The datastore exposes both
OptimizedRevision(likely-replicated, lower latency) andHeadRevision(guaranteed fresh) atpkg/datastore/datastore.go:711and:715. Clients pick a consistency level per request, andZedToken(pkg/zedtoken/zedtoken.go) lets them demand "at least as fresh as this earlier write" to avoid the New Enemy problem. - Dispatch as a cache-and-fan-out chain. Building caching, singleflight, and the local graph into one self-referential chain (
internal/dispatch/combined/combined.go:201,:245,:318) means a single check naturally spreads across nodes while each sub-result is memoized. - Pluggable datastores. A single
Datastoreinterface lets the same engine run on CockroachDB, PostgreSQL, MySQL, Spanner, or in-memory, trading Zanzibar's Spanner-only assumption for deployment flexibility.
Extension points
- Datastore drivers implement the
datastore.Datastoreinterface (pkg/datastore/datastore.go), the seam for adding a storage backend (this is how the MySQL driver was contributed). - Caveats are user-defined CEL expressions in the schema (
pkg/caveats/,internal/caveats/) that attach runtime conditions to relationships. - Watch API (
internal/services/v1/) streams relationship changes to downstream consumers in real time.