GraphQL Subscriptions

FLXBL provides real-time updates through GraphQL subscriptions. Subscribe to entity lifecycle events (create, update, delete) and receive instant notifications when data changes.

Overview

For each entity in your schema, FLXBL automatically generates three subscription types:

Subscription Event Returns
{entity}Created New entity created Full entity object
{entity}Updated Entity modified Full entity object with updated fields
{entity}Deleted Entity removed Deleted entity ID

Basic Usage

Subscribe to entity lifecycle events using standard GraphQL subscription syntax:

# Subscribe to new product creations
subscription {
  productCreated {
    id
    name
    price
    createdAt
  }
}

# Subscribe to updates on a specific product
subscription {
  productUpdated(id: "node_abc123") {
    id
    name
    price
    updatedAt
  }
}

# Subscribe to all product deletions
subscription {
  productDeleted
}

Filtering Events

Filter events using the where argument for created events, or the id argument to watch a specific entity:

# Subscribe to products with a filter
subscription {
  productCreated(where: { price: { gte: 100 } }) {
    id
    name
    price
  }
}

# Subscribe to updates on any product (no filter)
subscription {
  productUpdated {
    id
    name
    price
    updatedAt
  }
}

Subscription Types

The generated GraphQL schema includes subscription types for all your entities:

# Available subscription types for each entity
type Subscription {
  # Fires when a new entity is created
  productCreated(where: ProductWhereInput): Product!

  # Fires when an entity is updated (optional ID filter)
  productUpdated(id: ID): Product!

  # Fires when an entity is deleted (returns the deleted ID)
  productDeleted(id: ID): ID!

  # Same pattern for all entities:
  # customerCreated, customerUpdated, customerDeleted
  # orderCreated, orderUpdated, orderDeleted
  # etc.
}

Event Payload

Subscription events return data in standard GraphQL response format:

# Event payload format
{
  "data": {
    "productCreated": {
      "id": "node_xyz789",
      "name": "New Widget",
      "price": 49.99,
      "createdAt": "2025-01-15T10:30:00Z"
    }
  }
}

# Delete event payload (returns only the ID)
{
  "data": {
    "productDeleted": "node_abc123"
  }
}

WebSocket Connection

Subscriptions use the graphql-ws protocol over WebSockets. Connect to the subscription endpoint and authenticate with your API key:

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

# Connection init payload (tenantId is extracted from JWT token)
{
  "type": "connection_init",
  "payload": {
    "Authorization": "Bearer flxbl_your_api_key"
  }
}

# Subscribe message
{
  "id": "1",
  "type": "subscribe",
  "payload": {
    "query": "subscription { productCreated { id name } }"
  }
}

Connection Protocol

  1. Open WebSocket connection to wss://api.flxbl.dev/api/v1/dynamic-gql/ws
  2. Send connection_init message with authentication
  3. Receive connection_ack on successful auth
  4. Send subscribe messages for each subscription
  5. Receive next messages with event data
  6. Send complete to unsubscribe

Tenant Isolation

All subscription events are scoped to your tenant. You will only receive events for entities within your tenant's data, even when subscribing to all events without filters.

Security: Events are published to tenant-specific channels. There is no way to subscribe to events from other tenants.

Use Cases

  • Real-time dashboards: Update metrics and charts as data changes
  • Live feeds: Display new content immediately as it's created
  • Collaborative editing: Sync changes across multiple users
  • Notifications: Trigger alerts when specific events occur
  • Cache invalidation: Automatically refresh cached data when it changes

Best Practices

  • Use specific filters to reduce unnecessary event traffic
  • Handle connection drops and implement reconnection logic
  • Unsubscribe when components unmount to prevent memory leaks
  • Consider using the id filter for update/delete subscriptions when watching specific entities

Client Implementation

Ready-to-use client examples for integrating FLXBL subscriptions into your application. All examples use the graphql-ws protocol.

JavaScript / TypeScript

Install the graphql-ws package:

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

// Create a WebSocket client with authentication
const client = createClient({
  url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
  connectionParams: {
    Authorization: `Bearer ${API_KEY}`,
  },
  // Automatic reconnection with exponential backoff
  retryAttempts: 5,
  shouldRetry: () => true,
  on: {
    connected: () => console.log('Connected to FLXBL'),
    closed: () => console.log('Connection closed'),
    error: (err) => console.error('Connection error:', err),
  },
});

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

// Unsubscribe when done
// unsubscribe();

React Hook

A reusable React hook for managing subscriptions with automatic cleanup:

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

// Custom hook for FLXBL subscriptions
function useSubscription<T>(query: string, variables?: Record<string, unknown>) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    const client = createClient({
      url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
      connectionParams: {
        Authorization: `Bearer ${process.env.REACT_APP_FLXBL_API_KEY}`,
      },
      on: {
        connected: () => setIsConnected(true),
        closed: () => setIsConnected(false),
      },
    });

    const unsubscribe = client.subscribe(
      { query, variables },
      {
        next: (result) => setData(result.data as T),
        error: (err) => setError(err as Error),
        complete: () => console.log('Subscription complete'),
      }
    );

    return () => {
      unsubscribe();
      client.dispose();
    };
  }, [query, JSON.stringify(variables)]);

  return { data, error, isConnected };
}

// Usage in a component
function ProductFeed() {
  const { data, error, isConnected } = useSubscription<{
    productCreated: { id: string; name: string; price: number };
  }>(`
    subscription {
      productCreated {
        id
        name
        price
      }
    }
  `);

  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <span>Status: {isConnected ? '🟢 Connected' : '🔴 Disconnected'}</span>
      {data && (
        <div>
          <h3>New Product!</h3>
          <p>{data.productCreated.name} - ${data.productCreated.price}</p>
        </div>
      )}
    </div>
  );
}

Python

Install the websockets package:

pip install websockets
import asyncio
import json
from websockets import connect

API_KEY = "flxbl_your_api_key"
WS_URL = "wss://api.flxbl.dev/api/v1/dynamic-gql/ws"

async def subscribe_to_products():
    async with connect(WS_URL) as websocket:
        # Step 1: Send connection_init with auth
        await websocket.send(json.dumps({
            "type": "connection_init",
            "payload": {
                "Authorization": f"Bearer {API_KEY}"
            }
        }))

        # Step 2: Wait for connection_ack
        response = await websocket.recv()
        ack = json.loads(response)
        if ack["type"] != "connection_ack":
            raise Exception(f"Connection failed: {ack}")
        print("✓ Connected to FLXBL")

        # Step 3: Subscribe to product creations
        await websocket.send(json.dumps({
            "id": "product-sub-1",
            "type": "subscribe",
            "payload": {
                "query": """
                    subscription {
                        productCreated {
                            id
                            name
                            price
                            createdAt
                        }
                    }
                """
            }
        }))
        print("✓ Subscribed to productCreated events")

        # Step 4: Listen for events
        try:
            async for message in websocket:
                data = json.loads(message)

                if data["type"] == "next":
                    product = data["payload"]["data"]["productCreated"]
                    print(f"New product: {product['name']} (${product['price']})")

                elif data["type"] == "error":
                    print(f"Error: {data['payload']}")
                    break

                elif data["type"] == "complete":
                    print("Subscription completed")
                    break

        except KeyboardInterrupt:
            # Step 5: Unsubscribe gracefully
            await websocket.send(json.dumps({
                "id": "product-sub-1",
                "type": "complete"
            }))
            print("Unsubscribed")

# Run the subscription
asyncio.run(subscribe_to_products())

Error Handling & Reconnection

For production applications, implement robust reconnection logic to handle network interruptions gracefully:

import { createClient } from 'graphql-ws';

// Production-ready client with robust reconnection
const client = createClient({
  url: 'wss://api.flxbl.dev/api/v1/dynamic-gql/ws',
  connectionParams: async () => ({
    // Fetch fresh token on each reconnection
    Authorization: `Bearer ${await getApiKey()}`,
  }),

  // Retry configuration
  retryAttempts: Infinity,  // Keep trying forever
  retryWait: async (retries) => {
    // Exponential backoff: 1s, 2s, 4s, 8s, max 30s
    const delay = Math.min(1000 * Math.pow(2, retries), 30000);
    await new Promise(resolve => setTimeout(resolve, delay));
  },

  // Only retry on connection errors, not subscription errors
  shouldRetry: (errOrCloseEvent) => {
    // Don't retry on auth errors (4401)
    if ('code' in errOrCloseEvent && errOrCloseEvent.code === 4401) {
      console.error('Authentication failed - check your API key');
      return false;
    }
    return true;
  },

  // Connection lifecycle hooks
  on: {
    connecting: () => console.log('Connecting...'),
    connected: (socket) => console.log('Connected!'),
    closed: (event) => console.log('Disconnected:', event.reason),
    error: (error) => console.error('Error:', error),
  },

  // Keep-alive ping every 30 seconds
  keepAlive: 30000,
});

Common Error Codes

Code Meaning Action
4401 Authentication failed Check API key, don't retry
4400 Invalid subscription query Fix query syntax, don't retry
1006 Abnormal closure (network) Retry with backoff
1001 Server going away Retry immediately

Next Steps