master branch. The latest version is v0.17.92.
Overview
By default, a subscription resolver returns a plain channel:
func (r *subscriptionResolver) MessageAdded(ctx context.Context, room string) (<-chan *Message, error) {
ch := make(chan *Message, 1)
// populate ch from somewhere
return ch, nil
}
AroundResponses interceptors observe the subscription’s request context for every payload — that is, the context that existed when the subscription started. There is no per-event surface.
@subscriptionContext is an opt-in schema directive that changes this for one field at a time. When a subscription field is annotated, the resolver returns <-chan graphql.Event[T] instead of <-chan T, and each Event carries its own context. The graphql executor threads that context into the ctx parameter that AroundResponses interceptors already receive — no new field on graphql.Response, no new interceptor signature.
Per-field opt-in
Add the directive to your schema and to the schema’s directive declarations:
directive @subscriptionContext on FIELD_DEFINITION
type Subscription {
messageAdded(room: String!): Message! @subscriptionContext
presenceChanged: Presence!
}
messageAdded gets the per-event treatment; presenceChanged keeps the existing shape. The opt-in is per field — other subscriptions in the same project remain unchanged.
Run gqlgen generate and the resolver interface for the marked field becomes:
type SubscriptionResolver interface {
MessageAdded(ctx context.Context, room string) (<-chan graphql.Event[*Message], error)
PresenceChanged(ctx context.Context) (<-chan *Presence, error)
}
Global opt-in
To opt every subscription field into the same behavior without annotating each
field, set subscription_context_field: true in gqlgen.yml:
subscription_context_field: true
This is a project-wide shortcut over the same implementation used by
@subscriptionContext. It does not introduce a second runtime API: generated
subscription resolvers still return <-chan graphql.Event[T], and the event
context still flows through the ctx parameter received by AroundResponses
interceptors.
For example, with subscription_context_field: true, both fields below use
the event-context-aware resolver shape, even without field annotations:
type Subscription {
messageAdded(room: String!): Message!
presenceChanged: Presence!
}
type SubscriptionResolver interface {
MessageAdded(ctx context.Context, room string) (<-chan graphql.Event[*Message], error)
PresenceChanged(ctx context.Context) (<-chan graphql.Event[*Presence], error)
}
Use the directive when only some subscription fields need per-event context. Use the global config when all subscriptions in the schema should expose the same capability.
Publishing events with context
graphql.Event[T] is a plain struct with exported fields:
type Event[T any] struct {
Context context.Context
Value T
}
Build one per published event:
func (r *subscriptionResolver) MessageAdded(
ctx context.Context, room string,
) (<-chan graphql.Event[*Message], error) {
ch := make(chan graphql.Event[*Message], 1)
go func() {
defer close(ch)
for msg := range r.events {
eventCtx, span := tracer.Start(ctx, "subscription.event")
ch <- graphql.Event[*Message]{Context: eventCtx, Value: msg}
span.End()
}
}()
return ch, nil
}
Contract
Event.Contextmust be derived from the subscription request context (typically viacontext.WithValueorcontext.WithCancel). Replacing it with an unrelatedcontext.Background()loses request-scoped values such as the authenticated user and trace IDs. The runtime does not enforce this; it is your contract with the engine.- If
Event.Contextis nil, the engine falls back to the subscription request context for that event. Set it explicitly to avoid surprises.
Reading the context from an interceptor
The interceptor signature is unchanged:
srv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
// ctx is the per-event context that the resolver attached to this event.
// For unmarked subscriptions and for queries/mutations, ctx is the
// operation's request context.
span := trace.SpanFromContext(ctx)
// ... use span ...
return next(ctx)
})
The per-event context flows through ctx — the parameter the interceptor already has.
Resolution-time context contract
When a subscription field is marked, the resolver runs before the AroundResponses chain for each event. As a result:
- Interceptors can still enrich
ctxvianext(ctx2). Subsequent links in the AroundResponses chain seectx2. - Interceptor enrichment does not influence the field-resolver work for the current event (the work has already happened by the time the interceptor sees the response). Enrichment intended for resolvers belongs on
AroundFieldsorAroundRootFields. - For unmarked subscriptions and for queries/mutations, the existing order is preserved: middleware wraps resolver work, so
next(ctx2)does affect resolver context.
Default vs enabled: comparison
Default <-chan T |
Per-field @subscriptionContext |
Global subscription_context_field: true |
|
|---|---|---|---|
| Resolver return type | <-chan T |
<-chan graphql.Event[T] |
<-chan graphql.Event[T] for every subscription |
| AroundResponses ctx | Subscription request ctx | Per-event ctx attached by the resolver | Per-event ctx attached by the resolver |
| Schema opt-in | none | @subscriptionContext on each field |
none required |
| Project-wide config | none | none | subscription_context_field: true |
| Generated code for unmarked fields | byte-identical | byte-identical | all subscription fields use the event-aware shape |
| Per-event tracing / metadata | not available | available for marked fields | available for all subscription fields |
Known limitations
@subscriptionContextdoes not currently compose with theSUBSCRIPTION-location directive middleware. A field cannot be both@subscriptionContext-marked AND have a custom@directive on SUBSCRIPTIONin the same operation; the runtime will use the marked-field path and skip the SUBSCRIPTION middleware.- The contract that
Event.Contextmust derive from the subscription request context is documentation, not enforcement. The runtime cannot inspect the parent chain of an arbitrarycontext.Context.