You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Celeris has no GraphQL story today. Unlike #246 (gRPC), GraphQL needs zero core changes — it's a JSON-over-HTTP protocol and every transport piece already exists:
Transport
Celeris has it
POST application/json query
✅ c.BindJSON + c.JSON
GET ?query=…&variables=…
✅ c.Query family
Batched queries ([{…},{…}])
✅ same JSON path
graphql-transport-ws subscriptions
✅ middleware/websocket (docs already call this out as a canonical subprotocol example)
graphql-sse subscriptions
✅ middleware/sse
APQ (Automatic Persisted Queries) cache
✅ middleware/store.KV
@defer / @stream incremental
✅ StreamWriter (once H2 response trailers land via #246)
Goal: ship middleware/graphql that wires an existing Go executor (user-chosen) into celeris ergonomics, plus the two subscription transports. A from-scratch GraphQL parser + validator + executor is explicitly out of scope for this spike — tracked separately for future evaluation if adoption justifies it.
Scope
In scope
Package layout: middleware/graphql/ as a separate go.mod submodule (matches middleware/protobuf, keeps the executor dependency off the core go.sum).
Transport adapter for HTTP queries + mutations:
POST application/json (standard form)
POST application/graphql (raw query body)
POST multipart/form-data (the file-upload form — spec for operations/map/files)
GET with query, variables, operationName, extensions
Implements the standard timing rules (connection_init timeout, ping/pong keep-alive).
Runs on top of middleware/websocket, reuses its backpressure + pause/resume hooks.
Graceful legacy graphql-ws (Apollo) fallback as an opt-in flag.
Subscription transport: graphql-sse
Both single-connection mode (per spec §2) and distinct-connection mode (§3).
Runs on top of middleware/sse. Emits next / complete events; disconnect cleanup.
APQ via middleware/store.KV: client-sent SHA-256 hash → full query string cache lookup. Miss → 200 with PersistedQueryNotFound error; subsequent POST with query+hash populates the store.
Dependency-free executor. A from-scratch parser + validator + executor is ~10-15 k LOC, 1-2 engineer-months, and has to re-litigate every edge case of GraphQL's null-propagation + 20+ validation rules. Defer to a follow-up spike once adoption signals justify it.
Schema management / codegen. That's the executor's problem, not the middleware's.
APQ: unit tests against an in-memory store.KV, plus one integration pass against Redis (middleware/store's existing Redis adapter).
Cross-engine: run the above against each engine (iouring, epoll, std) — the middleware should be engine-agnostic but the subscription transports touch Detach, so verify per-engine.
Exit criteria
middleware/graphql/ submodule boots with a gqlgen-style executor adapter (pick one for the prototype — gqlgen is the most common so start there) and serves a trivial {hello} query end-to-end.
graphql-http audit passes against the prototype handler (allow ≤ 2 documented exemptions if any).
graphql-ws conformance harness reports green; one working subscription demo against a published event channel.
graphql-sse conformance harness reports green; same subscription demo also over SSE.
APQ hit/miss flow works against both NewMemoryKV and middleware/store/redisstore.
Benchmark: simple query RPS on msr1 vs a baseline net/http + gqlgen server with the same schema. Goal is parity or better — the JSON fast path in celeris should help.
README + godoc showing the three transport wirings.
middleware/store unification (v1.5.0) — APQ is a natural, non-trivial consumer of the unified KV.
middleware/websocket already ships graphql-transport-ws as a subprotocol example — this spike turns that doc note into an actual, conformance-tested integration.
Context
Celeris has no GraphQL story today. Unlike #246 (gRPC), GraphQL needs zero core changes — it's a JSON-over-HTTP protocol and every transport piece already exists:
application/jsonqueryc.BindJSON+c.JSON?query=…&variables=…c.Queryfamily[{…},{…}])graphql-transport-wssubscriptionsmiddleware/websocket(docs already call this out as a canonical subprotocol example)graphql-ssesubscriptionsmiddleware/ssemiddleware/store.KV@defer/@streamincrementalStreamWriter(once H2 response trailers land via #246)Goal: ship
middleware/graphqlthat wires an existing Go executor (user-chosen) into celeris ergonomics, plus the two subscription transports. A from-scratch GraphQL parser + validator + executor is explicitly out of scope for this spike — tracked separately for future evaluation if adoption justifies it.Scope
In scope
middleware/graphql/as a separatego.modsubmodule (matchesmiddleware/protobuf, keeps the executor dependency off the core go.sum).application/json(standard form)application/graphql(raw query body)multipart/form-data(the file-upload form — spec foroperations/map/files)query,variables,operationName,extensions[{…},{…}]array at the top level{"data": …, "errors": [{"message": …, "locations": [...], "path": [...], "extensions": {...}}]}github.com/99designs/gqlgengithub.com/graphql-go/graphqlgithub.com/graph-gophers/graphql-gogithub.com/wundergraph/graphql-go-toolsThe interface is basically
Execute(ctx, request) Response— everything else stays inside the user's executor choice.graphql-transport-wsconnection_init,connection_ack,ping/pong,subscribe,next,error,complete.middleware/websocket, reuses its backpressure + pause/resume hooks.graphql-ws(Apollo) fallback as an opt-in flag.graphql-ssemiddleware/sse. Emitsnext/completeevents; disconnect cleanup.middleware/store.KV: client-sent SHA-256 hash → full query string cache lookup. Miss → 200 withPersistedQueryNotFounderror; subsequent POST with query+hash populates the store.errors[]entry; celeris-layer errors (body too large, malformed JSON) → matching error shape so clients see a clean envelope.c.Context()→ executorctx.Context(so downstream tracing / auth / request-id middlewares just work).Out of scope (explicit non-goals)
@defer/@streamincremental delivery over H2 multipart. Blocked on Spike (v1.5.0): native gRPC support (unary, server/client/bidirectional streaming) #246's trailer work; revisit once landed.Conformance / test strategy
graphql/graphql-httpaudit suite (~100 checks over GET/POST/headers/status codes/error envelope). Wire into CI.enisdenjo/graphql-ws's server-side conformance harness.enisdenjo/graphql-sse's server-side conformance harness.store.KV, plus one integration pass against Redis (middleware/store's existing Redis adapter).Detach, so verify per-engine.Exit criteria
middleware/graphql/submodule boots with a gqlgen-style executor adapter (pick one for the prototype —gqlgenis the most common so start there) and serves a trivial{hello}query end-to-end.graphql-http auditpasses against the prototype handler (allow ≤ 2 documented exemptions if any).graphql-wsconformance harness reports green; one working subscription demo against a published event channel.graphql-sseconformance harness reports green; same subscription demo also over SSE.NewMemoryKVandmiddleware/store/redisstore.net/http + gqlgenserver with the same schema. Goal is parity or better — the JSON fast path in celeris should help.Critical files (likely touched)
middleware/graphql/{go.mod, http.go, ws.go, sse.go, apq.go, errors.go, doc.go, example_test.go}.middleware/websocket+middleware/ssemay grow godoc examples but no API change.Related
middleware/storeunification (v1.5.0) — APQ is a natural, non-trivial consumer of the unified KV.middleware/websocketalready shipsgraphql-transport-wsas a subprotocol example — this spike turns that doc note into an actual, conformance-tested integration.