mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
fix(docs): align edge function docs (#47148)
## TL;DR aligns the remaining Phase 2 Edge Functions docs snippets with `@supabase/server` ## Whats Fixed? updated outdated imports and version references, and refreshed JSON examples to use Response.json() where it makes sense. left non-JSON responses as is where the integration or format actually needs them ## Ref: - towards COM-269 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated numerous Edge Function guides and examples to use modern `npm:`/`jsr:` import specifiers instead of legacy Deno URL imports. * Standardized success and error responses to return JSON consistently (using `Response.json()` and equivalent helpers) and added/clarified appropriate HTTP status codes. * Improved example error payload shapes in several guides for clearer, structured failures. * **Chores** * Refreshed version ranges in documentation and examples across SDKs and client libraries. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
171ca026b5
commit
549c1fb6ca
22 files changed
+153
-125
No files matched your search
@@ -358,7 +358,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
|
||||
|
||||
```ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts'
|
||||
import OpenAI from 'jsr:@openai/openai@^6'
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
return new Response(...)
|
||||
return Response.json({ ok: true })
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
// Won't block the request, runs in background.
|
||||
EdgeRuntime.waitUntil(asyncLongRunningTask())
|
||||
|
||||
return new Response(...)
|
||||
return Response.json({ ok: true })
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -62,7 +62,7 @@ addEventListener('beforeunload', (ev) => {
|
||||
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
return new Response(...)
|
||||
return Response.json({ ok: true })
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
|
||||
return Response.json({ data })
|
||||
} catch (err) {
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
return Response.json({ error: String(err?.message ?? err) }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ If your function doesn't use `withSupabase`, add the headers yourself. See the [
|
||||
|
||||
</Admonition>
|
||||
|
||||
Import `corsHeaders` from `@supabase/supabase-js/cors` to automatically get all required headers:
|
||||
Import `corsHeaders` from `npm:@supabase/supabase-js@^2/cors` to automatically get all required headers:
|
||||
|
||||
```ts index.ts
|
||||
import { corsHeaders } from '@supabase/supabase-js/cors'
|
||||
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'
|
||||
|
||||
console.log(`Function "browser-with-cors" up and running!`)
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
fetch: async (req) => {
|
||||
// Handle the CORS preflight request.
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
return Response.json({ ok: true }, { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -46,9 +46,9 @@ And add the code to the `index.ts` file:
|
||||
```ts index.ts
|
||||
// We need to mock the file system for the AWS SDK to work.
|
||||
import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts'
|
||||
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime'
|
||||
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { decode } from 'npm:base64-arraybuffer'
|
||||
import { decode } from 'npm:base64-arraybuffer@^1'
|
||||
|
||||
console.log('Hello from Amazon Bedrock!')
|
||||
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
upsert: false,
|
||||
})
|
||||
if (!upload) {
|
||||
return Response.json(uploadError)
|
||||
return Response.json({ error: uploadError?.message ?? 'Upload failed' }, { status: 500 })
|
||||
}
|
||||
const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!)
|
||||
return Response.json(data)
|
||||
|
||||
+7
-7
@@ -34,11 +34,11 @@ supabase functions new send-email
|
||||
Paste the following code into the `index.ts` file:
|
||||
|
||||
```tsx supabase/functions/send-email/index.ts
|
||||
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
|
||||
import { renderAsync } from 'npm:@react-email/components@0.0.22'
|
||||
import { Webhook } from 'npm:standardwebhooks@^1'
|
||||
import { renderAsync } from 'npm:@react-email/components@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import React from 'npm:react@18.3.1'
|
||||
import { Resend } from 'npm:resend@4.0.0'
|
||||
import React from 'npm:react@^19'
|
||||
import { Resend } from 'npm:resend@^6'
|
||||
|
||||
import { MagicLinkEmail } from './_templates/magic-link.tsx'
|
||||
|
||||
@@ -48,7 +48,7 @@ const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'none' }, async (req) => {
|
||||
if (req.method !== 'POST') {
|
||||
return new Response('not allowed', { status: 400 })
|
||||
return Response.json({ error: 'not allowed' }, { status: 400 })
|
||||
}
|
||||
|
||||
const payload = await req.text()
|
||||
@@ -124,8 +124,8 @@ import {
|
||||
Link,
|
||||
Preview,
|
||||
Text,
|
||||
} from 'npm:@react-email/components@0.0.22'
|
||||
import * as React from 'npm:react@18.3.1'
|
||||
} from 'npm:@react-email/components@^1'
|
||||
import * as React from 'npm:react@^19'
|
||||
|
||||
interface MagicLinkEmailProps {
|
||||
supabase_url: string
|
||||
|
||||
@@ -54,9 +54,9 @@ export default {
|
||||
const outcome = await result.json()
|
||||
console.log(outcome)
|
||||
if (outcome.success) {
|
||||
return new Response('success')
|
||||
return Response.json({ success: true })
|
||||
}
|
||||
return new Response('failure')
|
||||
return Response.json({ success: false })
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,8 +102,8 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
|
||||
import * as hash from 'npm:object-hash'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
|
||||
import * as hash from 'npm:object-hash@^3'
|
||||
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
|
||||
|
||||
@@ -107,13 +107,13 @@ Since Supabase Edge Function uses the [Deno runtime](https://deno.land/), you do
|
||||
In your newly created `scribe-bot/index.ts` file, add the following code:
|
||||
|
||||
```ts supabase/functions/scribe-bot/index.ts
|
||||
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts'
|
||||
import { Bot, webhookCallback } from 'npm:grammy@^1'
|
||||
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
|
||||
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
|
||||
|
||||
console.log(`Function "elevenlabs-scribe-bot" up and running!`)
|
||||
|
||||
@@ -235,7 +235,7 @@ export default {
|
||||
try {
|
||||
const url = new URL(req.url)
|
||||
if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
|
||||
return new Response('not allowed', { status: 405 })
|
||||
return Response.json({ error: 'not allowed' }, { status: 405 })
|
||||
}
|
||||
|
||||
supabaseAdmin = ctx.supabaseAdmin
|
||||
|
||||
@@ -21,8 +21,8 @@ Generate Open Graph images with Deno and Supabase Edge Functions. [View on GitHu
|
||||
Create a `handler.tsx` file to construct the OG image in React:
|
||||
|
||||
```tsx handler.tsx
|
||||
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
import React from 'https://esm.sh/react@18.2.0'
|
||||
import { ImageResponse } from 'npm:@vercel/og@^0'
|
||||
import React from 'npm:react@^19'
|
||||
|
||||
export default function handler(req: Request) {
|
||||
return new ImageResponse(
|
||||
|
||||
@@ -164,7 +164,7 @@ Push notifications are an important part of any mobile app. They allow you to se
|
||||
|
||||
```ts supabase/functions/push/index.ts
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import { JWT } from 'npm:google-auth-library@9'
|
||||
import { JWT } from 'npm:google-auth-library@^10'
|
||||
import serviceAccount from '../service-account.json' with { type: 'json' }
|
||||
|
||||
interface Notification {
|
||||
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
.eq('id', id)
|
||||
if (error) console.warn(error.message)
|
||||
|
||||
return new Response('ok')
|
||||
return Response.json({ ok: true })
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -112,7 +112,7 @@ const model = new Supabase.ai.Session('gte-small')
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) return new Response('Please provide a search param!')
|
||||
if (!search) return Response.json({ error: 'Please provide a search param!' }, { status: 400 })
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
|
||||
@@ -23,12 +23,12 @@ supabase functions new sentryfied
|
||||
Handle exceptions within your function and send them to Sentry.
|
||||
|
||||
```tsx
|
||||
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
|
||||
import * as Sentry from 'npm:@sentry/deno@^8'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
Sentry.init({
|
||||
// https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn
|
||||
dsn: SENTRY_DSN,
|
||||
dsn: Deno.env.get('SENTRY_DSN'),
|
||||
defaultIntegrations: false,
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0,
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
Sentry.captureException(e)
|
||||
// Flush Sentry before the running process closes
|
||||
await Sentry.flush(2000)
|
||||
return Response.json({ msg: 'error' }, { status: 500 })
|
||||
return Response.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ set SLACK_TOKEN=<xoxb-0000000000-0000000000-01010101010nacho101010>
|
||||
Here's the code of the Edge Function, you can change the response to handle the text received:
|
||||
|
||||
```ts index.ts
|
||||
import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js'
|
||||
import { WebClient } from 'npm:@slack/web-api@^7'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? ''
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
text: `Hello <@${user}>!`,
|
||||
thread_ts: ts,
|
||||
})
|
||||
return new Response('ok', { status: 200 })
|
||||
return Response.json({ ok: true })
|
||||
}
|
||||
} catch (error) {
|
||||
return Response.json({ error: error.message }, { status: 500 })
|
||||
|
||||
@@ -39,7 +39,7 @@ supabase functions new upstash-redis-counter
|
||||
And add the code to the `index.ts` file:
|
||||
|
||||
```ts index.ts
|
||||
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
|
||||
import { Redis } from 'npm:@upstash/redis@^1'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
console.log(`Function "upstash-redis-counter" up and running!`)
|
||||
|
||||
@@ -34,7 +34,7 @@ GET YOUR CERT FROM YOUR PROJECT DASHBOARD
|
||||
Create a `DenoPostgresDriver.ts` file to manage the connection to Postgres via [deno-postgres](https://deno-postgres.com/):
|
||||
|
||||
```ts DenoPostgresDriver.ts
|
||||
import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
|
||||
import { Pool, PoolClient } from 'jsr:@db/postgres@^0'
|
||||
import {
|
||||
CompiledQuery,
|
||||
DatabaseConnection,
|
||||
@@ -42,9 +42,9 @@ import {
|
||||
PostgresCursorConstructor,
|
||||
QueryResult,
|
||||
TransactionSettings,
|
||||
} from 'https://esm.sh/kysely@0.23.4'
|
||||
import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js'
|
||||
import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js'
|
||||
} from 'npm:kysely@^0'
|
||||
import { freeze, isFunction } from 'npm:kysely@^0/dist/esm/util/object-utils.js'
|
||||
import { extendStackTrace } from 'npm:kysely@^0/dist/esm/util/stack-trace-utils.js'
|
||||
|
||||
export interface PostgresDialectConfig {
|
||||
pool: Pool | (() => Promise<Pool>)
|
||||
@@ -190,14 +190,14 @@ class PostgresConnection implements DatabaseConnection {
|
||||
Create an `index.ts` file to execute a query on incoming requests:
|
||||
|
||||
```ts index.ts
|
||||
import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
|
||||
import { Pool } from 'jsr:@db/postgres@^0'
|
||||
import {
|
||||
Generated,
|
||||
Kysely,
|
||||
PostgresAdapter,
|
||||
PostgresIntrospector,
|
||||
PostgresQueryCompiler,
|
||||
} from 'https://esm.sh/kysely@0.23.4'
|
||||
} from 'npm:kysely@^0'
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
|
||||
import { PostgresDriver } from './DenoPostgresDriver.ts'
|
||||
@@ -258,23 +258,23 @@ export default {
|
||||
// Neat, it's properly typed \o/
|
||||
console.log(animals[0].created_at.getFullYear())
|
||||
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
const data = animals.map((animal) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(animal).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
return Response.json({ error: String(err?.message ?? err) }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -44,13 +44,13 @@ import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
export default {
|
||||
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
||||
if (req.method === 'GET') {
|
||||
return new Response('Hello World!')
|
||||
return Response.json({ message: 'Hello World!' })
|
||||
}
|
||||
const { name } = await req.json()
|
||||
if (name) {
|
||||
return new Response(`Hello ${name}!`)
|
||||
return Response.json({ message: `Hello ${name}!` })
|
||||
}
|
||||
return new Response('Hello World!')
|
||||
return Response.json({ message: 'Hello World!' })
|
||||
}),
|
||||
}
|
||||
```
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
<TabPanel id="expressjs" label="Express">
|
||||
|
||||
```ts
|
||||
import express from 'npm:express@4.18.2'
|
||||
import express from 'npm:express@^5'
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
@@ -70,12 +70,12 @@ app.use(express.json())
|
||||
const port = 3000
|
||||
|
||||
app.get('/hello-world', (req, res) => {
|
||||
res.send('Hello World!')
|
||||
res.json({ message: 'Hello World!' })
|
||||
})
|
||||
|
||||
app.post('/hello-world', (req, res) => {
|
||||
const { name } = req.body
|
||||
res.send(`Hello ${name}!`)
|
||||
res.json({ message: `Hello ${name}!` })
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
@@ -88,18 +88,18 @@ app.listen(port, () => {
|
||||
<TabPanel id="oak" label="Oak">
|
||||
|
||||
```ts
|
||||
import { Application } from 'jsr:@oak/oak@15/application'
|
||||
import { Router } from 'jsr:@oak/oak@15/router'
|
||||
import { Application } from 'jsr:@oak/oak@^17/application'
|
||||
import { Router } from 'jsr:@oak/oak@^17/router'
|
||||
|
||||
const router = new Router()
|
||||
|
||||
router.get('/hello-world', (ctx) => {
|
||||
ctx.response.body = 'Hello world!'
|
||||
ctx.response.body = { message: 'Hello World!' }
|
||||
})
|
||||
|
||||
router.post('/hello-world', async (ctx) => {
|
||||
const { name } = await ctx.request.body.json()
|
||||
ctx.response.body = `Hello ${name}!`
|
||||
ctx.response.body = { message: `Hello ${name}!` }
|
||||
})
|
||||
|
||||
const app = new Application()
|
||||
@@ -114,17 +114,17 @@ app.listen({ port: 3000 })
|
||||
<TabPanel id="hono" label="Hono">
|
||||
|
||||
```ts
|
||||
import { Hono } from 'jsr:@hono/hono'
|
||||
import { Hono } from 'jsr:@hono/hono@^4'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.post('/hello-world', async (c) => {
|
||||
const { name } = await c.req.json()
|
||||
return new Response(`Hello ${name}!`)
|
||||
return c.json({ message: `Hello ${name}!` })
|
||||
})
|
||||
|
||||
app.get('/hello-world', (c) => {
|
||||
return new Response('Hello World!')
|
||||
return c.json({ message: 'Hello World!' })
|
||||
})
|
||||
|
||||
export default { fetch: app.fetch }
|
||||
@@ -173,15 +173,15 @@ let tasks: Task[] = []
|
||||
const router = new Map<string, (req: Request) => Promise<Response>>()
|
||||
|
||||
async function getAllTasks(): Promise<Response> {
|
||||
return new Response(JSON.stringify(tasks))
|
||||
return Response.json({ tasks })
|
||||
}
|
||||
|
||||
async function getTask(id: string): Promise<Response> {
|
||||
const task = tasks.find((t) => t.id === id)
|
||||
if (task) {
|
||||
return new Response(JSON.stringify(task))
|
||||
return Response.json({ task })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return Response.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,16 +189,17 @@ async function createTask(req: Request): Promise<Response> {
|
||||
const id = Math.random().toString(36).substring(7)
|
||||
const task = { id, name: '' }
|
||||
tasks.push(task)
|
||||
return new Response(JSON.stringify(task), { status: 201 })
|
||||
return Response.json({ task }, { status: 201 })
|
||||
}
|
||||
|
||||
async function updateTask(id: string, req: Request): Promise<Response> {
|
||||
const index = tasks.findIndex((t) => t.id === id)
|
||||
if (index !== -1) {
|
||||
tasks[index] = { ...tasks[index] }
|
||||
return new Response(JSON.stringify(tasks[index]))
|
||||
const updates = await req.json()
|
||||
tasks[index] = { ...tasks[index], ...updates }
|
||||
return Response.json({ task: tasks[index] })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return Response.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,9 +207,9 @@ async function deleteTask(id: string): Promise<Response> {
|
||||
const index = tasks.findIndex((t) => t.id === id)
|
||||
if (index !== -1) {
|
||||
tasks.splice(index, 1)
|
||||
return new Response('Task deleted successfully')
|
||||
return Response.json({ message: 'Task deleted successfully' })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return Response.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,19 +235,19 @@ export default {
|
||||
if (id) {
|
||||
return updateTask(id, req)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
return Response.json({ error: 'Bad Request' }, { status: 400 })
|
||||
}
|
||||
case 'DELETE':
|
||||
if (id) {
|
||||
return deleteTask(id)
|
||||
} else {
|
||||
return new Response('Bad Request', { status: 400 })
|
||||
return Response.json({ error: 'Bad Request' }, { status: 400 })
|
||||
}
|
||||
default:
|
||||
return new Response('Method Not Allowed', { status: 405 })
|
||||
return Response.json({ error: 'Method Not Allowed' }, { status: 405 })
|
||||
}
|
||||
} catch (error) {
|
||||
return new Response(`Internal Server Error: ${error}`, { status: 500 })
|
||||
return Response.json({ error: `Internal Server Error: ${error}` }, { status: 500 })
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -257,7 +258,7 @@ export default {
|
||||
<TabPanel id="expressjs" label="Express">
|
||||
|
||||
```ts
|
||||
import express from 'npm:express@4.18.2'
|
||||
import express from 'npm:express@^5'
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
@@ -293,8 +294,8 @@ app.delete('/tasks/:id', async (req, res) => {
|
||||
<TabPanel id="oak" label="Oak">
|
||||
|
||||
```ts
|
||||
import { Application } from 'jsr:@oak/oak/application'
|
||||
import { Router } from 'jsr:@oak/oak/router'
|
||||
import { Application } from 'jsr:@oak/oak@^17/application'
|
||||
import { Router } from 'jsr:@oak/oak@^17/router'
|
||||
|
||||
const router = new Router()
|
||||
|
||||
@@ -357,7 +358,7 @@ app.listen({ port: 3000 })
|
||||
<TabPanel id="hono" label="Hono">
|
||||
|
||||
```ts
|
||||
import { Hono } from 'jsr:@hono/hono'
|
||||
import { Hono } from 'jsr:@hono/hono@^4'
|
||||
|
||||
// You can set the basePath with Hono
|
||||
const functionName = 'tasks'
|
||||
@@ -368,9 +369,9 @@ app.get('/:id', async (c) => {
|
||||
const id = c.req.param('id')
|
||||
const task = {} // Fetch task by id here
|
||||
if (task) {
|
||||
return new Response(JSON.stringify(task))
|
||||
return c.json({ task })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return c.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -381,9 +382,9 @@ app.patch('/:id', async (c) => {
|
||||
const task = {} // Fetch task by id here
|
||||
if (task) {
|
||||
Object.assign(task, updates)
|
||||
return new Response(JSON.stringify(task))
|
||||
return c.json({ task })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return c.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -392,9 +393,9 @@ app.delete('/:id', async (c) => {
|
||||
const task = {} // Fetch task by id here
|
||||
if (task) {
|
||||
// Delete task
|
||||
return new Response('Task deleted successfully')
|
||||
return c.json({ message: 'Task deleted successfully' })
|
||||
} else {
|
||||
return new Response('Task not found', { status: 404 })
|
||||
return c.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ description: 'How to handle WebSocket connections in Edge Functions'
|
||||
subtitle: 'Handle WebSocket connections in Edge Functions.'
|
||||
---
|
||||
|
||||
Edge Functions supports hosting WebSocket servers that can facilitate bi-directional communications with browser clients.
|
||||
Edge Functions supports hosting WebSocket servers that can facilitate bi-directional
|
||||
communications with browser clients.
|
||||
|
||||
This allows you to:
|
||||
|
||||
@@ -13,7 +14,8 @@ This allows you to:
|
||||
- Create WebSocket relay servers for external APIs
|
||||
- Establish both incoming and outgoing WebSocket connections
|
||||
|
||||
For a production-ready reconnect pattern with session persistence and replay, see [Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets).
|
||||
For a production-ready reconnect pattern with session persistence and replay, see
|
||||
[Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets).
|
||||
|
||||
---
|
||||
|
||||
@@ -36,7 +38,10 @@ export default {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
return Response.json(
|
||||
{ error: "request isn't trying to upgrade to WebSocket." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
@@ -61,7 +66,7 @@ export default {
|
||||
|
||||
```ts
|
||||
import { createServer } from 'node:http'
|
||||
import { WebSocketServer } from 'npm:ws'
|
||||
import { WebSocketServer } from 'npm:ws@^8'
|
||||
|
||||
const server = createServer()
|
||||
// Since we manually created the HTTP server,
|
||||
@@ -105,7 +110,9 @@ server.listen(8080)
|
||||
|
||||
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](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server) for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview).
|
||||
Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a
|
||||
WebSocket proxy, for example as a [relay server](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server)
|
||||
for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview).
|
||||
|
||||
<$CodeSample
|
||||
external={true}
|
||||
@@ -121,11 +128,17 @@ lines={[[1, 3], [5, -1]]}
|
||||
|
||||
## 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.
|
||||
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.
|
||||
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. The [`withSupabase`](/docs/guides/functions/auth) wrapper validates credentials on request headers, so it can't authenticate WebSocket clients. Verify the JWT yourself, as shown below.
|
||||
To authenticate the user making WebSocket requests, you can pass the JWT in URL query
|
||||
params or via a custom protocol. The [`withSupabase`](/docs/guides/functions/auth)
|
||||
wrapper validates credentials on request headers, so it can't authenticate WebSocket
|
||||
clients. Verify the JWT yourself, as shown below.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -137,7 +150,7 @@ To authenticate the user making WebSocket requests, you can pass the JWT in URL
|
||||
<TabPanel id="query" label="Using query params">
|
||||
|
||||
```ts
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { createClient } from 'npm:@supabase/supabase-js@^2'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabase = createClient(
|
||||
@@ -150,7 +163,10 @@ export default {
|
||||
fetch: async (req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
return Response.json(
|
||||
{ error: "request isn't trying to upgrade to WebSocket." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Please be aware query params may be logged in some logging systems.
|
||||
@@ -159,19 +175,19 @@ export default {
|
||||
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
return Response.json({ error: '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 })
|
||||
return Response.json({ error: 'Invalid token provided' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
return Response.json({ error: 'User is not authenticated' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
@@ -194,7 +210,7 @@ export default {
|
||||
<TabPanel id="protocol" label="Using custom protocol">
|
||||
|
||||
```ts
|
||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
||||
import { createClient } from 'npm:@supabase/supabase-js@^2'
|
||||
|
||||
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
|
||||
const supabase = createClient(
|
||||
@@ -207,7 +223,10 @@ export default {
|
||||
fetch: async (req) => {
|
||||
const upgrade = req.headers.get('upgrade') || ''
|
||||
if (upgrade.toLowerCase() != 'websocket') {
|
||||
return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
|
||||
return Response.json(
|
||||
{ error: "request isn't trying to upgrade to WebSocket." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2`
|
||||
@@ -219,18 +238,18 @@ export default {
|
||||
|
||||
if (!jwt) {
|
||||
console.error('Auth token not provided')
|
||||
return new Response('Auth token not provided', { status: 403 })
|
||||
return Response.json({ error: '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 })
|
||||
return Response.json({ error: 'Invalid token provided' }, { status: 403 })
|
||||
}
|
||||
|
||||
if (!data.user) {
|
||||
console.error('user is not authenticated')
|
||||
return new Response('User is not authenticated', { status: 403 })
|
||||
return Response.json({ error: 'User is not authenticated' }, { status: 403 })
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
@@ -254,17 +273,24 @@ export default {
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
</Admonition>
|
||||
|
||||
When using WebSockets, keep in mind that the HTTP request is considered complete after `Deno.upgradeWebSocket(req)` returns the response. To prevent early worker retirement while the socket is still open, keep an unresolved `EdgeRuntime.waitUntil()` promise that resolves in `socket.onclose`.
|
||||
When using WebSockets, keep in mind that the HTTP request is considered complete after
|
||||
`Deno.upgradeWebSocket(req)` returns the response. To prevent early worker retirement
|
||||
while the socket is still open, keep an unresolved `EdgeRuntime.waitUntil()` promise
|
||||
that resolves in `socket.onclose`.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
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:
|
||||
|
||||
@@ -275,6 +301,7 @@ policy = "per_worker"
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
</Admonition>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import { drizzle } from 'npm:drizzle-orm@^0/postgres-js'
|
||||
import postgres from 'npm:postgres@^3'
|
||||
|
||||
import { countries } from '../_shared/schema.ts'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
|
||||
import { Pool } from 'jsr:@db/postgres@^0'
|
||||
|
||||
// Create a database pool with one connection.
|
||||
const pool = new Pool(
|
||||
@@ -24,16 +24,16 @@ export default {
|
||||
const result = await connection.queryObject`SELECT * FROM animals`
|
||||
const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
|
||||
|
||||
// Encode the result as pretty printed JSON
|
||||
const body = JSON.stringify(
|
||||
animals,
|
||||
(_key, value) => (typeof value === 'bigint' ? value.toString() : value),
|
||||
2
|
||||
const data = animals.map((animal) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(animal).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
// Return the response with the correct content type header
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
return Response.json(data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
return Response.json({ error: String(err?.message ?? err) }, { status: 500 })
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { withSupabase } from 'npm:@supabase/server@^1'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@2'
|
||||
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
|
||||
|
||||
interface Task {
|
||||
name: string
|
||||
|
||||
@@ -142,7 +142,7 @@ server.listen(9999)
|
||||
### Using npm packages in Functions
|
||||
|
||||
```ts
|
||||
import express from 'npm:express@4.18.2'
|
||||
import express from 'npm:express@^5'
|
||||
|
||||
const app = express()
|
||||
|
||||
|
||||
Reference in new issue
Block a user