Edge Functions

Handling WebSockets

How to handle WebSocket connections in Edge Functions


Edge Functions supports hosting WebSocket servers that can facilitate bi-directional communications with browser clients.

You can also establish outgoing WebSocket client connections to another server from Edge Functions (e.g., OpenAI Realtime API). You can find an example OpenAI Realtime Relay Server implementation on the supabase-community GitHub account.

Writing a WebSocket server

Here are some basic examples of setting up WebSocket servers using Deno and Node.js APIs.


_21
Deno.serve(req => {
_21
const upgrade = req.headers.get("upgrade") || "";
_21
_21
if (upgrade.toLowerCase() != "websocket") {
_21
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
_21
}
_21
_21
const { socket, response } = Deno.upgradeWebSocket(req);
_21
_21
socket.onopen = () => console.log("socket opened");
_21
socket.onmessage = (e) => {
_21
console.log("socket message:", e.data);
_21
socket.send(new Date().toString());
_21
};
_21
_21
socket.onerror = e => console.log("socket errored:", e.message);
_21
socket.onclose = () => console.log("socket closed");
_21
_21
return response;
_21
_21
});

Outbound WebSockets

You can also establish an outbound WebSocket connection to another server from an Edge Function.

Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a WebSocket proxy, for example as a relay server for the OpenAI Realtime API.

supabase/functions/relay/index.ts

_74
import { createServer } from "node:http";
_74
import { WebSocketServer } from "npm:ws";
_74
import { RealtimeClient } from "https://raw.githubusercontent.com/openai/openai-realtime-api-beta/refs/heads/main/lib/client.js";
_74
_74
// ...
_74
_74
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
_74
_74
const server = createServer();
_74
// Since we manually created the HTTP server,
_74
// turn on the noServer mode.
_74
const wss = new WebSocketServer({ noServer: true });
_74
_74
wss.on("connection", async (ws) => {
_74
console.log("socket opened");
_74
if (!OPENAI_API_KEY) {
_74
throw new Error("OPENAI_API_KEY is not set");
_74
}
_74
// Instantiate new client
_74
console.log(`Connecting with key "${OPENAI_API_KEY.slice(0, 3)}..."`);
_74
const client = new RealtimeClient({ apiKey: OPENAI_API_KEY });
_74
_74
// Relay: OpenAI Realtime API Event -> Browser Event
_74
client.realtime.on("server.*", (event) => {
_74
console.log(`Relaying "${event.type}" to Client`);
_74
ws.send(JSON.stringify(event));
_74
});
_74
client.realtime.on("close", () => ws.close());
_74
_74
// Relay: Browser Event -> OpenAI Realtime API Event
_74
// We need to queue data waiting for the OpenAI connection
_74
const messageQueue = [];
_74
const messageHandler = (data) => {
_74
try {
_74
const event = JSON.parse(data);
_74
console.log(`Relaying "${event.type}" to OpenAI`);
_74
client.realtime.send(event.type, event);
_74
} catch (e) {
_74
console.error(e.message);
_74
console.log(`Error parsing event from client: ${data}`);
_74
}
_74
};
_74
_74
ws.on("message", (data) => {
_74
if (!client.isConnected()) {
_74
messageQueue.push(data);
_74
} else {
_74
messageHandler(data);
_74
}
_74
});
_74
ws.on("close", () => client.disconnect());
_74
_74
// Connect to OpenAI Realtime API
_74
try {
_74
console.log(`Connecting to OpenAI...`);
_74
await client.connect();
_74
} catch (e) {
_74
console.log(`Error connecting to OpenAI: ${e.message}`);
_74
ws.close();
_74
return;
_74
}
_74
console.log(`Connected to OpenAI successfully!`);
_74
while (messageQueue.length) {
_74
messageHandler(messageQueue.shift());
_74
}
_74
});
_74
_74
server.on("upgrade", (req, socket, head) => {
_74
wss.handleUpgrade(req, socket, head, (ws) => {
_74
wss.emit("connection", ws, req);
_74
});
_74
});
_74
_74
server.listen(8080);

View source

Authentication

WebSocket browser clients don't have the option to send custom headers. Because of this, Edge Functions won't be able to perform the usual authorization header check to verify the JWT.

You can skip the default authorization header checks by explicitly providing --no-verify-jwt when serving and deploying functions.

To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol.


_44
import { createClient } from "jsr:@supabase/supabase-js@2";
_44
_44
const supabase = createClient(
_44
Deno.env.get("SUPABASE_URL"),
_44
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
_44
);
_44
Deno.serve(req => {
_44
const upgrade = req.headers.get("upgrade") || "";
_44
_44
if (upgrade.toLowerCase() != "websocket") {
_44
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
_44
}
_44
_44
// Please be aware query params may be logged in some logging systems.
_44
const url = new URL(req.url);
_44
const jwt = url.searchParams.get("jwt");
_44
if (!jwt) {
_44
console.error("Auth token not provided");
_44
return new Response("Auth token not provided", { status: 403 });
_44
}
_44
const { error, data } = await supabase.auth.getUser(jwt);
_44
if (error) {
_44
console.error(error);
_44
return new Response("Invalid token provided", { status: 403 });
_44
}
_44
if (!data.user) {
_44
console.error("user is not authenticated");
_44
return new Response("User is not authenticated", { status: 403 });
_44
}
_44
_44
const { socket, response } = Deno.upgradeWebSocket(req);
_44
_44
socket.onopen = () => console.log("socket opened");
_44
socket.onmessage = (e) => {
_44
console.log("socket message:", e.data);
_44
socket.send(new Date().toString());
_44
};
_44
_44
socket.onerror = e => console.log("socket errored:", e.message);
_44
socket.onclose = () => console.log("socket closed");
_44
_44
return response;
_44
_44
});

Limits

The maximum duration is capped based on the wall-clock, CPU, and memory limits. The Function will shutdown when it reaches one of these limits.

Testing WebSockets locally

When testing Edge Functions locally with Supabase CLI, the instances are terminated automatically after a request is completed. This will prevent keeping WebSocket connections open.

To prevent that, you can update the supabase/config.toml with the following settings:


_10
[edge_runtime]
_10
policy = "per_worker"

When running with per_worker policy, Function won't auto-reload on edits. You will need to manually restart it by running supabase functions serve.