diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 3f03268edb5ba..d731b4e78ecd5 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -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', diff --git a/apps/docs/content/guides/functions/background-tasks.mdx b/apps/docs/content/guides/functions/background-tasks.mdx index 6947f1a57608e..9480c2342cd54 100644 --- a/apps/docs/content/guides/functions/background-tasks.mdx +++ b/apps/docs/content/guides/functions/background-tasks.mdx @@ -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) } diff --git a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx index 3eeaded1bed81..908d9808ee77b 100644 --- a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx +++ b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx @@ -253,7 +253,7 @@ const code = { -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). diff --git a/apps/docs/content/guides/functions/limits.mdx b/apps/docs/content/guides/functions/limits.mdx index c21f46009fd47..9a41065188b8d 100644 --- a/apps/docs/content/guides/functions/limits.mdx +++ b/apps/docs/content/guides/functions/limits.mdx @@ -8,10 +8,13 @@ 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 @@ -19,6 +22,5 @@ subtitle: "Limits applied Edge Functions in Supabase's hosted platform." - 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). diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx new file mode 100644 index 0000000000000..d606fec4a8f7a --- /dev/null +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -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. + + + +```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; + +}); + +```` + + + +```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); +```` + + + + +### 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. + + + +```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; + +}); + +```` + + +```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; + }); +```` + + + + +### 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`.