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());
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:
$usecalls before.subscribe(...)wrap the subscribe phase — they run once per subscription, around the call that returns the async iterable.$usecalls 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);