Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,10 @@ export const functions: NavMenuConstant = {
name: 'Ephemeral Storage',
url: '/guides/functions/ephemeral-storage',
},
{
name: 'WebSockets',
url: '/guides/functions/websockets',
},
{
name: 'Running AI Models',
url: '/guides/functions/ai-models',
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/functions/background-tasks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ You can call `EdgeRuntime.waitUntil` in the request handler too. This will not b

```ts
async function fetchAndLog(url: string) {
const response = await fetch('https://httpbin.org/json')
const response = await fetch(url)
console.log(response)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ const code = {

<Admonition type="info">

You can find a selection of React Email templates in the [React Email Eamples](https://react.email/examples).
You can find a selection of React Email templates in the [React Email Examples](https://react.email/examples).

</Admonition>

Expand Down
12 changes: 7 additions & 5 deletions apps/docs/content/guides/functions/limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ subtitle: "Limits applied Edge Functions in Supabase's hosted platform."
## Runtime limits

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

## Other limits & restrictions

- Outgoing connections to ports `25` and `587` are not allowed.
- 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`).
- Deno and Node file system APIs are not available.
- Web Worker API (or Node `vm` API) are not available.
- Node Libraries that require multithreading are not supported. Examples: [libvips](https://github.com/libvips/libvips), [sharp](https://github.com/lovell/sharp).
313 changes: 313 additions & 0 deletions apps/docs/content/guides/functions/websockets.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
---
id: 'function-websockets'
title: 'Handling WebSockets'
description: 'How to handle WebSocket connections in Edge Functions'
subtitle: '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](https://platform.openai.com/docs/guides/realtime/overview)).

### Writing a WebSocket server

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

<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="deno"
queryGroup="runtime"
>
<TabPanel id="deno" label="Deno">
```ts
Deno.serve(req => {
const upgrade = req.headers.get("upgrade") || "";

if (upgrade.toLowerCase() != "websocket") {
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
}

const { socket, response } = Deno.upgradeWebSocket(req);

socket.onopen = () => console.log("socket opened");
socket.onmessage = (e) => {
console.log("socket message:", e.data);
socket.send(new Date().toString());
};

socket.onerror = e => console.log("socket errored:", e.message);
socket.onclose = () => console.log("socket closed");

return response;

});

````
</TabPanel>

<TabPanel id="node" label="Node.js">
```ts
import { createServer } from "node:http";
import { WebSocketServer } from "npm:ws";

const server = createServer();
// Since we manually created the HTTP server,
// turn on the noServer mode.
const wss = new WebSocketServer({ noServer: true });

wss.on("connection", ws => {
console.log("socket opened");
ws.on("message", (data /** Buffer */, isBinary /** bool */) => {
if (isBinary) {
console.log("socket message:", data);
} else {
console.log("socket message:", data.toString());
}

ws.send(new Date().toString());
});

ws.on("error", err => {
console.log("socket errored:", err.message);
});

ws.on("close", () => console.log("socket closed"));
});

server.on("upgrade", (req, socket, head) => {
wss.handleUpgrade(req, socket, head, ws => {
wss.emit("connection", ws, req);
});
});

server.listen(8080);
````

</TabPanel>
</Tabs>

### 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.

Here is an example of proxying messages to OpenAI Realtime API.

We use [Supabase Auth](/docs/guides/functions/auth#fetching-the-user) to authenticate the user who is sending the messages.

```ts
import { createClient } from 'jsr:@supabase/supabase-js@2'

const supabase = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
)
const OPENAI_API_KEY = Deno.env.get('OPENAI_API_KEY')

Deno.serve(async (req) => {
const upgrade = req.headers.get('upgrade') || ''

if (upgrade.toLowerCase() != 'websocket') {
return new Response("request isn't trying to upgrade to websocket.")
}

// WebSocket browser clients does not support sending custom headers.
// We have to use the URL query params to provide user's JWT.
// Please be aware query params may be logged in some logging systems.
const url = new URL(req.url)
const jwt = url.searchParams.get('jwt')
if (!jwt) {
console.error('Auth token not provided')
return new Response('Auth token not provided', { status: 403 })
}
const { error, data } = await supabase.auth.getUser(jwt)
if (error) {
console.error(error)
return new Response('Invalid token provided', { status: 403 })
}
if (!data.user) {
console.error('user is not authenticated')
return new Response('User is not authenticated', { status: 403 })
}

const { socket, response } = Deno.upgradeWebSocket(req)

socket.onopen = () => {
// initiate an outbound WS connection with OpenAI
const url = 'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01'

// openai-insecure-api-key isn't a problem since this code runs in an Edge Function (not client browser)
const openaiWS = new WebSocket(url, [
'realtime',
`openai-insecure-api-key.${OPENAI_API_KEY}`,
'openai-beta.realtime-v1',
])

openaiWS.onopen = () => {
console.log('Connected to OpenAI server.')

socket.onmessage = (e) => {
console.log('socket message:', e.data)
// only send the message if openAI ws is open
if (openaiWS.readyState === 1) {
openaiWS.send(e.data)
} else {
socket.send(
JSON.stringify({
type: 'error',
msg: 'openAI connection not ready',
})
)
}
}
}

openaiWS.onmessage = (e) => {
console.log(e.data)
socket.send(e.data)
}

openaiWS.onerror = (e) => console.log('OpenAI error: ', e.message)
openaiWS.onclose = (e) => console.log('OpenAI session closed')
}

socket.onerror = (e) => console.log('socket errored:', e.message)
socket.onclose = () => console.log('socket closed')

return response // 101 (Switching Protocols)
})
```

### 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.

<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="query"
queryGroup="auth"
>
<TabPanel id="query" label="Using query params">
```ts
import { createClient } from "jsr:@supabase/supabase-js@2";

const supabase = createClient(
Deno.env.get("SUPABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
);
Deno.serve(req => {
const upgrade = req.headers.get("upgrade") || "";

if (upgrade.toLowerCase() != "websocket") {
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
}

// Please be aware query params may be logged in some logging systems.
const url = new URL(req.url);
const jwt = url.searchParams.get("jwt");
if (!jwt) {
console.error("Auth token not provided");
return new Response("Auth token not provided", { status: 403 });
}
const { error, data } = await supabase.auth.getUser(jwt);
if (error) {
console.error(error);
return new Response("Invalid token provided", { status: 403 });
}
if (!data.user) {
console.error("user is not authenticated");
return new Response("User is not authenticated", { status: 403 });
}

const { socket, response } = Deno.upgradeWebSocket(req);

socket.onopen = () => console.log("socket opened");
socket.onmessage = (e) => {
console.log("socket message:", e.data);
socket.send(new Date().toString());
};

socket.onerror = e => console.log("socket errored:", e.message);
socket.onclose = () => console.log("socket closed");

return response;

});

````
</TabPanel>
<TabPanel id="protocol" label="Using custom protocol">
```ts
import { createClient } from "jsr:@supabase/supabase-js@2";

const supabase = createClient(
Deno.env.get("SUPABASE_URL"),
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
);
Deno.serve(req => {
const upgrade = req.headers.get("upgrade") || "";

if (upgrade.toLowerCase() != "websocket") {
return new Response("request isn't trying to upgrade to websocket.", { status: 400 });
}

// Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2`
const customProtocols = (req.headers.get("Sec-WebSocket-Protocol") ?? '').split(',').map(p => p.trim())
const jwt = customProtocols.find(p => p.startsWith('jwt')).replace('jwt-', '')
if (!jwt) {
console.error("Auth token not provided");
return new Response("Auth token not provided", { status: 403 });
}
const { error, data } = await supabase.auth.getUser(jwt);
if (error) {
console.error(error);
return new Response("Invalid token provided", { status: 403 });
}
if (!data.user) {
console.error("user is not authenticated");
return new Response("User is not authenticated", { status: 403 });
}

const { socket, response } = Deno.upgradeWebSocket(req);

socket.onopen = () => console.log("socket opened");
socket.onmessage = (e) => {
console.log("socket message:", e.data);
socket.send(new Date().toString());
};

socket.onerror = e => console.log("socket errored:", e.message);
socket.onclose = () => console.log("socket closed");

return response;
});
````

</TabPanel>
</Tabs>

### 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](/docs/guides/functions/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:

```toml
[edge_runtime]
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`.
Loading