Last active
August 17, 2025 07:15
-
-
Save ashbuilds/490d504d703a5ddfcdf504f6c21e220d to your computer and use it in GitHub Desktop.
Implementing GraphQL Subscriptions with Websockets in a Bun Server using graphql-yoga
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Bun from 'bun' | |
import { createYoga, YogaInitialContext, YogaServerInstance } from 'graphql-yoga' | |
import { makeHandler } from "graphql-ws/lib/use/bun"; | |
import { ExecutionArgs } from "@envelop/types"; | |
import { schema } from './graphql/schema'; | |
interface IUserContext { | |
token?: string; | |
} | |
const PORT = process.env.PORT || 4000; | |
const yoga: YogaServerInstance<{}, YogaInitialContext> = createYoga({ | |
graphqlEndpoint: '/graphql', | |
schema, | |
graphiql: { | |
subscriptionsProtocol: 'WS', | |
}, | |
context: async ({ request }): Promise<IUserContext> => { | |
return { | |
token: request.headers.get("token") ?? "" | |
} | |
} | |
}) | |
const websocketHandler = makeHandler({ | |
schema, | |
execute: (args: ExecutionArgs) => args.rootValue.execute(args), | |
subscribe: (args: ExecutionArgs) => args.rootValue.subscribe(args), | |
onSubscribe: async (ctx, msg) => { | |
const {schema, execute, subscribe, contextFactory, parse, validate} = yoga.getEnveloped({ | |
...ctx, | |
req: ctx.extra.request, | |
socket: ctx.extra.socket, | |
params: msg.payload | |
}) | |
const args = { | |
schema, | |
operationName: msg.payload.operationName, | |
document: parse(msg.payload.query), | |
variableValues: msg.payload.variables, | |
contextValue: await contextFactory(), | |
rootValue: { | |
execute, | |
subscribe | |
} | |
} | |
const errors = validate(args.schema, args.document) | |
if (errors.length) return errors | |
return args | |
}, | |
}) | |
const server: Bun.Server = Bun.serve({ | |
fetch: (request: Request, server: Bun.Server): Promise<Response> | Response => { | |
// Upgrade the request to a WebSocket | |
if (server.upgrade(request)) { | |
return new Response() | |
} | |
return yoga.fetch(request, server) | |
}, | |
port: PORT, | |
websocket: websocketHandler, | |
}) | |
console.info( | |
`🚀 Server is running on ${new URL( | |
yoga.graphqlEndpoint, | |
`http://${server.hostname}:${server.port}` | |
)}` | |
) |
For those who faced issues with accessing the passed headers for custom context providing, like authorisation header, here's a useful note on the Authorization token example:
export function getContextJWTToken(context: AppQraphQLContext): string | null {
if (context.params.extensions?.headers?.Authorization) {
return getJWTFromStr(context.params.extensions.headers.Authorization);
}
if (context.request) {
const request = context.request;
const authHeader = request.headers.get('Authorization');
if (authHeader) return getJWTFromStr(authHeader);
}
return null;
}
The typical context.request
headers are not present in WS connection but appear in context.params.extensions?.headers
Full example in context of global user
context pass:
type AppQraphQLContext = YogaInitialContext & {
user: User | null;
};
function getContextJWTToken(context: AppQraphQLContext): string | null {
if (context.params.extensions?.headers?.Authorization) {
return getJWTFromStr(context.params.extensions.headers.Authorization);
}
if (context.request) {
const request = context.request;
const authHeader = request.headers.get('Authorization');
if (authHeader) return getJWTFromStr(authHeader);
}
return null;
}
const getContextUser = async (context: AppQraphQLContext) => {
let token = getContextJWTToken(context);
try {
const decoded: any = jwt.verify(token || '', process.env.JWT_SECRET);
const user = await findUser({ id: decoded.userId });
return user;
}
catch (err) {
return null;
}
};
const yoga = createYoga<AppQraphQLContext>({
schema,
graphiql: {
subscriptionsProtocol: 'WS',
},
context: async (context: AppQraphQLContext) => {
return {
...context,
user: await getContextUser(context)
}
},
landingPage: false,
graphqlEndpoint: '/',
});
if anyone happens to find this, here is the setup for graphql-yoga v5, with bun
import { makeHandler } from 'graphql-ws/use/bun';
import { createYoga } from 'graphql-yoga';
const yogaApp = createYoga({ ... config here... })
const bunServer = Bun.serve({
port: PORT,
hostname: '0.0.0.0',
websocket: makeHandler<{ token: string }, { user: UserSession.DefaultTokenPayload }>({
// extra is available here so we don't have to revalidate the JWT
context: setWebhookContext,
// return false or throw to prevent connection
async onConnect({ connectionParams, extra }) {
const token = connectionParams?.token;
if (typeof token !== 'string') return false;
try {
extra.user = authenticateJWT(token);
return true;
} catch {
return false;
}
},
schema: executableSchema,
}),
async fetch(request, server) {
const url = new URL(request.url);
switch (url.pathname) {
case '/health': {
return new Response('OK', { status: 200 });
}
case '/subscriptions': {
if (server.upgrade(request)) {
return new Response('OK', { status: 200 });
}
return new Response('Unable to upgrade to websockets', { status: 400 });
}
case yogaApp.graphqlEndpoint: {
return yogaApp.fetch(request, server);
}
... etc
}
},
});
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this line cannot set request to context (line 34)
req: ctx.extra.request,
it is undefined so cannot access headers