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
┌──────────────┐ 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/wsEach 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
# 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:
subscription {
sales_dashboard {
total_revenue
order_count
average_order_value
}
}JavaScript Client
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
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:
-- 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