Skip to content

Commit 1eebae9

Browse files
laktekcharislam
andauthored
Add websocket guide (supabase#30819)
* chore: add WebSockets guide * update the limits * add missing navigation link * Apply suggestions from code review Co-authored-by: Charis <[email protected]> * formatting --------- Co-authored-by: Charis <[email protected]>
1 parent b6ed149 commit 1eebae9

File tree

4 files changed

+325
-6
lines changed

4 files changed

+325
-6
lines changed

apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,10 @@ export const functions: NavMenuConstant = {
12761276
name: 'Ephemeral Storage',
12771277
url: '/guides/functions/ephemeral-storage',
12781278
},
1279+
{
1280+
name: 'WebSockets',
1281+
url: '/guides/functions/websockets',
1282+
},
12791283
{
12801284
name: 'Running AI Models',
12811285
url: '/guides/functions/ai-models',

apps/docs/content/guides/functions/background-tasks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ You can call `EdgeRuntime.waitUntil` in the request handler too. This will not b
4848

4949
```ts
5050
async function fetchAndLog(url: string) {
51-
const response = await fetch('https://httpbin.org/json')
51+
const response = await fetch(url)
5252
console.log(response)
5353
}
5454

apps/docs/content/guides/functions/limits.mdx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ subtitle: "Limits applied Edge Functions in Supabase's hosted platform."
88
## Runtime limits
99

1010
- Maximum Memory: 256MB
11-
- Maximum Duration (Wall clock limit): 400s (this is the duration an Edge Function worker will stay active. During this period, a worker can serve multiple requests)
12-
- Maximum CPU Time: 2s
13-
- Request idle timeout: 150s (if an Edge Function doesn't send a response before the timeout, 504 Gateway Timeout will be returned)
14-
- Maximum Function Size (after bundling via CLI): 10MB
11+
- Maximum Duration (Wall clock limit):
12+
This is the duration an Edge Function worker will stay active. During this period, a worker can serve multiple requests or process background tasks.
13+
- Free plan: 150s
14+
- Paid plans: 400s
15+
- Maximum CPU Time: 2s (Amount of actual time spent on the CPU per request - does not include async I/O.)
16+
- Request idle timeout: 150s (If an Edge Function doesn't send a response before the timeout, 504 Gateway Timeout will be returned)
17+
- Maximum Function Size: 20MB (After bundling using CLI)
1518
- Maximum log message length: 10,000 characters
1619
- Log event threshold: 100 events per 10 seconds
1720

1821
## Other limits & restrictions
1922

2023
- Outgoing connections to ports `25` and `587` are not allowed.
2124
- Serving of HTML content is only supported with [custom domains](/docs/reference/cli/supabase-domains) (Otherwise `GET` requests that return `text/html` will be rewritten to `text/plain`).
22-
- Deno and Node file system APIs are not available.
2325
- Web Worker API (or Node `vm` API) are not available.
2426
- Node Libraries that require multithreading are not supported. Examples: [libvips](https://github.com/libvips/libvips), [sharp](https://github.com/lovell/sharp).
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
---
2+
id: 'function-websockets'
3+
title: 'Handling WebSockets'
4+
description: 'How to handle WebSocket connections in Edge Functions'
5+
subtitle: 'How to handle WebSocket connections in Edge Functions'
6+
---
7+
8+
Edge Functions supports hosting WebSocket servers that can facilitate bi-directional communications with browser clients.
9+
10+
You can also establish outgoing WebSocket client connections to another server from Edge Functions (e.g., [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview)).
11+
12+
### Writing a WebSocket server
13+
14+
Here are some basic examples of setting up WebSocket servers using Deno and Node.js APIs.
15+
16+
<Tabs
17+
scrollable
18+
size="small"
19+
type="underlined"
20+
defaultActiveId="deno"
21+
queryGroup="runtime"
22+
>
23+
<TabPanel id="deno" label="Deno">
24+
```ts
25+
Deno.serve(req => {
26+
const upgrade = req.headers.get("upgrade") || "";
27+
28+
if (upgrade.toLowerCase() != "websocket") {
29+
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
30+
}
31+
32+
const { socket, response } = Deno.upgradeWebSocket(req);
33+
34+
socket.onopen = () => console.log("socket opened");
35+
socket.onmessage = (e) => {
36+
console.log("socket message:", e.data);
37+
socket.send(new Date().toString());
38+
};
39+
40+
socket.onerror = e => console.log("socket errored:", e.message);
41+
socket.onclose = () => console.log("socket closed");
42+
43+
return response;
44+
45+
});
46+
47+
````
48+
</TabPanel>
49+
50+
<TabPanel id="node" label="Node.js">
51+
```ts
52+
import { createServer } from "node:http";
53+
import { WebSocketServer } from "npm:ws";
54+
55+
const server = createServer();
56+
// Since we manually created the HTTP server,
57+
// turn on the noServer mode.
58+
const wss = new WebSocketServer({ noServer: true });
59+
60+
wss.on("connection", ws => {
61+
console.log("socket opened");
62+
ws.on("message", (data /** Buffer */, isBinary /** bool */) => {
63+
if (isBinary) {
64+
console.log("socket message:", data);
65+
} else {
66+
console.log("socket message:", data.toString());
67+
}
68+
69+
ws.send(new Date().toString());
70+
});
71+
72+
ws.on("error", err => {
73+
console.log("socket errored:", err.message);
74+
});
75+
76+
ws.on("close", () => console.log("socket closed"));
77+
});
78+
79+
server.on("upgrade", (req, socket, head) => {
80+
wss.handleUpgrade(req, socket, head, ws => {
81+
wss.emit("connection", ws, req);
82+
});
83+
});
84+
85+
server.listen(8080);
86+
````
87+
88+
</TabPanel>
89+
</Tabs>
90+
91+
### Outbound Websockets
92+
93+
You can also establish an outbound WebSocket connection to another server from an Edge Function.
94+
95+
Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a WebSocket proxy.
96+
97+
Here is an example of proxying messages to OpenAI Realtime API.
98+
99+
We use [Supabase Auth](/docs/guides/functions/auth#fetching-the-user) to authenticate the user who is sending the messages.
100+
101+
```ts
102+
import { createClient } from 'jsr:@supabase/supabase-js@2'
103+
104+
const supabase = createClient(
105+
Deno.env.get('SUPABASE_URL'),
106+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
107+
)
108+
const OPENAI_API_KEY = Deno.env.get('OPENAI_API_KEY')
109+
110+
Deno.serve(async (req) => {
111+
const upgrade = req.headers.get('upgrade') || ''
112+
113+
if (upgrade.toLowerCase() != 'websocket') {
114+
return new Response("request isn't trying to upgrade to websocket.")
115+
}
116+
117+
// WebSocket browser clients does not support sending custom headers.
118+
// We have to use the URL query params to provide user's JWT.
119+
// Please be aware query params may be logged in some logging systems.
120+
const url = new URL(req.url)
121+
const jwt = url.searchParams.get('jwt')
122+
if (!jwt) {
123+
console.error('Auth token not provided')
124+
return new Response('Auth token not provided', { status: 403 })
125+
}
126+
const { error, data } = await supabase.auth.getUser(jwt)
127+
if (error) {
128+
console.error(error)
129+
return new Response('Invalid token provided', { status: 403 })
130+
}
131+
if (!data.user) {
132+
console.error('user is not authenticated')
133+
return new Response('User is not authenticated', { status: 403 })
134+
}
135+
136+
const { socket, response } = Deno.upgradeWebSocket(req)
137+
138+
socket.onopen = () => {
139+
// initiate an outbound WS connection with OpenAI
140+
const url = 'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01'
141+
142+
// openai-insecure-api-key isn't a problem since this code runs in an Edge Function (not client browser)
143+
const openaiWS = new WebSocket(url, [
144+
'realtime',
145+
`openai-insecure-api-key.${OPENAI_API_KEY}`,
146+
'openai-beta.realtime-v1',
147+
])
148+
149+
openaiWS.onopen = () => {
150+
console.log('Connected to OpenAI server.')
151+
152+
socket.onmessage = (e) => {
153+
console.log('socket message:', e.data)
154+
// only send the message if openAI ws is open
155+
if (openaiWS.readyState === 1) {
156+
openaiWS.send(e.data)
157+
} else {
158+
socket.send(
159+
JSON.stringify({
160+
type: 'error',
161+
msg: 'openAI connection not ready',
162+
})
163+
)
164+
}
165+
}
166+
}
167+
168+
openaiWS.onmessage = (e) => {
169+
console.log(e.data)
170+
socket.send(e.data)
171+
}
172+
173+
openaiWS.onerror = (e) => console.log('OpenAI error: ', e.message)
174+
openaiWS.onclose = (e) => console.log('OpenAI session closed')
175+
}
176+
177+
socket.onerror = (e) => console.log('socket errored:', e.message)
178+
socket.onclose = () => console.log('socket closed')
179+
180+
return response // 101 (Switching Protocols)
181+
})
182+
```
183+
184+
### Authentication
185+
186+
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.
187+
188+
You can skip the default authorization header checks by explicitly providing `--no-verify-jwt` when serving and deploying functions.
189+
190+
To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol.
191+
192+
<Tabs
193+
scrollable
194+
size="small"
195+
type="underlined"
196+
defaultActiveId="query"
197+
queryGroup="auth"
198+
>
199+
<TabPanel id="query" label="Using query params">
200+
```ts
201+
import { createClient } from "jsr:@supabase/supabase-js@2";
202+
203+
const supabase = createClient(
204+
Deno.env.get("SUPABASE_URL"),
205+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
206+
);
207+
Deno.serve(req => {
208+
const upgrade = req.headers.get("upgrade") || "";
209+
210+
if (upgrade.toLowerCase() != "websocket") {
211+
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
212+
}
213+
214+
// Please be aware query params may be logged in some logging systems.
215+
const url = new URL(req.url);
216+
const jwt = url.searchParams.get("jwt");
217+
if (!jwt) {
218+
console.error("Auth token not provided");
219+
return new Response("Auth token not provided", { status: 403 });
220+
}
221+
const { error, data } = await supabase.auth.getUser(jwt);
222+
if (error) {
223+
console.error(error);
224+
return new Response("Invalid token provided", { status: 403 });
225+
}
226+
if (!data.user) {
227+
console.error("user is not authenticated");
228+
return new Response("User is not authenticated", { status: 403 });
229+
}
230+
231+
const { socket, response } = Deno.upgradeWebSocket(req);
232+
233+
socket.onopen = () => console.log("socket opened");
234+
socket.onmessage = (e) => {
235+
console.log("socket message:", e.data);
236+
socket.send(new Date().toString());
237+
};
238+
239+
socket.onerror = e => console.log("socket errored:", e.message);
240+
socket.onclose = () => console.log("socket closed");
241+
242+
return response;
243+
244+
});
245+
246+
````
247+
</TabPanel>
248+
<TabPanel id="protocol" label="Using custom protocol">
249+
```ts
250+
import { createClient } from "jsr:@supabase/supabase-js@2";
251+
252+
const supabase = createClient(
253+
Deno.env.get("SUPABASE_URL"),
254+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
255+
);
256+
Deno.serve(req => {
257+
const upgrade = req.headers.get("upgrade") || "";
258+
259+
if (upgrade.toLowerCase() != "websocket") {
260+
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
261+
}
262+
263+
// Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2`
264+
const customProtocols = (req.headers.get("Sec-WebSocket-Protocol") ?? '').split(',').map(p => p.trim())
265+
const jwt = customProtocols.find(p => p.startsWith('jwt')).replace('jwt-', '')
266+
if (!jwt) {
267+
console.error("Auth token not provided");
268+
return new Response("Auth token not provided", { status: 403 });
269+
}
270+
const { error, data } = await supabase.auth.getUser(jwt);
271+
if (error) {
272+
console.error(error);
273+
return new Response("Invalid token provided", { status: 403 });
274+
}
275+
if (!data.user) {
276+
console.error("user is not authenticated");
277+
return new Response("User is not authenticated", { status: 403 });
278+
}
279+
280+
const { socket, response } = Deno.upgradeWebSocket(req);
281+
282+
socket.onopen = () => console.log("socket opened");
283+
socket.onmessage = (e) => {
284+
console.log("socket message:", e.data);
285+
socket.send(new Date().toString());
286+
};
287+
288+
socket.onerror = e => console.log("socket errored:", e.message);
289+
socket.onclose = () => console.log("socket closed");
290+
291+
return response;
292+
});
293+
````
294+
295+
</TabPanel>
296+
</Tabs>
297+
298+
### Limits
299+
300+
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](/docs/guides/functions/limits).
301+
302+
### Testing WebSockets locally
303+
304+
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.
305+
306+
To prevent that, you can update the `supabase/config.toml` with the following settings:
307+
308+
```toml
309+
[edge_runtime]
310+
policy = "per_worker"
311+
```
312+
313+
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`.

0 commit comments

Comments
 (0)