DocsRealtime

Realtime Subscriptions

Live data synchronization through GraphQL subscriptions powered by PostgreSQL LISTEN/NOTIFY.

How realtime subscriptions work

Postrust provides realtime data synchronization through GraphQL subscriptions. Subscribe to changes on any table or view and receive updates instantly.

  • Live updates when data changes
  • Reactive dashboards and UIs
  • Collaborative features
  • No polling required

Architecture

Architecture
┌──────────────┐   WebSocket   ┌──────────────┐   LISTEN/NOTIFY   ┌────────────┐
│   Client     │◀────────────▶│   Postrust   │◀────────────────▶│  PostgreSQL│
│  (Browser)   │              │   Server     │                   │  Database  │
└──────────────┘              └──────────────┘                   └────────────┘

Connecting

Connect via WebSocket to the GraphQL subscription endpoint using the graphql-transport-ws protocol:

ws://localhost:3000/v1/graphql/ws

Each subscription field mirrors the query root and is a live query: the answer now, and the answer again whenever it changes. Hasura’s cursor-based _stream subscriptions are not implemented — see Hasura conformance.

Basic Subscription

GraphQL
# Every row of a table, again whenever it changes
subscription {
  users {
    id
    name
    updated_at
  }
}

# The same arguments a query takes
subscription {
  orders(
    where: { status: { _eq: "pending" } }
    order_by: [{ created_at: desc }]
    limit: 20
  ) {
    id
    total
    status
    customer {
      name
    }
  }
}

Subscribe to Views

Subscribe to PostgreSQL views for computed or aggregated data. A view has no trigger of its own, so a subscription to one is re-read every PGRST_SUBSCRIPTION_REFRESH seconds (30 by default) rather than at once:

GraphQL
subscription {
  sales_dashboard {
    total_revenue
    order_count
    average_order_value
  }
}

JavaScript Client

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

const client = createClient({
  url: 'ws://localhost:3000/v1/graphql/ws',
  connectionParams: {
    authorization: `Bearer ${token}`,
  },
});

// Subscribe to user changes
const unsubscribe = client.subscribe(
  {
    query: `
      subscription {
        users {
          id
          name
          email
        }
      }
    `,
  },
  {
    next: (data) => console.log('Update:', data),
    error: (err) => console.error('Error:', err),
    complete: () => console.log('Done'),
  }
);

// Later: unsubscribe()

React with Apollo

React
import { useSubscription, gql } from '@apollo/client';

const ORDERS_SUBSCRIPTION = gql`
  subscription OnOrderUpdate {
    orders(where: { status: { _eq: "pending" } }) {
      id
      total
      status
      customer { name }
    }
  }
`;

function PendingOrders() {
  const { data, loading, error } = useSubscription(ORDERS_SUBSCRIPTION);

  if (loading) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.orders.map(order => (
        <li key={order.id}>
          Order #{order.id} - ${order.total}
        </li>
      ))}
    </ul>
  );
}

PostgreSQL Triggers

A subscription is woken at once by a notification on its table's channel, postrust_<schema>_<table>; without one it waits for the next refresh. This trigger sends it for public.orders, and postrust_graphql::subscription::create_notify_trigger_sql generates the same for any table:

SQL
-- The server listens on postrust_<schema>_<table>
CREATE OR REPLACE FUNCTION public.postrust_notify_public_orders_fn()
RETURNS TRIGGER AS $$
DECLARE
    payload jsonb;
BEGIN
    IF TG_OP = 'DELETE' THEN
        payload := jsonb_build_object(
            'operation', 'DELETE',
            'table', TG_TABLE_NAME,
            'schema', TG_TABLE_SCHEMA,
            'old', row_to_json(OLD)
        );
    ELSIF TG_OP = 'UPDATE' THEN
        payload := jsonb_build_object(
            'operation', 'UPDATE',
            'table', TG_TABLE_NAME,
            'schema', TG_TABLE_SCHEMA,
            'old', row_to_json(OLD),
            'new', row_to_json(NEW)
        );
    ELSE
        payload := jsonb_build_object(
            'operation', 'INSERT',
            'table', TG_TABLE_NAME,
            'schema', TG_TABLE_SCHEMA,
            'new', row_to_json(NEW)
        );
    END IF;

    PERFORM pg_notify('postrust_public_orders', payload::text);
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS postrust_notify_public_orders ON public.orders;
CREATE TRIGGER postrust_notify_public_orders
    AFTER INSERT OR UPDATE OR DELETE ON public.orders
    FOR EACH ROW
    EXECUTE FUNCTION public.postrust_notify_public_orders_fn();

Use Cases

Live Dashboards

Real-time metrics, KPIs, and analytics updates

Chat Applications

Instant message delivery and presence

Collaborative Editing

Multi-user document editing and cursors

Notifications

Push alerts and activity feeds

Best Practices

  • Use filters - Subscribe to specific records, not entire tables
  • Select only needed fields - Minimize payload size
  • Handle reconnection - Implement retry logic for network issues
  • Clean up subscriptions - Unsubscribe when components unmount