Skip to main content

Realtime Hooks

Hook into WebSocket lifecycle events to control connections, subscriptions, and messages.

Overview

Realtime hooks are defined in edgebase.config.ts under realtime.handlers.hooks. They run inside the Realtime Durable Object.

HookTimingBehaviorCan ModifyCan Reject
onConnectAfter initial JWT authNon-blocking (waitUntil)NoNo
onSubscribeAfter channel access rules passBlockingNoYes (return false / throw)
onMessageBefore broadcast to other clientsBlockingYes (return payload)Yes (throw)

Channel access rules always run before onSubscribe. If a rule rejects the subscription, the hook does not execute.

TypeScript Types

interface AuthContext {
id: string;
role?: string;
isAnonymous?: boolean;
email?: string;
custom?: Record<string, unknown>;
}

interface RealtimeHookCtx {
waitUntil(promise: Promise<unknown>): void;
}

interface RealtimeHooks {
onConnect?: (auth: AuthContext, connectionId: string, ctx: RealtimeHookCtx) =>
Promise<void> | void;
onSubscribe?: (auth: AuthContext, channel: string, ctx: RealtimeHookCtx) =>
Promise<boolean | void> | boolean | void;
onMessage?: (
auth: AuthContext,
channel: string,
message: Record<string, unknown>,
ctx: RealtimeHookCtx,
) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
}

Configuration

// edgebase.config.ts
import { defineConfig } from '@edgebase/shared';

export default defineConfig({
realtime: {
authTimeoutMs: 5000,
handlers: {
hooks: {
onConnect: async (auth, connectionId, ctx) => { /* ... */ },
onSubscribe: async (auth, channel, ctx) => { /* ... */ },
onMessage: async (auth, channel, message, ctx) => { /* ... */ },
},
},
namespaces: {
public: { access: { subscribe: () => true } },
},
},
});

onConnect

Fires once when a client first authenticates on the WebSocket connection. Does not fire on re-authentication (token refresh over an existing connection). Non-blocking via ctx.waitUntil().

onConnect: async (auth, connectionId, ctx) => {
// Log connection for analytics
console.log(`User ${auth.id} connected (${connectionId})`);

// Track active connections via external service
ctx.waitUntil(
fetch('https://analytics.example.com/ws-connect', {
method: 'POST',
body: JSON.stringify({
userId: auth.id,
connectionId,
connectedAt: Date.now(),
}),
}).catch(() => {}),
);
},
ParameterTypeDescription
authAuthContextThe authenticated user (always present — unauthenticated connections don't reach this hook)
connectionIdstringUnique identifier for this WebSocket connection
ctxRealtimeHookCtxHook context with waitUntil()

This hook cannot reject connections. To control who can connect, use JWT validation and auth configuration.

onSubscribe

Fires after channel access rules have passed. Use this for additional business logic beyond static rules — e.g., checking payment status, rate limiting subscriptions, or dynamic channel authorization.

onSubscribe: async (auth, channel, ctx) => {
// Premium channels require paid subscription
if (channel.startsWith('premium:') && auth.custom?.plan !== 'pro') {
return false; // Reject subscription
}

// Rate limit: max 10 channel subscriptions per user
// (implement with external counter or KV)
},
ParameterTypeDescription
authAuthContextThe authenticated user
channelstringThe channel name being subscribed to
ctxRealtimeHookCtxHook context

Return value:

ReturnBehavior
void / undefined / trueAllow the subscription
falseReject the subscription silently
ThrowReject the subscription with an error message sent to the client
Error Behavior

When onSubscribe throws, the client receives a WebSocket error frame with the error message. When it returns false, the subscription is silently rejected.

onMessage

Fires when a client sends a broadcast message, before it is forwarded to other subscribers. Can transform the message payload, reject the message, or pass it through unchanged.

onMessage: async (auth, channel, message, ctx) => {
// Filter profanity from chat messages
if (message.text && typeof message.text === 'string') {
return { ...message, text: filterProfanity(message.text) };
}

// Block messages with forbidden content
if (message.type === 'spam') {
throw new Error('Message rejected: spam detected');
}

// Return nothing → send original message unchanged
},
ParameterTypeDescription
authAuthContextThe user sending the message
channelstringThe broadcast event name
messageRecord<string, unknown>The message payload
ctxRealtimeHookCtxHook context

Return value:

ReturnBehavior
Record<string, unknown>Replace the message payload with the returned object
void / undefinedSend the original message unchanged
ThrowBlock the message — it is not broadcast to other clients

Hook Context

RealtimeHookCtx provides:

PropertyTypeDescription
waitUntil(promise)(p: Promise<unknown>) => voidKeep the DO alive for background work
Blocking vs Non-blocking
  • onConnect is non-blocking — runs via ctx.waitUntil(), does not delay the connection.
  • onSubscribe and onMessage are blocking — the operation waits for the hook to complete before proceeding. Keep hook logic fast to avoid delaying real-time communication.