Skip to content

GraphQL Subscriptions

GraphQL subscriptions provide real-time updates through WebSocket connections. api.zig supports both the graphql-ws and legacy subscriptions-transport-ws protocols.

Overview

Subscriptions enable clients to receive push notifications when data changes, perfect for:

  • Live notifications
  • Real-time chat
  • Live dashboards
  • Collaborative editing
  • Gaming events

Basic Setup

Schema Definition

zig
const std = @import("std");
const api = @import("api");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    var app = try api.App.init(allocator, .{});
    defer app.deinit();

    var schema = api.GraphQLSchema.init(allocator);
    defer schema.deinit();

    // Define subscription type
    try schema.setSubscriptionType(.{
        .name = "Subscription",
        .fields = &.{
            .{
                .name = "messageAdded",
                .type_name = "Message",
                .is_non_null = true,
                .args = &.{
                    .{ .name = "channelId", .type_name = "ID", .is_non_null = true },
                },
                .description = "Subscribe to new messages in a channel",
            },
            .{
                .name = "userStatusChanged",
                .type_name = "UserStatus",
                .is_non_null = true,
                .description = "Subscribe to user status changes",
            },
            .{
                .name = "orderUpdated",
                .type_name = "Order",
                .args = &.{
                    .{ .name = "orderId", .type_name = "ID", .is_non_null = true },
                },
                .description = "Subscribe to order updates",
            },
        },
    });

    // Enable GraphQL with subscriptions
    try app.enableGraphQL(&schema, .{
        .enable_subscriptions = true,
        .subscription_config = .{
            .protocol = .graphql_ws,
            .keep_alive = true,
            .keep_alive_interval_ms = 30000,
        },
    });

    try app.run(.{ .port = 8000 });
}

Subscription Configuration

SubscriptionConfig

zig
pub const SubscriptionConfig = struct {
    /// WebSocket protocol to use
    protocol: SubscriptionProtocol = .graphql_ws,
    
    /// Enable keep-alive pings
    keep_alive: bool = true,
    
    /// Keep-alive interval in milliseconds
    keep_alive_interval_ms: u32 = 30000,
    
    /// Connection timeout in milliseconds
    connection_timeout_ms: u32 = 30000,
    
    /// Maximum retry attempts
    max_retry_attempts: u32 = 5,
    
    /// Retry delay in milliseconds
    retry_delay_ms: u32 = 1000,
    
    /// Lazy connection (connect on first subscription)
    lazy: bool = true,
    
    /// Maximum concurrent subscriptions per connection
    max_subscriptions: u32 = 100,
    
    /// Enable connection acknowledgment timeout
    ack_timeout_ms: u32 = 10000,
};

Subscription Protocols

zig
pub const SubscriptionProtocol = enum {
    /// Modern graphql-ws protocol
    graphql_ws,
    
    /// Legacy subscriptions-transport-ws protocol
    subscriptions_transport_ws,
    
    /// Server-Sent Events (SSE)
    sse,
};

WebSocket Endpoint

Default Setup

zig
try app.enableGraphQL(&schema, .{
    .path = "/graphql",
    .enable_subscriptions = true,
    // WebSocket endpoint: ws://localhost:8000/graphql/ws
});

Custom WebSocket Path

zig
try app.enableGraphQL(&schema, .{
    .path = "/api/graphql",
    .subscription_path = "/api/graphql/subscriptions",
    .enable_subscriptions = true,
});

UI Configuration

GraphiQL with Subscriptions

zig
try app.enableGraphQL(&schema, .{
    .graphiql_path = "/graphql/graphiql",
    .ui_config = .{
        .endpoint = "/graphql",
        .subscription_endpoint = "ws://localhost:8000/graphql/ws",
    },
});

Playground with Subscriptions

zig
try app.enableGraphQL(&schema, .{
    .playground_path = "/graphql/playground",
    .ui_config = .{
        .provider = .playground,
        .endpoint = "/graphql",
        .subscription_endpoint = "ws://localhost:8000/graphql/ws",
    },
});

Client Usage

JavaScript Client (graphql-ws)

javascript
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'ws://localhost:8000/graphql/ws',
  connectionParams: {
    authToken: 'your-token',
  },
});

// Subscribe to messages
const unsubscribe = client.subscribe(
  {
    query: `
      subscription MessageAdded($channelId: ID!) {
        messageAdded(channelId: $channelId) {
          id
          text
          sender {
            name
          }
        }
      }
    `,
    variables: { channelId: '123' },
  },
  {
    next: (data) => console.log('Message:', data),
    error: (err) => console.error('Error:', err),
    complete: () => console.log('Subscription complete'),
  }
);

// Later: unsubscribe
unsubscribe();

Apollo Client

javascript
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({
  uri: 'http://localhost:8000/graphql',
});

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:8000/graphql/ws',
  })
);

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

Common Patterns

Chat Application

zig
// Schema
try schema.addObjectType(.{
    .name = "Message",
    .fields = &.{
        .{ .name = "id", .type_name = "ID", .is_non_null = true },
        .{ .name = "text", .type_name = "String", .is_non_null = true },
        .{ .name = "sender", .type_name = "User", .is_non_null = true },
        .{ .name = "channelId", .type_name = "ID", .is_non_null = true },
        .{ .name = "createdAt", .type_name = "DateTime", .is_non_null = true },
    },
});

try schema.setSubscriptionType(.{
    .name = "Subscription",
    .fields = &.{
        .{
            .name = "messageAdded",
            .type_name = "Message",
            .is_non_null = true,
            .args = &.{
                .{ .name = "channelId", .type_name = "ID", .is_non_null = true },
            },
        },
        .{
            .name = "userTyping",
            .type_name = "TypingIndicator",
            .args = &.{
                .{ .name = "channelId", .type_name = "ID", .is_non_null = true },
            },
        },
    },
});

Live Dashboard

zig
try schema.setSubscriptionType(.{
    .name = "Subscription",
    .fields = &.{
        .{
            .name = "metricsUpdated",
            .type_name = "Metrics",
            .is_non_null = true,
            .description = "Real-time metrics updates",
        },
        .{
            .name = "alertTriggered",
            .type_name = "Alert",
            .is_non_null = true,
            .description = "New alert notifications",
        },
        .{
            .name = "serverStatus",
            .type_name = "ServerStatus",
            .is_non_null = true,
            .args = &.{
                .{ .name = "serverId", .type_name = "ID" },
            },
        },
    },
});

Collaborative Editing

zig
try schema.setSubscriptionType(.{
    .name = "Subscription",
    .fields = &.{
        .{
            .name = "documentChanged",
            .type_name = "DocumentChange",
            .is_non_null = true,
            .args = &.{
                .{ .name = "documentId", .type_name = "ID", .is_non_null = true },
            },
        },
        .{
            .name = "cursorMoved",
            .type_name = "CursorPosition",
            .args = &.{
                .{ .name = "documentId", .type_name = "ID", .is_non_null = true },
            },
        },
        .{
            .name = "userJoined",
            .type_name = "User",
            .args = &.{
                .{ .name = "documentId", .type_name = "ID", .is_non_null = true },
            },
        },
    },
});

Authentication

Connection-Level Auth

zig
try app.enableGraphQL(&schema, .{
    .enable_subscriptions = true,
    .subscription_config = .{
        .on_connect = struct {
            fn authenticate(params: anytype) !bool {
                const token = params.get("authToken") orelse return false;
                return validateToken(token);
            }
        }.authenticate,
    },
});

Client Connection Params

javascript
const client = createClient({
  url: 'ws://localhost:8000/graphql/ws',
  connectionParams: async () => ({
    authToken: await getAuthToken(),
    userId: getCurrentUserId(),
  }),
});

Error Handling

Subscription Errors

javascript
client.subscribe(
  { query: subscriptionQuery },
  {
    next: (data) => handleData(data),
    error: (errors) => {
      if (errors instanceof CloseEvent) {
        // Connection closed
        console.log('Connection closed:', errors.code, errors.reason);
      } else {
        // GraphQL errors
        console.error('Subscription error:', errors);
      }
    },
    complete: () => console.log('Subscription completed'),
  }
);

Reconnection

javascript
const client = createClient({
  url: 'ws://localhost:8000/graphql/ws',
  retryAttempts: 5,
  retryWait: (retries) => {
    // Exponential backoff
    return new Promise((resolve) =>
      setTimeout(resolve, Math.min(1000 * 2 ** retries, 30000))
    );
  },
  on: {
    connected: () => console.log('Connected'),
    closed: (event) => console.log('Closed:', event),
    error: (error) => console.error('Error:', error),
  },
});

Best Practices

1. Use Specific Subscriptions

graphql
# Good: Subscribe to specific resources
subscription OrderUpdates($orderId: ID!) {
  orderUpdated(orderId: $orderId) {
    status
    updatedAt
  }
}

# Avoid: Broad subscriptions without filters
subscription AllOrderUpdates {
  allOrders {
    ...everything
  }
}

2. Handle Disconnections

javascript
const client = createClient({
  url: 'ws://localhost:8000/graphql/ws',
  keepAlive: 30000, // 30 seconds
  on: {
    ping: () => console.log('Ping'),
    pong: () => console.log('Pong'),
  },
});

3. Limit Concurrent Subscriptions

zig
try app.enableGraphQL(&schema, .{
    .subscription_config = .{
        .max_subscriptions = 10, // Per connection
    },
});

See Also

Released under the MIT License.