GraphQL Subscriptions

FLXBL GraphQL subscriptions stream entity lifecycle events to applications over WebSockets. Use them when a user-facing screen should react as data is created, updated, or deleted without polling.

When To Use Subscriptions

  • Live feeds: append new records as soon as another user or process creates them.
  • Dashboards: refresh operational counters and tables after entity changes.
  • Collaborative screens: update visible records when another session edits them.
  • Notifications: show in-app notices for important entity events.
  • Client cache refresh: invalidate or refetch local data after a matching event.
Durable server workflows: Use webhooks when another backend must receive a signed event even if it is offline. Use subscriptions for live clients that can reconnect and resubscribe.

Generated Subscription Fields

For each entity in your active schema, FLXBL generates lower-camel-case create, update, and delete subscription fields.

# Generated for every entity in your active schema
type Subscription {
  productCreated: Product!
  productUpdated(id: ID): Product!
  productDeleted(id: ID): ID!

  # Same lower-camel-case pattern for other entities:
  # customerCreated, customerUpdated, customerDeleted
  # blogPostCreated, blogPostUpdated, blogPostDeleted
}

Basic Operations

Created and updated events return the selected entity fields. Delete events return the deleted entity id. The current implementation supports an optional id argument for update and delete subscriptions.

# Receive every new Product as it is created
subscription WatchProductCreates {
  productCreated {
    id
    name
    price
    createdAt
  }
}

# Receive updates for one Product
subscription WatchOneProduct($id: ID!) {
  productUpdated(id: $id) {
    id
    name
    price
    updatedAt
  }
}

# Variables
{
  "id": "node_abc123"
}

# Receive delete events for one Product
subscription WatchProductDelete($id: ID!) {
  productDeleted(id: $id)
}

Event Payloads

FLXBL sends subscription results in standard graphql-transport-ws next messages. The GraphQL data shape matches your subscription selection set.

# Created and updated events return the selected entity fields
{
  "id": "products-created-1",
  "type": "next",
  "payload": {
    "data": {
      "productCreated": {
        "id": "node_xyz789",
        "name": "New Widget",
        "price": 49.99,
        "createdAt": "2026-04-29T10:30:00Z"
      }
    }
  }
}

# Deleted events return the deleted entity id
{
  "id": "products-deleted-1",
  "type": "next",
  "payload": {
    "data": {
      "productDeleted": "node_abc123"
    }
  }
}

WebSocket Connection

Connect to the shared subscription endpoint with the graphql-transport-ws WebSocket subprotocol, then authenticate in the connection_init payload.

# WebSocket endpoint
wss://api.flxbl.dev/api/v1/dynamic-gql/ws

# Required WebSocket subprotocol
Sec-WebSocket-Protocol: graphql-transport-ws

# connection_init payload
{
  "type": "connection_init",
  "payload": {
    "Authorization": "Bearer <admin-or-end-user-jwt>"
  }
}

# subscribe payload
{
  "id": "products-created-1",
  "type": "subscribe",
  "payload": {
    "query": "subscription WatchProducts { productCreated { id name price } }",
    "operationName": "WatchProducts"
  }
}

Authentication

WebSocket subscriptions currently authenticate with JWTs. Use an admin JWT only from trusted server-side code. Browser applications should subscribe with an end-user JWT from FLXBL end-user authentication.

API keys are still the right fit for CI, scripts, headless agents, REST, and HTTP GraphQL requests, but they do not authenticate the current WebSocket subscription gateway.

Connection Flow

  1. Open a WebSocket connection to wss://api.flxbl.dev/api/v1/dynamic-gql/ws.
  2. Send connection_init with Authorization: Bearer <jwt>.
  3. Wait for connection_ack.
  4. Send one or more subscribe messages with unique ids.
  5. Handle next messages as events arrive.
  6. Send complete for a subscription id when you no longer need it.

What Triggers Events

Subscriptions receive events from entity changes made through both generated REST endpoints and GraphQL mutations. Event delivery is backed by Redis Pub/Sub; if Redis is unavailable, mutations continue but live delivery may not occur until Redis is available again.

Client Implementation

The FLXBL TypeScript SDK and CLI currently cover HTTP GraphQL queries and mutations. For subscriptions, use a WebSocket client that supports graphql-transport-ws.

JavaScript / TypeScript

npm install graphql-ws
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
  webSocketImpl: WebSocket,
  connectionParams: async () => ({
    Authorization: `Bearer ${await getJwt()}`,
  }),
});

const unsubscribe = client.subscribe(
  {
    query: `subscription WatchProductCreates {
      productCreated {
        id
        name
        price
        createdAt
      }
    }`,
  },
  {
    next: (message) => {
      console.log('New product:', message.data?.productCreated);
    },
    error: (error) => {
      console.error('Subscription error:', error);
    },
    complete: () => {
      console.log('Subscription completed');
    },
  }
);

// Later, when the page/component/job no longer needs updates:
unsubscribe();

React Hook

import { useEffect, useMemo, useState } from 'react';
import { createClient } from 'graphql-ws';

type ProductCreated = {
  productCreated?: {
    id: string;
    name: string;
    price: number;
  };
};

export function useProductCreates(getJwt: () => Promise<string>) {
  const [latest, setLatest] = useState<ProductCreated['productCreated'] | null>(null);
  const [connected, setConnected] = useState(false);
  const [error, setError] = useState<unknown>(null);

  const client = useMemo(
    () =>
      createClient({
        url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
        connectionParams: async () => ({
          Authorization: `Bearer ${await getJwt()}`,
        }),
        on: {
          connected: () => setConnected(true),
          closed: () => setConnected(false),
        },
      }),
    [getJwt],
  );

  useEffect(() => {
    const unsubscribe = client.subscribe(
      {
        query: `subscription ProductFeed {
          productCreated {
            id
            name
            price
          }
        }`,
      },
      {
        next: (message) => {
          const data = message.data as ProductCreated | undefined;
          setLatest(data?.productCreated ?? null);
        },
        error: setError,
        complete: () => setConnected(false),
      },
    );

    return () => {
      unsubscribe();
    };
  }, [client]);

  return { latest, connected, error };
}

Python

pip install websockets
import asyncio
import json
import os
from websockets import connect

JWT = os.environ["FLXBL_END_USER_JWT"]
WS_URL = "wss://api.flxbl.dev/api/v1/dynamic-gql/ws"

async def watch_products():
    async with connect(
        WS_URL,
        subprotocols=["graphql-transport-ws"],
    ) as websocket:
        await websocket.send(json.dumps({
            "type": "connection_init",
            "payload": {
                "Authorization": f"Bearer {JWT}",
            },
        }))

        ack = json.loads(await websocket.recv())
        if ack.get("type") != "connection_ack":
            raise RuntimeError(f"Connection failed: {ack}")

        await websocket.send(json.dumps({
            "id": "products-created-1",
            "type": "subscribe",
            "payload": {
                "query": """
                    subscription ProductFeed {
                        productCreated {
                            id
                            name
                            price
                        }
                    }
                """,
            },
        }))

        try:
            async for raw_message in websocket:
                message = json.loads(raw_message)
                if message.get("type") == "next":
                    product = message["payload"]["data"]["productCreated"]
                    print(f"New product: {product['name']} ({product['price']})")
                elif message.get("type") == "error":
                    raise RuntimeError(message["payload"])
                elif message.get("type") == "complete":
                    break
        finally:
            await websocket.send(json.dumps({
                "id": "products-created-1",
                "type": "complete",
            }))

asyncio.run(watch_products())

Error Handling And Reconnection

Treat the socket as an online connection. Reconnect with backoff, refresh the JWT before reconnecting, and resubscribe to the operations your screen still needs.

import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
  connectionParams: async () => ({
    // Fetch a fresh JWT whenever the socket reconnects.
    Authorization: `Bearer ${await getFreshJwt()}`,
  }),
  retryAttempts: Infinity,
  retryWait: async (retries) => {
    const delay = Math.min(1000 * 2 ** retries, 30000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  },
  shouldRetry: (errorOrCloseEvent) => {
    if ('code' in errorOrCloseEvent && errorOrCloseEvent.code === 4401) {
      console.error('Authentication failed. Refresh or replace the JWT.');
      return false;
    }

    return true;
  },
  keepAlive: 30000,
});

Common Close And Error Codes

Code Meaning Action
4401 Authentication failed Refresh or replace the JWT before retrying
4400 Invalid message or subscription query Fix the JSON message or GraphQL operation
4408 Authentication timeout Send connection_init promptly after connecting
1006 Abnormal closure Reconnect with backoff

Next Steps