Skip to main content
Version: Next (2.x)

Subscriptions

Baeta provides type-safe real-time functionality through GraphQL subscriptions.

PubSub Setup

You can use any PubSub implementation. Here's an example using GraphQL Yoga's built-in PubSub:

// src/lib/pubsub.ts
import { createPubSub } from "graphql-yoga";
import type { User } from "../__generated__/types.ts";

export type PubSubMap = {
"user-created": [string];
"user-updated": [User];
};

export const pubsub = createPubSub<PubSubMap>();

Alternatively, you can use Baeta's Typed PubSub wrapper through @baeta/subscriptions-pubsub for graphql-subscriptions:

import { createTypedPubSub } from "@baeta/subscriptions-pubsub";
import { PubSub } from "graphql-subscriptions";

export const pubsub = createTypedPubSub<PubSub, PubSubMap>(new PubSub());
warning

graphql-subscriptions is for a single server instance. If you need to scale your application, consider using a more robust solution like graphql-redis-subscriptions.

Context Integration

Add PubSub to your context:

// src/types/context.ts
import type { PubSub } from "graphql-yoga";
import type { PubSubMap } from "../lib/pubsub.ts";

export type Context = {
pubsub: PubSub<PubSubMap>;
};

Schema Definition

Define your subscription types:

# src/modules/user/user.gql
type Subscription {
userCreated: User!
userUpdated: User!
}

Implementing Subscriptions

Subscriptions use the .subscribe() and .resolve() chain. The .resolve() step supports the same chaining methods as regular resolvers (.map, .key, .to, .withDefault, etc.):

import { db } from "../../lib/db/prisma.ts";
import { UserModule } from "./typedef.ts";

const { Subscription } = UserModule;

// Subscription with database lookup in resolve
const userCreatedSubscription = Subscription.userCreated
.subscribe(({ ctx }) => {
return ctx.pubsub.subscribe("user-created");
})
.resolve(({ source }) => {
return db.user.findFirstOrThrow({
where: { id: source },
});
});

// Direct subscription — source is passed through
const userUpdatedSubscription = Subscription.userUpdated
.subscribe(({ ctx }) => {
return ctx.pubsub.subscribe("user-updated");
})
.resolve(({ source }) => {
return source;
});

export default Subscription.$fields({
userCreated: userCreatedSubscription,
userUpdated: userUpdatedSubscription,
});

Publishing Events

Publish events from your mutations or other resolvers:

// Publishing after user update
const updateUserMutation = Mutation.updateUser
.$use(async (next, { ctx }) => {
const user = await next();
if (user) {
ctx.pubsub.publish("user-updated", user);
}
return user;
})
.resolve(async ({ args }) => {
return db.user.update({
where: { id: args.where.id },
data: args.data,
});
});

// Publishing after user creation
const createUserMutation = Mutation.createUser.resolve(
async ({ args, ctx }) => {
const user = await db.user.create({
data: args.data,
});
ctx.pubsub.publish("user-created", user.id);
return user;
},
);

Subscription Middlewares

You can attach middlewares to both phases of a subscription independently:

  • $use calls before .subscribe(...) wrap the subscribe phase — they run once per subscription, around the call that returns the async iterable.
  • $use calls between .subscribe(...) and .resolve(...) wrap the resolve phase — they run once per published event, around the resolver that maps the payload to the response.
const userUpdatedSubscription = Subscription.userUpdated
// Subscribe-phase middleware — runs once when the client connects
.$use(async (next) => {
console.log("Subscribing to user updates");
const sub = await next();
console.log("Subscribed");
return sub;
})
.subscribe(({ ctx }) => {
return ctx.pubsub.subscribe("user-updated");
})
// Resolve-phase middleware — runs once per published event
.$use(async (next) => {
const result = await next();
console.log("Delivering update:", result);
return result;
})
.resolve(({ source }) => {
return source;
});

App-plugin helpers like auth work on both phases — see the authorization guide for an example.

Production Setup

For production environments, consider using a multi-instance solution with graphql-redis-subscriptions and the Typed PubSub wrapper:

// src/lib/pubsub.ts
import { createTypedPubSub } from "@baeta/subscriptions-pubsub";
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";

const options = {
host: REDIS_DOMAIN_NAME,
port: PORT_NUMBER,
};

const redisPubSub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
});

export const pubsub = createTypedPubSub<RedisPubSub, PubSubMap>(redisPubSub);