Skip to main content
Version: Next

GraphQL Subscriptions

Subscriptions notify you when an event occurs on the server side. PostGraphile supports subscriptions out of the box (assuming you are using it with a supported webserver), but you are responsible for adding subscription fields to your schema - without any subscription fields, the subscription operation is not possible.

Websockets

The most common transport for GraphQL subscriptions is via Websockets. To enable or disable websocket support in your API, use the preset.grafserv.websockets option:

graphile.config.mjs
export default {
//...
grafserv: {
websockets: true,
},
};
info

The endpoint for subscriptions is the same as for GraphQL, except the protocol is changed from http or https to ws or wss respectively.

Adding subscription fields

The easiest way to add subscription fields is by using makeExtendSchemaPlugin to extend the Subscription type and add your field. Your subscription fields will typically leverage the Grafast listen() step to subscribe to events on a pubsub-capable entity.

subscribePlan() and plan()

Unlike the rest of your schema where you typically use the plan() plan resolver, for subscriptions we need two plan resolvers: subscribePlan() which will return a streamable step (essentially wrapping an async iterable), and plan() which will take the payload of each of the events from this stream and convert them into something that the subscription result type can use.

pgSubscriber

By default (if your PostgreSQL adaptor supports it), PostGraphile will add a pgSubscriber entry to your GraphQL context. This object is pubsub-capable and uses Postgres' LISTEN/NOTIFY functionality, which is probably the easiest way to get started. You can build a step that represents it in your subscribePlan() resolver via:

const $pgSubscriber = context().get("pgSubscriber");
tip

You can use any pubsub-capable provider with PostGraphile or Grafast: redis, MQTT, EventEmitter, etc; all you need is an abstraction that conforms to the expected interface. See the listen() documentation for specifics.

Subscription topic

You will also need a topic to listen on. PostgreSQL topics are limited to 63 characters. This topic can be anything you like within PostgreSQL's constraints, but it's typically useful to include the primary key of the entity you're subscribing to in it. In the example below we'll use forum:FORUM_ID:message as the topic for when a new message is posted to a forum, substituting FORUM_ID for the id of the forum that we're interested in (whether this is an int or a UUID it shouldn't go over the 63 character limit).

Example

The following is an example pulling it all together. In this example when a new message is created an event will be sent to forum:FORUM_ID:message containing the payload {"event": "create", "sub": FORUM_ID, "id": MESSAGE_ID}. This event will be picked up by the pgSubscriber, which listen() subscribes to. This event will then be parsed by listen() using the jsonParse method, and the resulting object will be passed to the Subscription.forumMessage.plan() plan resolver, which does no further processing. The data then flows down to the ForumMessageSubscriptionPayload field steps, which will extract the details that they care about and use them to provide the relevant data to the user.

import { makeExtendSchemaPlugin } from "postgraphile/utils";
import { context, lambda, listen } from "postgraphile/grafast";
import { jsonParse } from "postgraphile/@dataplan/json";

const MySubscriptionPlugin = makeExtendSchemaPlugin((build) => {
const { messages } = build.input.pgRegistry.pgResources;
return {
typeDefs: /* GraphQL */ `
extend type Subscription {
forumMessage(forumId: Int!): ForumMessageSubscriptionPayload
}

type ForumMessageSubscriptionPayload {
event: String
message: Message
}
`,
plans: {
Subscription: {
forumMessage: {
subscribePlan(_$root, args) {
const $pgSubscriber = context().get("pgSubscriber");
const $forumId = args.get("forumId");
const $topic = lambda($forumId, (id) => `forum:${id}:message`);
return listen($pgSubscriber, $topic, jsonParse);
},
plan($event) {
return $event;
},
},
},
ForumMessageSubscriptionPayload: {
event($event) {
return $event.get("event");
},
message($event) {
const $id = $event.get("id");
return messages.get({ id: $id });
},
},
},
};
});

Triggering subscriptions manually

You can use the NOTIFY keyword or pg_notify function in PostgreSQL to trigger an event. For example, you might simulate an event for creating message 27 within forum 1 with:

NOTIFY "forum:1:message", '{"event": "create", "sub": 1, "id": 27}';

Triggering subscriptions automatically

I'm using a fairly complex PostgreSQL function so that I can just use `CREATE TRIGGER` to trigger events in future without having to define a function for each trigger. Click this paragraph to expand and see the function.

IMPORTANT: this trigger assumes that the primary key for your tables is always id. If this is not the case, you should delete the line containing 'id', v_record.id.

CREATE FUNCTION tg__graphql_subscription() RETURNS trigger
LANGUAGE plpgsql
AS $_$
declare
v_process_new bool = (TG_OP = 'INSERT' OR TG_OP = 'UPDATE');
v_process_old bool = (TG_OP = 'UPDATE' OR TG_OP = 'DELETE');
v_event text = TG_ARGV[0];
v_topic_template text = TG_ARGV[1];
v_attribute text = TG_ARGV[2];
v_record record;
v_sub text;
v_topic text;
v_i int = 0;
v_last_topic text;
begin
for v_i in 0..1 loop
if (v_i = 0) and v_process_new is true then
v_record = new;
elsif (v_i = 1) and v_process_old is true then
v_record = old;
else
continue;
end if;
if v_attribute is not null then
execute 'select $1.' || quote_ident(v_attribute)
using v_record
into v_sub;
end if;
if v_sub is not null then
v_topic = replace(v_topic_template, '$1', v_sub);
else
v_topic = v_topic_template;
end if;
if v_topic is distinct from v_last_topic then
-- This if statement prevents us from triggering the same notification twice
v_last_topic = v_topic;
perform pg_notify(v_topic, json_build_object(
'event', v_event,
'subject', v_sub,
'id', v_record.id
)::text);
end if;
end loop;
return v_record;
end;
$_$;

Hooking the database up to a GraphQL subscription can be achieved via CREATE TRIGGER:

CREATE TRIGGER _500_gql_insert
AFTER INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION tg__graphql_subscription(
'create', -- the "event" string, useful for the client to know what happened
'forum:$1:message', -- the "topic" the event will be published to, as a template
'forum_id' -- If specified, `$1` above will be replaced with NEW.forum_id or OLD.forum_id from the trigger.
);

Testing your subscription with Ruru

To test your subscription you will need to first subscribe and then trigger it.

To subscribe, in one Ruru tab execute

subscription MySubscription {
forumMessage(forumId: 1) {
user
event
}
}

You should get the answer: "Waiting for subscription to yield data…"

To trigger the subscription, in another Ruru tab run a mutation that adds a message to that forum. This will depend on your implementation, for example:

mutation MyMutation {
createMessage(input: { message: { forumId: 1, body: "Hello World!" } }) {
clientMutationId
}
}

In this tab you will get the regular mutation answer. Going back to the previous tab, you will see the subscription payload. You are good to go! This should serve as the basis to implement your own custom subscriptions.