Azure / Azure/data-api-builder
[Enh]: GraphQL subscription support
- Dominant language
- C#
- Stars
- 1.5k
- Forks
- 370
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 8
Description
## 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](https://chillicream.com/docs/hotchocolate/v16/server/endpoints/?utm_source=chatgpt.com)][1])
```http
POST /graphql
```
Used for GraphQL queries and mutations.
```http
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](https://chillicream.com/docs/hotchocolate/v16/defining-a-schema/subscriptions/?utm_source=chatgpt.com)][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.
```json
{
"entities": {
"Actor": {
"graphql": {
"enabled": true,
"subscription": {
"events": [ "created", "updated", "deleted" ]
}
}
}
}
}
```
Explicit enable:
```json
{
"graphql": {
"subscription": {
"enabled": true,
"events": [ "created", "updated", "deleted" ]
}
}
}
```
Explicit disable:
```json
{
"graphql": {
"subscription": {
"enabled": false,
"events": [ "created", "updated", "deleted" ]
}
}
}
```
Supported events:
```json
[ "created", "updated", "deleted" ]
```
## Defaults
`graphql.subscription.enabled` defaults to `true` when a `subscription` section exists.
Subscriptions are active only when all are true:
```text
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.
```graphql
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.
```graphql
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.
```graphql
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:
```json
{
"subscription": {
"events": [ "updated" ]
}
}
```
Generates only:
```graphql
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](https://spec.graphql.org/October2021/?utm_source=chatgpt.com)][3])
Valid:
```graphql
subscription {
actorUpdated {
eventId
utcDateTime
actorRole
record {
Id
Name
}
}
}
```
Invalid:
```graphql
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:
```text
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:
```graphql
subscription {
actorDeleted {
eventId
utcDateTime
actorRole
record {
Id
Name
BirthYear
}
}
}
```
Response:
```json
{
"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:
```graphql
mutation {
createActor(item: { Id: 42, FirstName: "Grace", LastName: "Lee", BirthYear: 1980 }) {
Id
}
updateSeries(Id: 1, item: { Name: "Star Trek Updated" }) {
Id
}
}
```
This operation publishes:
```text
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:
```graphql
mutation {
createActors(items: [
{ Id: 42, FirstName: "Grace", LastName: "Lee", BirthYear: 1980 },
{ Id: 43, FirstName: "Alan", LastName: "Kim", BirthYear: 1981 }
]) {
items {
Id
}
}
}
```
This operation publishes:
```text
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](https://github.com/ChilliCream/graphql-platform/blob/main/website/src/docs/hotchocolate/v10/execution-engine/subscriptions.md?utm_source=chatgpt.com)][4])
## Command line
Add `dab update` support for subscription settings.
Enable events:
```sh
dab update Actor --graphql.subscription.events "created,updated,deleted"
```
Explicit enable:
```sh
dab update Actor --graphql.subscription.enabled true
```
Explicit disable:
```sh
dab update Actor --graphql.subscription.enabled false
```
Single event examples:
```sh
dab update Actor --graphql.subscription.events "created"
dab update Actor --graphql.subscription.events "updated"
dab update Actor --graphql.subscription.events "deleted"
```
Empty event list:
```sh
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:
```text
GraphQLSubscriptionCreated
```
DAB must expose an OpenTelemetry measure that counts active GraphQL subscriptions.
Suggested measure name:
```text
dab.graphql.subscriptions.active
```
Subscription telemetry must include:
```text
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:
```text
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:
```text
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:
```json
"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:
```text
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.
[1]: https://chillicream.com/docs/hotchocolate/v16/server/endpoints/?utm_source=chatgpt.com "Endpoints - ChilliCream GraphQL Platform"
[2]: https://chillicream.com/docs/hotchocolate/v16/defining-a-schema/subscriptions/?utm_source=chatgpt.com "Subscriptions - ChilliCream GraphQL Platform"
[3]: https://spec.graphql.org/October2021/?utm_source=chatgpt.com "GraphQL Specification"
[4]: https://github.com/ChilliCream/graphql-platform/blob/main/website/src/docs/hotchocolate/v10/execution-engine/subscriptions.md?utm_source=chatgpt.com "What are GraphQL subscriptions?"
Contributor guide
Assessment
This issue has not been assessed yet.