Graph Gateway API (draft)
This document describes the Graph Gateway (StellaOps.Graph.Api) HTTP surface and points to the authoritative contracts. It is reconciled against the implementation in src/Graph/StellaOps.Graph.Api (notably Program.cs, the Endpoints/* and Contracts/* files, and Security/GraphPolicies.cs).
Audience: API client integrators building impact-analysis, lineage, or export workflows against the Graph Gateway, and UI authors consuming its streaming tile protocol.
Authoritative contracts
- OpenAPI (endpoints, schemas, responses):
docs/api/graph-gateway-spec-draft.yaml- The OpenAPI file is an early draft (
version: 0.0.3-pre) and is not fully aligned with the shipped service. Where it diverges from the implementation, this page is the corrected reference; the specific divergences are called out under “Known spec drift” below.
- The OpenAPI file is an early draft (
- Overlay + tile cache schema (materialized/cached representation):
docs/api/graph/overlay-schema.md - Sample cached tile:
docs/api/graph/samples/overlay-sample.json
What the Graph Gateway is for
Graph Gateway provides deterministic, tenant-scoped graph queries used for:
- Impact analysis and traversal (search/query/paths/diff)
- Lineage and edge-metadata (provenance) lookups
- Overlay enrichment (policy, VEX, AOC/advisory) when requested
- Export workflows for offline analysis and audit
- Cartographer-compatible build/overlay job endpoints used by the Scheduler Worker
Endpoints
All routes are minimal-API endpoints registered in src/Graph/StellaOps.Graph.Api/Program.cs and the Endpoints/*.cs files. Streaming routes return application/x-ndjson; the remaining routes return application/json.
Streaming graph routes
| Method & path | Scope policy | Streaming? | Notes |
|---|---|---|---|
POST /graph/search | graph:read or graph:query | NDJSON | Node + cursor tiles. |
POST /graph/query | graph:query | NDJSON | Node/edge/stats/cursor tiles; may return 501 (see below). |
POST /graph/paths | graph:query | NDJSON | Path tiles (pathHop set); may return 501. |
POST /graph/diff | graph:query | NDJSON | Snapshot diff tiles (added/removed/changed). |
JSON graph routes
| Method & path | Scope policy | Notes |
|---|---|---|
POST /graph/lineage | graph:read or graph:query | Returns a lineage object (not NDJSON). |
POST /graph/export | graph:export | Returns a completed export manifest (200, status:"completed"). On live hosts this currently returns 501 (GRAPH_FEATURE_UNAVAILABLE); see “Backend availability”. |
GET /graph/export/{jobId} | graph:export | Downloads the export file; sets X-Content-SHA256. 404 GRAPH_EXPORT_NOT_FOUND for unknown/cross-tenant jobs. |
POST /graph/edges/metadata | graph:read or graph:query | Batch edge-metadata lookup. |
GET /graph/edges/{edgeId}/metadata | graph:read or graph:query | Single edge; 404 EDGE_NOT_FOUND. |
GET /graph/edges/path/{sourceNodeId}/{targetNodeId} | graph:query | Edges along a path with metadata. |
GET /graph/edges/by-reason/{reason} | graph:read or graph:query | reason must parse to an EdgeReason; 400 INVALID_REASON otherwise. Supports limit (default 100) and cursor. |
GET /graph/edges/by-evidence | graph:read or graph:query | Query params evidenceType, evidenceRef. |
GET /healthz | (anonymous) | { "status": "ok" }. |
The edge-metadata family (/graph/edges/...) returns 501 GRAPH_FEATURE_UNAVAILABLE on live hosts; see “Backend availability”.
Compatibility routes (CompatibilityEndpoints.cs)
Tenant-scoped read/query routes used by the UI/legacy clients (all enforce .RequireTenant()):
GET /graphs,GET /graphs/{graphId},GET /graphs/{graphId}/tiles—graph:read/graph:queryGET /search,GET /assets/{assetId}/snapshot,GET /nodes/{nodeId}/adjacency—graph:read/graph:queryGET /paths—graph:queryGET /graphs/{graphId}/export—graph:exportGET /graphs/{graphId}/saved-views—graph:read/graph:queryPOST /graphs/{graphId}/saved-views,DELETE /graphs/{graphId}/saved-views/{viewId}—graph:query
Asset registry routes (AssetRegistryEndpoints.cs, base /graph/assets)
GET /graph/assets,POST /graph/assets/query,GET /graph/assets/{assetId}—graph:read/graph:queryPOST /graph/assets/export—graph:export
Cartographer-compatible job routes (CartographerEndpoints.cs)
These match the Scheduler Worker’s HTTP client contracts and are not scope-gated by GraphPolicies (they rely on tenant/identity from the request body and the audit pipeline):
POST /api/graphs/builds,GET /api/graphs/builds/{jobId}—404BUILD_NOT_FOUNDfor unknown ids.POST /api/graphs/overlays,GET /api/graphs/overlays/{jobId}—404OVERLAY_NOT_FOUNDfor unknown ids.
Job ids are deterministic: an Idempotency-Key header collapses retries; otherwise the id derives from the canonical request body so byte-equal POSTs collapse to the same job.
Tenancy and authentication
- Tenancy is mandatory for graph routes.
- Authentication uses a custom header scheme (
GraphHeaderAuthenticationHandler). Missing/invalid credentials yield401GRAPH_UNAUTHORIZED. Authentication is checked before tenant resolution so unauthenticated requests always get401(not400). - The canonical tenant header is
X-StellaOps-TenantId(StellaOpsHttpHeaderNames.Tenant). The resolver also accepts the legacy aliasesX-Stella-TenantandX-Tenant-Id, and a tenant claim (tenant,tid, ortenant_id) on the principal. Conflicting tenant values across header(s)/claim yield400GRAPH_VALIDATION_FAILED(tenant conflict). A missing tenant yields400GRAPH_VALIDATION_FAILED(tenant missing). See the Gateway Tenant Auth & ABAC Contract. - Scopes per route (
src/Graph/StellaOps.Graph.Api/Security/GraphPolicies.cs):graph:read(StellaOpsScopes.GraphRead) — read/lineage/edge-metadata, in combination with query.graph:query— query/paths/diff. Note: this is a service-local constant (GraphPolicies.GraphQueryScope), not a member of the canonicalStellaOpsScopescatalog. The catalog definesgraph:read,graph:write,graph:export,graph:simulate, andgraph:admin.graph:export(StellaOpsScopes.GraphExport) — export routes.- Missing scope yields
403GRAPH_FORBIDDEN.
NDJSON tile streaming
The streaming routes (/graph/search, /graph/query, /graph/paths, /graph/diff) emit application/x-ndjson, where each line is a serialized TileEnvelope (Contracts/SearchContracts.cs).
Envelope fields:
type: tile discriminator (string). Actual values emitted by the services:- search:
node,cursor, anderror(on failure) - query:
node,edge,stats,cursor, anderror - paths:
node,edge,stats, anderror - diff:
node_added,node_removed,node_changed,edge_added,edge_removed,edge_changed,stats, anderror
- search:
seq: monotonic integer per response stream (starts at0).cost: optional per-streamCostBudget(limit,remaining,consumed).data: payload varies bytype(NodeTile,EdgeTile,StatsTile,CursorTile, diff records, orErrorResponse).
There is no
diagnostictile type in the implementation. Errors surfaced mid-stream usetype: "error"carrying anErrorResponsepayload. The draft OpenAPI’sdiagnosticenum value andDiagnosticTileschema are not implemented.
Request shapes and limits
Validators in Contracts/SearchContracts.cs enforce (selected rules):
search/query:kindsis required;limitmust be 1–500; at least one ofquery,filters, orcursormust be supplied;ordering(search) must berelevanceorid.paths:sourcesandtargetsrequired;maxDepth1–6.diff:snapshotAandsnapshotBrequired.export:formatmust be one ofndjson,csv,graphml,png,svg(defaultndjson).- Optional
budget(GraphQueryBudget):tiles,nodes,edgescaps. Defaults applied when omitted aretiles=6000,nodes=5000,edges=10000.
Validation failures return 400 with error: "GRAPH_VALIDATION_FAILED".
Cursor and resume
Search/query responses include a cursor tile carrying an opaque token and a resumeUrl when more results remain. Requests can pass a cursor field to resume from a prior stream.
Budgets and rate limits
- A fixed-window in-process rate limiter (
RateLimiterService, default 120 requests per window, partitioned by tenant + route) gates streaming/JSON routes. When tripped the service returns429witherror: "GRAPH_RATE_LIMITED". - The service does not currently emit
X-RateLimit-RemainingorRetry-Afterheaders. Those headers appear in the draft OpenAPI but are not set by the implementation. The only response header the service sets explicitly isX-Content-SHA256(on export downloads). - Per-stream cost is reported in-band via the
costfield of eachTileEnvelope, not via headers.
Overlays
node and edge tiles may carry overlays when requested (includeOverlays: true). Overlays are keyed by overlay kind and use the OverlayPayload shape { kind, version, data }.
On live (non-Testing) hosts the overlay service is UnsupportedGraphOverlayService, which throws GRAPH_FEATURE_UNAVAILABLE (501) rather than synthesizing policy/VEX/AOC overlays. See “Backend availability”.
For the cache/materialized representation used by gateway/UI tile caches, see docs/api/graph/overlay-schema.md.
Backend availability
The service guards configuration on startup: outside the Testing environment a Graph Postgres connection string (Postgres:Graph:ConnectionString or ConnectionStrings:Default) is required, or startup fails.
Several features are backed by Unsupported* services on live hosts and therefore return 501 with error: "GRAPH_FEATURE_UNAVAILABLE" (and a details.feature discriminator) until durable backends are wired:
overlays(UnsupportedGraphOverlayService)export(UnsupportedGraphExportService) — bothPOST /graph/exportandGET /graph/export/{jobId}edge-metadata(UnsupportedEdgeMetadataService) — the entire/graph/edges/...family
search, query, paths, and diff are served by in-memory implementations over the Postgres runtime repository and remain available.
Errors and determinism
Error responses use the ErrorResponse shape { error, message, details?, requestId? }. Codes emitted by the service:
GRAPH_UNAUTHORIZED(401)GRAPH_FORBIDDEN(403)GRAPH_VALIDATION_FAILED(400)GRAPH_RATE_LIMITED(429)GRAPH_FEATURE_UNAVAILABLE(501)GRAPH_EXPORT_NOT_FOUND,EDGE_NOT_FOUND,INVALID_REASON(graph routes)BUILD_NOT_FOUND,OVERLAY_NOT_FOUND(Cartographer routes)
Determinism requirements:
- stable ordering for equivalent requests (search results scored then ordered by id; cache key is order-insensitive over kinds/filters)
- stable ids for nodes/edges where defined by schema
- UTC ISO-8601 timestamps; runtime audit/observability timestamps are explicitly excluded from canonical graph snapshots.
Observability
Metrics are emitted via the StellaOps.Graph.Api meter (Services/GraphMetrics.cs):
graph_query_budget_denied_total(counter)graph_tile_latency_seconds(histogram, seconds)graph_overlay_cache_hits_total/graph_overlay_cache_misses_total(counters)graph_export_latency_seconds(histogram, seconds)
Per-request audit events (tenant, route, method, actor, scopes, status, duration) are logged and forwarded to the unified audit pipeline.
Known spec drift (draft OpenAPI vs. implementation)
The following items in docs/api/graph-gateway-spec-draft.yaml do not match the shipped service:
diagnostictile type /DiagnosticTileschema — not implemented (errors usetype: "error").X-RateLimit-Remaining/Retry-Afterresponse headers — not emitted by the service.POST /graph/exportis documented as202 Acceptedreturning an async job; the service returns200 OKwithstatus:"completed"(or501on live hosts).- The draft documents
/graph/export/{jobId}/manifest; the service exposes the download atGET /graph/export/{jobId}and setsX-Content-SHA256. - The draft omits
/graph/lineage, the/graph/edges/...family, the compatibility routes (/graphs,/search,/paths, etc.), the asset-registry routes (/graph/assets/...), and the Cartographer routes (/api/graphs/...).
