master
master v0.17.92 v0.17.91 v0.17.90 v0.17.89 v0.17.88 v0.17.87 v0.17.86 v0.17.85 v0.17.84 v0.17.83 v0.17.82 v0.17.81 v0.17.80 v0.17.79 v0.17.78 v0.17.77 v0.17.76 v0.17.75 v0.17.74 v0.17.73

Subscription event context

Per-event context for subscriptions
[edit]
You are looking at the docs for the unreleased 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

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:

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