Skip to main content

GraphQL API

Purpose

The GraphQL API is the exclusive read-surface interface for TUI clients and IDE extensions. It exposes push subscriptions over WebSocket — clients never poll. The control plane pushes projections as events occur (control_plane_api_and_state_model.md §3.2).

v1 deployment note In v1 local-embedded mode, the GraphQL API is served over an in-process transport (bufconn). No port is opened. TUI clients connect through the embedded control plane transparently. Team and enterprise deployments expose the GraphQL API over WebSocket on the syndicate-server endpoint.

Design constraint: single mutation

The GraphQL API is read-only with exactly one exception — the submitDecision mutation, which accepts operator approval/denial responses. This mutation is the only write surface exposed to the TUI. All other state mutations occur via the REST command API (control_plane_api_and_state_model.md CP-API-007).

The GraphQL API cannot mutate policy, issue permits, or modify specialist state.

Subscriptions

The following subscriptions are available:

SubscriptionDescription
sessionStateCurrent session lifecycle state, policy version, turn count
activeEnvelopeCurrent execution envelope scope, permit expiry, approval mode
trustSummaryPer-boundary trust state snapshot, updated on every trust event
evidenceFeedLive stream of audit events for the current session
pendingDecisionscheckpoint_pending events awaiting operator response
turnOutputStreaming model output fragments for the current turn
approvalModeCurrent approval granularity (envelope vs per-step) and policy basis

checkpoint_pending event

The control plane is responsible for assembling the checkpoint_pending payload completely before pushing it to subscribers. The TUI must be able to render the full checkpoint detail view from this payload without additional queries.

submitDecision mutation

The only write operation in the GraphQL API:

mutation SubmitDecision(
  $proposalId: ID!
  $decision: DecisionType!   # APPROVE | DENY
  $idempotencyKey: String!
  $reason: String
) {
  submitDecision(
    proposalId: $proposalId
    decision: $decision
    idempotencyKey: $idempotencyKey
    reason: $reason
  ) {
    decisionId
    outcome
  }
}

The control plane validates the decision against the active checkpoint, records an approval_decision event, evaluates policy, and either issues a permit or records a denial. The mutation returns the recorded decision ID and outcome. It does not return a permit — permits are issued by internal control plane logic and are not client-visible objects.

Deployment topology

Local embedded (v1)

┌─────────────────────────────────────────────────┐
│  syndicate (single process, single binary)       │
│                                                  │
│  ┌────────────┐  gRPC/bufconn  ┌──────────────┐  │
│  │ CLI / TUI  │ ◄────────────► │ Control      │  │
│  │            │  (in-process)  │ Plane        │  │
│  └────────────┘                └──────┬───────┘  │
│                                       │           │
│                                  ┌────▼─────┐    │
│                                  │ SQLite   │    │
│                                  │ (local)  │    │
│                                  └──────────┘    │
└─────────────────────────────────────────────────┘

No server process, no socket path, no daemon, no port. The operator runs syndicate and everything works.

Remote endpoint (team/enterprise)

The control plane is deployed as a standalone syndicate-server binary. The GraphQL API is served over WebSocket on the server endpoint.

                          ┌────────────────────────────────┐
  Developer A             │  syndicate-server              │
  ┌──────────┐  gRPC/TLS  │                                │
  │ syndicate│ ──────────►│  ┌──────────┐  ┌────────────┐ │
  │ (client) │            │  │ Control  │  │ Event Store│ │
  └──────────┘            │  │ Plane    │  │ (shared)   │ │
                          │  └──────────┘  └────────────┘ │
  Developer B             │                                │
  ┌──────────┐  gRPC/TLS  │                                │
  │ syndicate│ ──────────►│                                │
  │ (client) │            └────────────────────────────────┘
  └──────────┘

Configuration seam in ~/.syndicate/config.toml:

[control_plane]
# Empty for local embedded mode (v1 default)
endpoint = ""

# Remote address for Team/Enterprise mode
# endpoint = "grpcs://cp.acme.com:443"
# tls_cert = "/path/to/client.crt"
# tls_key  = "/path/to/client.key"
# tls_ca   = "/path/to/ca.crt"

No code changes or recompilation are required to switch topologies.

Event-sourced state model

All state exposed via GraphQL subscriptions is derived from an append-only event log (control_plane_api_and_state_model.md §4). The event log is the single source of truth.

Derived state (materialized views) must be reconstructable and discardable.

State is derived from proposal events, approval events, permit events, execution events, specialist lifecycle events, and trust state transitions.

Idempotency

All mutating operations (including submitDecision) require an explicit idempotency key (control_plane_api_and_state_model.md §5). Duplicate requests return the original result without creating new events.

Concurrency

All state mutations use optimistic concurrency controls with expected_version. A version mismatch rejects the operation and requires retry with updated state.