Server
The server module creates a Socket.IO server with a full GraphQL execution pipeline, including live query support and schema transformation.
createServer
Section titled “createServer”import { createServer } from '@requence/socketql/server'
const { server, namespace, liveQueryStore, attach, addSchema } = createServer({ path: '/ws/', transports: ['websocket'],})For multi-instance deployments with Redis, see the Redis adapter section below.
Adding Schemas
Section titled “Adding Schemas”There are two ways to register GraphQL schemas.
Via schemas option
Section titled “Via schemas option”Pass schemas directly when creating the server using the schemas option. Use defineSchema for type-safe schema definitions:
import { createServer, defineSchema } from '@requence/socketql/server'
const helloSchema = defineSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String! @live } `, resolvers: { Query: { hello: () => 'world', }, },})
const { attach } = createServer({ path: '/ws/', schemas: [helloSchema],})Via addSchema
Section titled “Via addSchema”Alternatively, use addSchema to register schemas dynamically after server creation:
const { addSchema, attach } = createServer({ path: '/ws/' })
addSchema({ typeDefs: /* GraphQL */ ` type Query { hello: String! @live } `, resolvers: { Query: { hello: () => 'world', }, },})Attaching to a Server
Section titled “Attaching to a Server”Node.js
Section titled “Node.js”import { createServer as createHTTPServer } from 'node:http'
const httpServer = createHTTPServer()attach(httpServer)httpServer.listen(4000)Import from @requence/socketql/server/bun instead — it replaces attach with a handler() method:
import { createServer, defineSchema } from '@requence/socketql/server/bun'
const mySchema = defineSchema({ typeDefs, resolvers })
const { handler } = createServer({ path: '/ws/', schemas: [mySchema],})
export default { port: 4000, ...handler() }HTTP Handler
Section titled “HTTP Handler”By default, SocketQL serves GraphQL exclusively over WebSocket. Use httpHandler() to expose a standard HTTP POST /graphql endpoint for queries and mutations.
Basic usage
Section titled “Basic usage”const { httpHandler } = createServer({ path: '/ws/', schemas: [mySchema] })
app.post('/graphql', httpHandler())The handler is compatible with any framework that uses a Hono-style context object (c.req.json(), c.req.raw).
Context extension
Section titled “Context extension”Extend the GraphQL context per HTTP request using extendContext. This receives the raw Request object, so you can extract auth tokens, cookies, or other headers:
app.post('/graphql', httpHandler({ extendContext: async (req) => { const token = req.headers.get('authorization')?.replace('Bearer ', '') const session = await verifySession(token) return { userId: session.userId } },}))Schema filtering with mapSchema
Section titled “Schema filtering with mapSchema”By default, the HTTP endpoint uses the same schema as the WebSocket server. Use mapSchema to transform or filter the schema for HTTP — for example, to expose only a subset of queries and mutations.
app.post('/graphql', httpHandler({ mapSchema: (schema) => { // Return a transformed schema — only @http-marked fields will be accessible return myFilteredSchema(schema) },}))The mapSchema callback receives the full compiled GraphQLSchema and must return a GraphQLSchema. The result is cached after the first request.
Example: @http directive
Section titled “Example: @http directive”A common pattern is to mark specific fields with a custom @http directive and filter the schema to only expose those over HTTP.
Define the directive in your schema:
directive @http on FIELD_DEFINITION
type Query { publicStatus: Status! @http users: [User!]! @live dashboard: Dashboard! @live}
type Mutation { webhookCallback(input: WebhookInput!): Boolean! @http updateUser(input: UpdateUserInput!): User!}Then filter the schema using mapSchema with @graphql-tools/utils:
import { MapperKind, getDirective, mapSchema } from '@graphql-tools/utils'
app.post('/graphql', httpHandler({ mapSchema: (schema) => mapSchema(schema, { [MapperKind.QUERY_ROOT_FIELD]: (fieldConfig) => { return getDirective(schema, fieldConfig, 'http')?.length ? fieldConfig : null }, [MapperKind.MUTATION_ROOT_FIELD]: (fieldConfig) => { return getDirective(schema, fieldConfig, 'http')?.length ? fieldConfig : null }, }),}))In this setup, only publicStatus and webhookCallback are accessible over HTTP. All other fields are stripped from the HTTP schema and won’t appear in introspection.
Context
Section titled “Context”The GraphQL context is a discriminated union based on the transport field. This means resolvers can narrow the context type to access transport-specific properties.
WebSocket context
Section titled “WebSocket context”When a query arrives over WebSocket, the context includes the Socket.IO socket and namespace:
createServer({ extendContext: ({ socket }) => ({ userId: socket.handshake.auth.userId, }),})HTTP context
Section titled “HTTP context”When a query arrives over HTTP (via httpHandler), the context does not include socket or namespace. Use the extendContext option on httpHandler to add per-request context:
app.post('/graphql', httpHandler({ extendContext: async (req) => ({ userId: await getUserFromHeaders(req), }),}))Transport narrowing
Section titled “Transport narrowing”Use context.transport to differentiate between transports in your resolvers:
const resolvers = { Query: { me: (_, __, context) => { if (context.transport === 'websocket') { // context.socket and context.namespace are available console.log('WebSocket client:', context.socket.id) }
if (context.transport === 'http') { // context.socket and context.namespace do not exist }
return getUser(context.userId) }, },}The exported types WebSocketGraphQLContext and HttpGraphQLContext can be used directly for type-safe resolver signatures:
import type { GraphQLContext, WebSocketGraphQLContext, HttpGraphQLContext,} from '@requence/socketql/server'Lifecycle Hooks
Section titled “Lifecycle Hooks”onConnect
Section titled “onConnect”Called when a client connects, before any GraphQL operations are processed. Use this to authenticate connections.
To reject a connection, throw a ConnectionRejectedError. The client will receive a connect_error event with the error message and no server-side log is produced:
import { ConnectionRejectedError, createServer } from '@requence/socketql/server'
createServer({ onConnect: async (socket) => { const token = socket.handshake.auth?.token
if (!token || !(await isValid(token))) { throw new ConnectionRejectedError('Unauthorized') }
socket.data.userId = getUserId(token) },})ConnectionRejectedError accepts an optional data object as the second argument. This is sent to the client alongside the error message, allowing you to pass structured context:
throw new ConnectionRejectedError('Subscription expired', { code: 'SUBSCRIPTION_EXPIRED', retryable: false,})On the client, the error is received as a ConnectionError with a data property:
client.onConnectError((err) => { err.message // "Subscription expired" err.data // { code: "SUBSCRIPTION_EXPIRED", retryable: false }})onDisconnect
Section titled “onDisconnect”createServer({ onDisconnect: (socket, reason) => { console.log('disconnected:', socket.id, reason) },})Redis Adapter
Section titled “Redis Adapter”For multi-instance deployments, use createRedisAdapter from @requence/socketql/server/redis. This provides both Socket.IO adapter coordination and Redis-backed live query invalidation across instances.
Install the required peer dependencies:
npm add ioredis @socket.io/redis-adapterimport { createServer } from '@requence/socketql/server'import { createRedisAdapter } from '@requence/socketql/server/redis'
const redis = createRedisAdapter({ url: 'redis://localhost:6379' })
const { attach } = createServer({ path: '/ws/', redis, schemas: [mySchema],})