Skip to content

[Enh]: GraphQL subscription support #3547

@JerryNixon

Description

@JerryNixon

What

Add GraphQL subscription support to DAB.

GraphQL subscriptions are a third GraphQL operation type alongside queries and mutations. Queries fetch data once. Mutations change data once. Subscriptions keep a long-lived connection open so the server can push events to the client when data changes.

Subscriptions are GraphQL-only. They are additive and invisible unless an entity configures at least one subscription event.

Why

DAB already exposes configured entities through GraphQL queries and mutations. Many clients also need to react to changes without polling.

Subscriptions let clients receive events when DAB creates, updates, or deletes records through REST, GraphQL, or MCP.

Non-goals

This feature does not publish direct database changes.

This feature does not add a separate subscription endpoint.

This feature does not support distributed subscription fan-out in v1.

This feature does not require Redis in v1.

This feature does not support stored procedure subscription events in v1.

This feature does not change existing query or mutation behavior.

This feature does not add subscriptions to REST or MCP.

Endpoint

GraphQL subscriptions use the existing GraphQL endpoint.

The same runtime.graphql.path handles HTTP GraphQL requests and WebSocket upgrade requests. Hot Chocolate’s MapGraphQL() registers the GraphQL endpoint at /graphql by default and supports HTTP GET, HTTP POST, and WebSocket GraphQL requests when ASP.NET Core WebSocket middleware is registered. ([Chillicream]1)

POST /graphql

Used for GraphQL queries and mutations.

GET /graphql
Upgrade: websocket

Used for GraphQL subscriptions.

No separate subscription path is required.

When at least one entity enables GraphQL subscriptions with at least one event, DAB accepts WebSocket upgrade requests on the configured GraphQL path.

Protocol

DAB uses WebSockets for GraphQL subscriptions.

DAB should use the modern graphql-ws protocol. Hot Chocolate supports both modern graphql-ws and legacy subscriptions-transport-ws, but new work should use graphql-ws. ([Chillicream]2)

Legacy transport support is out of scope unless already provided by Hot Chocolate without extra DAB code.

Configuration

Subscriptions are configured under an entity’s GraphQL settings.

{
  "entities": {
    "Actor": {
      "graphql": {
        "enabled": true,
        "subscription": {
          "events": [ "created", "updated", "deleted" ]
        }
      }
    }
  }
}

Explicit enable:

{
  "graphql": {
    "subscription": {
      "enabled": true,
      "events": [ "created", "updated", "deleted" ]
    }
  }
}

Explicit disable:

{
  "graphql": {
    "subscription": {
      "enabled": false,
      "events": [ "created", "updated", "deleted" ]
    }
  }
}

Supported events:

[ "created", "updated", "deleted" ]

Defaults

graphql.subscription.enabled defaults to true when a subscription section exists.

Subscriptions are active only when all are true:

graphql.enabled is true
graphql.subscription.enabled is not false
graphql.subscription.events contains at least one supported event

If subscription.enabled is true and events is empty, DAB starts and logs a warning.

No subscription fields are generated for an empty event list.

Entity support

Tables support subscription events.

Views support subscription events when the configured view supports the matching write operation through DAB.

Stored procedures do not emit subscription events in v1.

If an entity does not support a write operation, the matching event should not be generated.

For example, if an entity does not support delete, deleted should not be exposed for that entity.

Multiple configuration files

Subscriptions must work across merged configuration files.

Entity-level subscription settings follow the same merge behavior as other entity GraphQL settings.

Multiple data sources

Subscriptions must work across multiple configured data sources.

Events are scoped to the entity that raised the event.

The event payload uses the entity’s exposed GraphQL shape, not the physical database object shape.

GraphQL schema

DAB adds a Subscription root type only when at least one entity enables at least one subscription event.

DAB adds a reusable event interface when at least one entity enables at least one subscription event.

interface SubscriptionEvent {
  eventId: UUID!
  utcDateTime: DateTime!
  actorRole: String!
}

eventId is a unique ID generated for each published event.

utcDateTime is the UTC time when DAB published the event.

actorRole is the resolved DAB role that performed the operation, including anonymous.

actorRole does not contain a user name.

Each entity event type implements the shared interface.

type ActorCreatedEvent implements SubscriptionEvent {
  eventId: UUID!
  utcDateTime: DateTime!
  actorRole: String!
  record: Actor!
}

type ActorUpdatedEvent implements SubscriptionEvent {
  eventId: UUID!
  utcDateTime: DateTime!
  actorRole: String!
  record: Actor!
}

type ActorDeletedEvent implements SubscriptionEvent {
  eventId: UUID!
  utcDateTime: DateTime!
  actorRole: String!
  record: Actor!
}

DAB generates subscription fields from the entity singular GraphQL name and the enabled event.

type Subscription {
  actorCreated: ActorCreatedEvent
  actorUpdated: ActorUpdatedEvent
  actorDeleted: ActorDeletedEvent
}

A subscription field appears only when the matching event is present in the entity’s events array.

For example:

{
  "subscription": {
    "events": [ "updated" ]
  }
}

Generates only:

type Subscription {
  actorUpdated: ActorUpdatedEvent
}

Subscription operation rules

A subscription operation must have exactly one root field. This is required by the GraphQL specification. ([spec.graphql.org]3)

Valid:

subscription {
  actorUpdated {
    eventId
    utcDateTime
    actorRole
    record {
      Id
      Name
    }
  }
}

Invalid:

subscription {
  actorCreated {
    record {
      Id
    }
  }
  actorUpdated {
    record {
      Id
    }
  }
}

DAB should rely on GraphQL validation where possible.

Permissions

Subscriptions use the permissions for the event being subscribed to.

A role can subscribe to created only when that role has create permission for the entity.

A role can subscribe to updated only when that role has update permission for the entity.

A role can subscribe to deleted only when that role has delete permission for the entity.

Subscription payload fields also respect read permissions.

If a role cannot read the entity, it cannot receive a payload for that entity.

If a role cannot read a field, that field must not be returned in the subscription payload.

This matters because events are raised across sessions. A delete performed by one client can notify another client only when the receiving client’s role has permission to delete that entity and read the selected fields.

Authorization timing

DAB must authorize the subscription when the subscription operation starts.

DAB must also enforce field permissions when each event payload is resolved.

If role context changes after the WebSocket connection is established, DAB should use the role resolved for that connection unless DAB already has a general mechanism for revalidating identity during long-lived requests.

Change source

Subscriptions publish events for create, update, and delete operations executed through DAB.

Events are raised for successful DAB writes from:

GraphQL mutations
REST writes
MCP writes

Direct database changes are not published.

Events should be published only after the write operation succeeds.

Failed writes must not publish subscription events.

Event payloads

Created events return the created record using the GraphQL entity shape.

Updated events return the updated record using the GraphQL entity shape.

Deleted events return key fields only. Non-key fields return null when selected.

Delete example:

subscription {
  actorDeleted {
    eventId
    utcDateTime
    actorRole
    record {
      Id
      Name
      BirthYear
    }
  }
}

Response:

{
  "data": {
    "actorDeleted": {
      "eventId": "40c5ff11-98d2-4496-bec4-9013a0f20d17",
      "utcDateTime": "2026-05-13T22:14:05Z",
      "actorRole": "authenticated",
      "record": {
        "Id": 42,
        "Name": null,
        "BirthYear": null
      }
    }
  }
}

Delete behavior is intentional. DAB should not query deleted rows after deletion.

Compound GraphQL mutations

Subscriptions must work when a single GraphQL document contains more than one mutation field.

Each successful mutation field publishes its own subscription event.

Example:

mutation {
  createActor(item: { Id: 42, FirstName: "Grace", LastName: "Lee", BirthYear: 1980 }) {
    Id
  }
  updateSeries(Id: 1, item: { Name: "Star Trek Updated" }) {
    Id
  }
}

This operation publishes:

actorCreated
seriesUpdated

If one mutation field succeeds and another fails, DAB publishes events only for the successful mutation fields.

Multiple-create mutations

Subscriptions must work with DAB’s existing multiple-create mutation support.

When a mutation creates multiple records, DAB publishes one created event per created record.

Example:

mutation {
  createActors(items: [
    { Id: 42, FirstName: "Grace", LastName: "Lee", BirthYear: 1980 },
    { Id: 43, FirstName: "Alan", LastName: "Kim", BirthYear: 1981 }
  ]) {
    items {
      Id
    }
  }
}

This operation publishes:

actorCreated
actorCreated

Each event gets its own eventId, utcDateTime, actorRole, and record.

Distributed hosting

Subscriptions do not support distributed hosting in v1.

DAB should use the simplest viable Hot Chocolate subscription provider.

No Redis or distributed pub/sub requirement is included in v1.

This means subscriptions are scoped to one running DAB instance. If a deployment has multiple DAB instances, a subscriber receives only events published by the instance that owns the WebSocket connection.

Hot Chocolate supports Redis subscriptions for multi-instance scenarios, but DAB v1 does not need to add that requirement. ([GitHub]4)

Command line

Add dab update support for subscription settings.

Enable events:

dab update Actor --graphql.subscription.events "created,updated,deleted"

Explicit enable:

dab update Actor --graphql.subscription.enabled true

Explicit disable:

dab update Actor --graphql.subscription.enabled false

Single event examples:

dab update Actor --graphql.subscription.events "created"
dab update Actor --graphql.subscription.events "updated"
dab update Actor --graphql.subscription.events "deleted"

Empty event list:

dab update Actor --graphql.subscription.events ""

An empty event list is valid. No subscription fields are generated. DAB logs a warning when subscriptions are enabled.

Telemetry

DAB must instrument GraphQL subscriptions.

When a subscription is created, DAB logs a subscription-created event.

Suggested log event name:

GraphQLSubscriptionCreated

DAB must expose an OpenTelemetry measure that counts active GraphQL subscriptions.

Suggested measure name:

dab.graphql.subscriptions.active

Subscription telemetry must include:

eventId
entity name
event name
actorRole
operation name when available
connection established
connection closed
active subscription count
duration
success or failure

Telemetry must not include:

authorization token
connection init payload secrets
full GraphQL document if it may contain sensitive values
record payload values

If DAB logs subscription operation details, it should prefer operation name and selected subscription field over the full document.

Validation

DAB must validate subscription configuration at startup and in dab validate.

Validation rules:

graphql.subscription.events can contain only created, updated, deleted
graphql.subscription.enabled must use the standard boolean-or-string behavior
empty events list is valid
enabled true with empty events logs a warning
subscription settings are ignored when graphql.enabled is false
stored procedures cannot enable subscription events
unsupported entity or action combinations do not generate subscription fields

Schema change

Add subscription under entity-level graphql.

Suggested shape:

"subscription": {
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "enabled": {
      "$ref": "#/$defs/boolean-or-string",
      "description": "Enable GraphQL subscriptions for this entity when subscription events are configured.",
      "default": true
    },
    "events": {
      "type": "array",
      "description": "GraphQL subscription events to generate for this entity.",
      "items": {
        "type": "string",
        "enum": [ "created", "updated", "deleted" ]
      },
      "uniqueItems": true,
      "default": []
    }
  }
}

Testing

Tests should cover:

Subscription schema is absent when no entity has subscription events
Subscription schema is absent when enabled is false
Subscription schema is absent when enabled is true but events is empty
Warning is logged when enabled is true and events is empty
created generates only the created field
updated generates only the updated field
deleted generates only the deleted field
Multiple configured events generate multiple schema fields
Multiple root fields in one subscription operation are rejected
Permissions block unauthorized event subscriptions
Field permissions are enforced in subscription payloads
Delete payload returns key values and null non-key values
REST writes raise subscription events
GraphQL mutations raise subscription events
MCP writes raise subscription events
Failed writes do not raise subscription events
Direct database writes do not raise subscription events
Views support subscription events when the matching write operation is supported
Stored procedures do not raise subscription events
Multiple config files merge correctly
Multiple data sources work correctly
Active subscription count changes when subscriptions connect and disconnect
eventId is emitted in payloads and telemetry
Compound GraphQL mutations publish one event per successful mutation field
Multiple-create mutations publish one created event per created record
Existing query and mutation behavior is unchanged

Acceptance criteria

GraphQL subscriptions use the existing runtime.graphql.path.

DAB accepts WebSocket upgrade requests on the GraphQL path when at least one subscription field exists.

DAB uses the modern graphql-ws protocol.

Subscriptions are configured per entity under graphql.subscription.

subscription.enabled defaults to true when the section exists.

events supports created, updated, and deleted.

No subscription schema is generated when no entity has enabled events.

Tables support subscription events.

Views support subscription events when DAB supports the matching write operation.

Stored procedures do not emit subscription events in v1.

The Subscription root type is generated only when at least one subscription event is enabled.

Each event payload includes eventId, utcDateTime, actorRole, and record.

actorRole contains the resolved DAB role, not the user name.

Each subscription operation can contain only one root subscription field.

Subscription access respects entity permissions and field permissions.

DAB publishes events only for successful DAB writes.

REST writes, GraphQL mutations, and MCP writes can raise events.

Direct database writes do not raise events.

Delete events return key fields and null for non-key fields.

Compound GraphQL mutations publish events for successful mutation fields.

Multiple-create mutations publish one created event per created record.

Subscriptions are single-instance only in v1.

DAB does not require Redis or distributed pub/sub for v1.

CLI supports --graphql.subscription.enabled.

CLI supports --graphql.subscription.events.

DAB emits logs and OTEL metrics for subscription lifecycle and active subscription count.

Tests cover schema generation, permissions, event publishing, delete payloads, compound mutations, multiple-create mutations, config merging, multiple data sources, and telemetry.

Metadata

Metadata

Labels

No fields configured for Feature.

Projects

Status

Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions