diff --git a/apps/docs/content/guides/functions/ai-models.mdx b/apps/docs/content/guides/functions/ai-models.mdx
index f31c01ed897..b349cfe424f 100644
--- a/apps/docs/content/guides/functions/ai-models.mdx
+++ b/apps/docs/content/guides/functions/ai-models.mdx
@@ -73,19 +73,18 @@ Generate text embeddings using the built-in [`gte-small`](https://huggingface.co
```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
const model = new Supabase.ai.Session('gte-small')
-Deno.serve(async (req: Request) => {
- const params = new URL(req.url).searchParams
- const input = params.get('input')
- const output = await model.run(input, { mean_pool: true, normalize: true })
- return new Response(JSON.stringify(output), {
- headers: {
- 'Content-Type': 'application/json',
- Connection: 'keep-alive',
- },
- })
-})
+export default {
+ fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+ const params = new URL(req.url).searchParams
+ const input = params.get('input')
+ const output = await model.run(input, { mean_pool: true, normalize: true })
+ return Response.json(output)
+ }),
+}
```
---
@@ -157,42 +156,46 @@ We are progressively rolling out support for the hosted solution. To sign up for
```ts supabase/functions/ollama-test/index.ts
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
+ import { withSupabase } from 'npm:@supabase/server@^1'
+
const session = new Supabase.ai.Session('mistral')
- Deno.serve(async (req: Request) => {
- const params = new URL(req.url).searchParams
- const prompt = params.get('prompt') ?? ''
+ export default {
+ fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+ const params = new URL(req.url).searchParams
+ const prompt = params.get('prompt') ?? ''
- // Get the output as a stream
- const output = await session.run(prompt, { stream: true })
+ // Get the output as a stream
+ const output = await session.run(prompt, { stream: true })
- const headers = new Headers({
- 'Content-Type': 'text/event-stream',
- Connection: 'keep-alive',
- })
+ const headers = new Headers({
+ 'Content-Type': 'text/event-stream',
+ Connection: 'keep-alive',
+ })
- // Create a stream
- const stream = new ReadableStream({
- async start(controller) {
- const encoder = new TextEncoder()
+ // Create a stream
+ const stream = new ReadableStream({
+ async start(controller) {
+ const encoder = new TextEncoder()
- try {
- for await (const chunk of output) {
- controller.enqueue(encoder.encode(chunk.response ?? ''))
+ try {
+ for await (const chunk of output) {
+ controller.enqueue(encoder.encode(chunk.response ?? ''))
+ }
+ } catch (err) {
+ console.error('Stream error:', err)
+ } finally {
+ controller.close()
}
- } catch (err) {
- console.error('Stream error:', err)
- } finally {
- controller.close()
- }
- },
- })
+ },
+ })
- // Return the stream to the user
- return new Response(stream, {
- headers,
- })
- })
+ // Return the stream to the user
+ return new Response(stream, {
+ headers,
+ })
+ }),
+ }
```
@@ -201,7 +204,7 @@ We are progressively rolling out support for the hosted solution. To sign up for
```bash
- supabase functions serve --env-file supabase/functions/.env
+ supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
```
@@ -265,36 +268,40 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
```ts supabase/functions/llamafile-test/index.ts
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
+ import { withSupabase } from 'npm:@supabase/server@^1'
+
const session = new Supabase.ai.Session('LLaMA_CPP')
- Deno.serve(async (req: Request) => {
- const params = new URL(req.url).searchParams
- const prompt = params.get('prompt') ?? ''
+ export default {
+ fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+ const params = new URL(req.url).searchParams
+ const prompt = params.get('prompt') ?? ''
- // Get the output as a stream
- const output = await session.run(
- {
- messages: [
- {
- role: 'system',
- content:
- 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
- },
- {
- role: 'user',
- content: prompt,
- },
- ],
- },
- {
- mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
- stream: false,
- }
- )
+ // Get the output as a stream
+ const output = await session.run(
+ {
+ messages: [
+ {
+ role: 'system',
+ content:
+ 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
+ },
+ {
+ role: 'user',
+ content: prompt,
+ },
+ ],
+ },
+ {
+ mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama')
+ stream: false,
+ }
+ )
- console.log('done')
- return Response.json(output)
- })
+ console.log('done')
+ return Response.json(output)
+ }),
+ }
```
@@ -302,7 +309,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
```bash
- supabase functions serve --env-file supabase/functions/.env
+ supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
```
@@ -350,60 +357,63 @@ 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'
- Deno.serve(async (req) => {
- const client = new OpenAI()
- const { prompt } = await req.json()
- const stream = true
+ export default {
+ fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+ const client = new OpenAI()
+ const { prompt } = await req.json()
+ const stream = true
- const chatCompletion = await client.chat.completions.create({
- model: 'LLaMA_CPP',
- stream,
- messages: [
- {
- role: 'system',
- content:
- 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
- },
- {
- role: 'user',
- content: prompt,
- },
- ],
- })
-
- if (stream) {
- const headers = new Headers({
- 'Content-Type': 'text/event-stream',
- Connection: 'keep-alive',
+ const chatCompletion = await client.chat.completions.create({
+ model: 'LLaMA_CPP',
+ stream,
+ messages: [
+ {
+ role: 'system',
+ content:
+ 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.',
+ },
+ {
+ role: 'user',
+ content: prompt,
+ },
+ ],
})
- // Create a stream
- const stream = new ReadableStream({
- async start(controller) {
- const encoder = new TextEncoder()
+ if (stream) {
+ const headers = new Headers({
+ 'Content-Type': 'text/event-stream',
+ Connection: 'keep-alive',
+ })
- try {
- for await (const part of chatCompletion) {
- controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
+ // Create a stream
+ const stream = new ReadableStream({
+ async start(controller) {
+ const encoder = new TextEncoder()
+
+ try {
+ for await (const part of chatCompletion) {
+ controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || ''))
+ }
+ } catch (err) {
+ console.error('Stream error:', err)
+ } finally {
+ controller.close()
}
- } catch (err) {
- console.error('Stream error:', err)
- } finally {
- controller.close()
- }
- },
- })
+ },
+ })
- // Return the stream to the user
- return new Response(stream, {
- headers,
- })
- }
+ // Return the stream to the user
+ return new Response(stream, {
+ headers,
+ })
+ }
- return Response.json(chatCompletion)
- })
+ return Response.json(chatCompletion)
+ }),
+ }
```
@@ -411,7 +421,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it
```bash
- supabase functions serve --env-file supabase/functions/.env
+ supabase functions serve --no-verify-jwt --env-file supabase/functions/.env
```
@@ -455,7 +465,7 @@ Once the function is working locally, it's time to deploy to production.
```bash
- supabase functions deploy
+ supabase functions deploy --no-verify-jwt
```
diff --git a/apps/docs/content/guides/functions/background-tasks.mdx b/apps/docs/content/guides/functions/background-tasks.mdx
index 923e70e4a10..adde605ed48 100644
--- a/apps/docs/content/guides/functions/background-tasks.mdx
+++ b/apps/docs/content/guides/functions/background-tasks.mdx
@@ -19,29 +19,39 @@ This allows you to:
You can use `EdgeRuntime.waitUntil(promise)` to explicitly mark background tasks. The Function instance continues to run until the promise provided to `waitUntil` completes.
```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
// Mark the asyncLongRunningTask's returned promise as a background task.
// ⚠️ We are NOT using `await` because we don't want it to block!
EdgeRuntime.waitUntil(asyncLongRunningTask())
-Deno.serve(async (req) => {
- return new Response(...)
-})
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ return new Response(...)
+ }),
+}
```
You can call `EdgeRuntime.waitUntil` in the request handler too. This will not block the request.
```ts
-Deno.serve(async (req) => {
- // Won't block the request, runs in background.
- EdgeRuntime.waitUntil(asyncLongRunningTask())
+import { withSupabase } from 'npm:@supabase/server@^1'
- return new Response(...)
-})
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ // Won't block the request, runs in background.
+ EdgeRuntime.waitUntil(asyncLongRunningTask())
+
+ return new Response(...)
+ }),
+}
```
You can listen to the `beforeunload` event handler to be notified when the Function is about to be shut down.
```tsx
+import { withSupabase } from 'npm:@supabase/server@^1'
+
EdgeRuntime.waitUntil(asyncLongRunningTask())
// Use beforeunload event handler to be notified when function is about to shutdown
@@ -50,9 +60,11 @@ addEventListener('beforeunload', (ev) => {
// Save state or log the current progress
})
-Deno.serve(async (req) => {
- return new Response(...)
-})
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ return new Response(...)
+ }),
+}
```
## Handling errors
diff --git a/apps/docs/content/guides/functions/connect-to-postgres.mdx b/apps/docs/content/guides/functions/connect-to-postgres.mdx
index 2c99a64fc2c..b3df92c5411 100644
--- a/apps/docs/content/guides/functions/connect-to-postgres.mdx
+++ b/apps/docs/content/guides/functions/connect-to-postgres.mdx
@@ -13,33 +13,28 @@ You can also use other Postgres clients like [Deno Postgres](https://deno.land/x
## Using supabase-js
-The `supabase-js` client handles authorization with Row Level Security and automatically formats responses as JSON. This is the recommended approach for most applications:
+The [`withSupabase`](/docs/guides/functions/auth) wrapper from `@supabase/server` hands you a `supabase-js` client (`ctx.supabase`) already scoped to the caller's Row Level Security policies, so you don't manage keys or authorization headers yourself. It also provides `ctx.supabaseAdmin` for privileged operations that bypass Row Level Security. Responses are automatically formatted as JSON. This is the recommended approach for most applications:
```ts index.ts
-import { createClient } from 'npm:@supabase/supabase-js@2'
+import { withSupabase } from 'npm:@supabase/server@^1'
-Deno.serve(async (req) => {
- try {
- const supabase = createClient(
- Deno.env.get('SUPABASE_URL') ?? '',
- Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '',
- { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
- )
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ try {
+ // ctx.supabase respects the caller's RLS policies.
+ // ctx.supabaseAdmin bypasses RLS for privileged operations.
+ const { data, error } = await ctx.supabase.from('countries').select('*')
- const { data, error } = await supabase.from('countries').select('*')
+ if (error) {
+ throw error
+ }
- if (error) {
- throw error
+ return Response.json({ data })
+ } catch (err) {
+ return new Response(String(err?.message ?? err), { status: 500 })
}
-
- return new Response(JSON.stringify({ data }), {
- headers: { 'Content-Type': 'application/json' },
- status: 200,
- })
- } catch (err) {
- return new Response(String(err?.message ?? err), { status: 500 })
- }
-})
+ }),
+}
```
This enables:
diff --git a/apps/docs/content/guides/functions/cors.mdx b/apps/docs/content/guides/functions/cors.mdx
index 2b48518de9c..420aa5a74aa 100644
--- a/apps/docs/content/guides/functions/cors.mdx
+++ b/apps/docs/content/guides/functions/cors.mdx
@@ -6,9 +6,24 @@ description: 'Add CORS headers to invoke Edge Functions from the browser.'
To invoke edge functions from the browser, you need to handle [CORS Preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) requests.
-See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
+## Automatic CORS handling
-### Recommended setup
+The [`withSupabase`](/docs/guides/functions/auth) wrapper handles CORS and preflight (`OPTIONS`) requests for you, so you don't add headers manually:
+
+```ts index.ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const { name } = await req.json()
+ return Response.json({ message: `Hello ${name}!` })
+ }),
+}
+```
+
+## Manual CORS handling
+
+If your function doesn't use `withSupabase`, add the headers yourself. See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts).
@@ -23,34 +38,26 @@ import { corsHeaders } from '@supabase/supabase-js/cors'
console.log(`Function "browser-with-cors" up and running!`)
-Deno.serve(async (req) => {
- // This is needed if you're planning to invoke your function from a browser.
- if (req.method === 'OPTIONS') {
- return new Response('ok', { headers: corsHeaders })
- }
-
- try {
- const { name } = await req.json()
- const data = {
- message: `Hello ${name}!`,
+export default {
+ fetch: async (req) => {
+ // Handle the CORS preflight request.
+ if (req.method === 'OPTIONS') {
+ return new Response('ok', { headers: corsHeaders })
}
- return new Response(JSON.stringify(data), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
- } catch (error) {
- return new Response(JSON.stringify({ error: error.message }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 400,
- })
- }
-})
+ try {
+ const { name } = await req.json()
+ return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders })
+ } catch (error) {
+ return Response.json({ error: error.message }, { status: 400, headers: corsHeaders })
+ }
+ },
+}
```
This approach ensures that when new headers are added to the Supabase SDK, your Edge Functions automatically include them, preventing CORS errors.
-#### For versions before 2.95.0
+### For versions before 2.95.0
If you're using `@supabase/supabase-js` before v2.95.0, you'll need to hardcode the CORS headers. Add a `cors.ts` file within a [`_shared` folder](/docs/guides/functions/development-environment#recommended-project-structure):
diff --git a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx
index 99808e2f6b9..04c258cb300 100644
--- a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx
+++ b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx
@@ -46,91 +46,83 @@ 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 { createClient } from 'npm:@supabase/supabase-js'
+import { withSupabase } from 'npm:@supabase/server@^1'
import { decode } from 'npm:base64-arraybuffer'
console.log('Hello from Amazon Bedrock!')
-const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
+// Called with a publishable key on the `apikey` header. Deploy with `verify_jwt = false`.
+export default {
+ fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+ prepareVirtualFile('./aws/config')
+ prepareVirtualFile('./aws/credentials')
-Deno.serve(async (req) => {
- prepareVirtualFile('./aws/config')
- prepareVirtualFile('./aws/credentials')
-
- const client = new BedrockRuntimeClient({
- region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2',
- credentials: {
- accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
- secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
- sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
- },
- })
-
- const { prompt, seed } = await req.json()
- console.log(prompt)
- const input = {
- contentType: 'application/json',
- accept: '*/*',
- modelId: 'amazon.titan-image-generator-v1',
- body: JSON.stringify({
- taskType: 'TEXT_IMAGE',
- textToImageParams: { text: prompt },
- imageGenerationConfig: {
- numberOfImages: 1,
- quality: 'standard',
- cfgScale: 8.0,
- height: 512,
- width: 512,
- seed: seed ?? 0,
+ const client = new BedrockRuntimeClient({
+ region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2',
+ credentials: {
+ accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
+ secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
+ sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
},
- }),
- }
+ })
- const command = new InvokeModelCommand(input)
- const response = await client.send(command)
- console.log(response)
-
- if (response.$metadata.httpStatusCode === 200) {
- const { body, $metadata } = response
-
- const textDecoder = new TextDecoder('utf-8')
- const jsonString = textDecoder.decode(body.buffer)
- const parsedData = JSON.parse(jsonString)
- console.log(parsedData)
- const image = parsedData.images[0]
-
- const supabaseClient = createClient(
- // Supabase API URL - env var exported by default.
- Deno.env.get('SUPABASE_URL')!,
- // Using the default Supabase API PUB KEY.
- // If you want to use a different api key, change 'default' to your preferred key name
- SUPABASE_PUBLISHABLE_KEYS['default']
- )
-
- const { data: upload, error: uploadError } = await supabaseClient.storage
- .from('images')
- .upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
- contentType: 'image/png',
- cacheControl: '3600',
- upsert: false,
- })
- if (!upload) {
- return Response.json(uploadError)
+ const { prompt, seed } = await req.json()
+ console.log(prompt)
+ const input = {
+ contentType: 'application/json',
+ accept: '*/*',
+ modelId: 'amazon.titan-image-generator-v1',
+ body: JSON.stringify({
+ taskType: 'TEXT_IMAGE',
+ textToImageParams: { text: prompt },
+ imageGenerationConfig: {
+ numberOfImages: 1,
+ quality: 'standard',
+ cfgScale: 8.0,
+ height: 512,
+ width: 512,
+ seed: seed ?? 0,
+ },
+ }),
}
- const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!)
- return Response.json(data)
- }
- return Response.json(response)
-})
+ const command = new InvokeModelCommand(input)
+ const response = await client.send(command)
+ console.log(response)
+
+ if (response.$metadata.httpStatusCode === 200) {
+ const { body, $metadata } = response
+
+ const textDecoder = new TextDecoder('utf-8')
+ const jsonString = textDecoder.decode(body.buffer)
+ const parsedData = JSON.parse(jsonString)
+ console.log(parsedData)
+ const image = parsedData.images[0]
+
+ const { data: upload, error: uploadError } = await ctx.supabase.storage
+ .from('images')
+ .upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
+ contentType: 'image/png',
+ cacheControl: '3600',
+ upsert: false,
+ })
+ if (!upload) {
+ return Response.json(uploadError)
+ }
+ const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!)
+ return Response.json(data)
+ }
+
+ return Response.json(response)
+ }),
+}
```
## Run the function locally
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
-2. Start with env: `supabase functions serve --env-file supabase/.env`
+2. Start with env: `supabase functions serve --no-verify-jwt --env-file supabase/.env`
3. Make an HTTP request:
```bash
@@ -146,7 +138,7 @@ Deno.serve(async (req) => {
```bash
supabase link
-supabase functions deploy amazon-bedrock
+supabase functions deploy amazon-bedrock --no-verify-jwt
supabase secrets set --env-file supabase/.env
```
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 e21a4dfab24..7b10c3140c6 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
@@ -34,84 +34,80 @@ supabase functions new send-email
Paste the following code into the `index.ts` file:
```tsx supabase/functions/send-email/index.ts
-import React from 'npm:react@18.3.1'
import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0'
-import { Resend } from 'npm:resend@4.0.0'
import { renderAsync } from 'npm:@react-email/components@0.0.22'
+import { withSupabase } from 'npm:@supabase/server@^1'
+import React from 'npm:react@18.3.1'
+import { Resend } from 'npm:resend@4.0.0'
+
import { MagicLinkEmail } from './_templates/magic-link.tsx'
const resend = new Resend(Deno.env.get('RESEND_API_KEY') as string)
const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v1,whsec_', '')
-Deno.serve(async (req) => {
- if (req.method !== 'POST') {
- return new Response('not allowed', { status: 400 })
- }
-
- const payload = await req.text()
- const headers = Object.fromEntries(req.headers)
- const wh = new Webhook(hookSecret)
- try {
- const {
- user,
- email_data: { token, token_hash, redirect_to, email_action_type },
- } = wh.verify(payload, headers) as {
- user: {
- email: string
- }
- email_data: {
- token: string
- token_hash: string
- redirect_to: string
- email_action_type: string
- site_url: string
- token_new: string
- token_hash_new: string
- }
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req) => {
+ if (req.method !== 'POST') {
+ return new Response('not allowed', { status: 400 })
}
- const html = await renderAsync(
- React.createElement(MagicLinkEmail, {
- supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
- token,
- token_hash,
- redirect_to,
- email_action_type,
+ const payload = await req.text()
+ const headers = Object.fromEntries(req.headers)
+ const wh = new Webhook(hookSecret)
+ try {
+ const {
+ user,
+ email_data: { token, token_hash, redirect_to, email_action_type },
+ } = wh.verify(payload, headers) as {
+ user: {
+ email: string
+ }
+ email_data: {
+ token: string
+ token_hash: string
+ redirect_to: string
+ email_action_type: string
+ site_url: string
+ token_new: string
+ token_hash_new: string
+ }
+ }
+
+ const html = await renderAsync(
+ React.createElement(MagicLinkEmail, {
+ supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
+ token,
+ token_hash,
+ redirect_to,
+ email_action_type,
+ })
+ )
+
+ const { error } = await resend.emails.send({
+ from: 'welcome ',
+ to: [user.email],
+ subject: 'Supa Custom MagicLink!',
+ html,
})
- )
-
- const { error } = await resend.emails.send({
- from: 'welcome ',
- to: [user.email],
- subject: 'Supa Custom MagicLink!',
- html,
- })
- if (error) {
- throw error
- }
- } catch (error) {
- console.log(error)
- return new Response(
- JSON.stringify({
- error: {
- http_code: error.code,
- message: error.message,
- },
- }),
- {
- status: 401,
- headers: { 'Content-Type': 'application/json' },
+ if (error) {
+ throw error
}
- )
- }
+ } catch (error) {
+ console.log(error)
+ return Response.json(
+ {
+ error: {
+ http_code: error.code,
+ message: error.message,
+ },
+ },
+ { status: 401 }
+ )
+ }
- const responseHeaders = new Headers()
- responseHeaders.set('Content-Type', 'application/json')
- return new Response(JSON.stringify({}), {
- status: 200,
- headers: responseHeaders,
- })
-})
+ return Response.json({})
+ }),
+}
```
### 3. Create React Email templates
diff --git a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx
index 8d6c208e3c3..f28412b1025 100644
--- a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx
+++ b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx
@@ -23,7 +23,7 @@ supabase functions new cloudflare-turnstile
And add the code to the `index.ts` file:
```ts index.ts
-import { corsHeaders } from '@supabase/supabase-js/cors' // v2.95.0+
+import { withSupabase } from 'npm:@supabase/server@^1'
console.log('Hello from Cloudflare Trunstile!')
@@ -31,36 +31,34 @@ function ips(req: Request) {
return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/)
}
-Deno.serve(async (req) => {
- // This is needed if you're planning to invoke your function from a browser.
- if (req.method === 'OPTIONS') {
- return new Response('ok', { headers: corsHeaders })
- }
+// `withSupabase` handles CORS and preflight requests for you.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req) => {
+ const { token } = await req.json()
+ const clientIps = ips(req) || ['']
+ const ip = clientIps[0]
- const { token } = await req.json()
- const clientIps = ips(req) || ['']
- const ip = clientIps[0]
+ // Validate the token by calling the
+ // "/siteverify" API endpoint.
+ let formData = new FormData()
+ formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '')
+ formData.append('response', token)
+ formData.append('remoteip', ip)
- // Validate the token by calling the
- // "/siteverify" API endpoint.
- let formData = new FormData()
- formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '')
- formData.append('response', token)
- formData.append('remoteip', ip)
+ const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
+ const result = await fetch(url, {
+ body: formData,
+ method: 'POST',
+ })
- const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
- const result = await fetch(url, {
- body: formData,
- method: 'POST',
- })
-
- const outcome = await result.json()
- console.log(outcome)
- if (outcome.success) {
- return new Response('success', { headers: corsHeaders })
- }
- return new Response('failure', { headers: corsHeaders })
-})
+ const outcome = await result.json()
+ console.log(outcome)
+ if (outcome.success) {
+ return new Response('success')
+ }
+ return new Response('failure')
+ }),
+}
```
## Deploy the server-side validation Edge Functions
@@ -68,7 +66,7 @@ Deno.serve(async (req) => {
- https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
```bash
-supabase functions deploy cloudflare-turnstile
+supabase functions deploy cloudflare-turnstile --no-verify-jwt
supabase secrets set CLOUDFLARE_SECRET_KEY=your_secret_key
```
diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx
index 6ea7ac92d9c..d6ef95ec477 100644
--- a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx
+++ b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx
@@ -100,95 +100,91 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the
```ts supabase/functions/text-to-speech/index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
-import { createClient } from 'npm:@supabase/supabase-js@2'
+
+import { withSupabase } from 'npm:@supabase/server@^1'
import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
import * as hash from 'npm:object-hash'
-const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
-
-// If you want to use a different api key, change 'default' to your preferred key name
-const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default'])
-
const client = new ElevenLabsClient({
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
})
-// Upload audio to Supabase Storage in a background task
-async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
- const { data, error } = await supabase.storage
- .from('audio')
- .upload(`${requestHash}.mp3`, stream, {
- contentType: 'audio/mp3',
- })
+// Deploy with verify_jwt = false
+// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
+ // Upload audio to Supabase Storage in a background task
+ async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) {
+ const { data, error } = await ctx.supabaseAdmin.storage
+ .from('audio')
+ .upload(`${requestHash}.mp3`, stream, {
+ contentType: 'audio/mp3',
+ })
- console.log('Storage upload result', { data, error })
+ console.log('Storage upload result', { data, error })
+ }
+
+ // To secure your function for production, you can for example validate the request origin,
+ // or append a user access token and validate it with Supabase Auth.
+ console.log('Request origin', req.headers.get('host'))
+ const url = new URL(req.url)
+ const params = new URLSearchParams(url.search)
+ const text = params.get('text')
+ const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
+
+ const requestHash = hash.MD5({ text, voiceId })
+ console.log('Request hash', requestHash)
+
+ // Check storage for existing audio file
+ const { data } = await ctx.supabaseAdmin.storage
+ .from('audio')
+ .createSignedUrl(`${requestHash}.mp3`, 60)
+
+ if (data) {
+ console.log('Audio file found in storage', data)
+ const storageRes = await fetch(data.signedUrl)
+ if (storageRes.ok) return storageRes
+ }
+
+ if (!text) {
+ return Response.json({ error: 'Text parameter is required' }, { status: 400 })
+ }
+
+ try {
+ console.log('ElevenLabs API call')
+ const response = await client.textToSpeech.convertAsStream(voiceId, {
+ output_format: 'mp3_44100_128',
+ model_id: 'eleven_multilingual_v2',
+ text,
+ })
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ for await (const chunk of response) {
+ controller.enqueue(chunk)
+ }
+ controller.close()
+ },
+ })
+
+ // Branch stream to Supabase Storage
+ const [browserStream, storageStream] = stream.tee()
+
+ // Upload to Supabase Storage in the background
+ EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
+
+ // Return the streaming response immediately
+ return new Response(browserStream, {
+ headers: {
+ 'Content-Type': 'audio/mpeg',
+ },
+ })
+ } catch (error) {
+ console.log('error', { error })
+ return Response.json({ error: error.message }, { status: 500 })
+ }
+ }),
}
-
-Deno.serve(async (req) => {
- // To secure your function for production, you can for example validate the request origin,
- // or append a user access token and validate it with Supabase Auth.
- console.log('Request origin', req.headers.get('host'))
- const url = new URL(req.url)
- const params = new URLSearchParams(url.search)
- const text = params.get('text')
- const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb'
-
- const requestHash = hash.MD5({ text, voiceId })
- console.log('Request hash', requestHash)
-
- // Check storage for existing audio file
- const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60)
-
- if (data) {
- console.log('Audio file found in storage', data)
- const storageRes = await fetch(data.signedUrl)
- if (storageRes.ok) return storageRes
- }
-
- if (!text) {
- return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
- status: 400,
- headers: { 'Content-Type': 'application/json' },
- })
- }
-
- try {
- console.log('ElevenLabs API call')
- const response = await client.textToSpeech.convertAsStream(voiceId, {
- output_format: 'mp3_44100_128',
- model_id: 'eleven_multilingual_v2',
- text,
- })
-
- const stream = new ReadableStream({
- async start(controller) {
- for await (const chunk of response) {
- controller.enqueue(chunk)
- }
- controller.close()
- },
- })
-
- // Branch stream to Supabase Storage
- const [browserStream, storageStream] = stream.tee()
-
- // Upload to Supabase Storage in the background
- EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
-
- // Return the streaming response immediately
- return new Response(browserStream, {
- headers: {
- 'Content-Type': 'audio/mpeg',
- },
- })
- } catch (error) {
- console.log('error', { error })
- return new Response(JSON.stringify({ error: error.message }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- })
- }
-})
```
## Run locally
diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx
index 6cdc51f58ca..e4913a30afa 100644
--- a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx
+++ b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx
@@ -108,8 +108,11 @@ 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 'jsr:@supabase/functions-js/edge-runtime.d.ts'
-import { createClient } from 'npm:@supabase/supabase-js@2'
+
+import { withSupabase } from 'npm:@supabase/server@^1'
+import type { SupabaseClient } from 'npm:@supabase/supabase-js@2'
import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5'
console.log(`Function "elevenlabs-scribe-bot" up and running!`)
@@ -117,13 +120,9 @@ console.log(`Function "elevenlabs-scribe-bot" up and running!`)
const elevenLabsClient = new ElevenLabsClient({
apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '',
})
-const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
-const supabase = createClient(
- Deno.env.get('SUPABASE_URL') || '',
- SUPABASE_SECRET_KEYS['default'] || ''
-)
async function scribe({
+ supabaseAdmin,
fileURL,
fileType,
duration,
@@ -131,6 +130,7 @@ async function scribe({
messageId,
username,
}: {
+ supabaseAdmin: SupabaseClient
fileURL: string
fileType: string
duration: number
@@ -178,9 +178,13 @@ async function scribe({
error: errorMsg,
}
console.log({ logLine })
- await supabase.from('transcription_logs').insert({ ...logLine, transcript })
+ await supabaseAdmin.from('transcription_logs').insert({ ...logLine, transcript })
}
+// Set by the request handler before delegating to grammY, so bot handlers
+// can write transcription logs with the admin client.
+let supabaseAdmin: SupabaseClient
+
const telegramBotToken = Deno.env.get('TELEGRAM_BOT_TOKEN')
const bot = new Bot(telegramBotToken || '')
const startMessage = `Welcome to the ElevenLabs Scribe Bot\\! I can transcribe speech in 99 languages with super high accuracy\\!
@@ -202,6 +206,7 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => {
// Run the transcription in the background.
EdgeRuntime.waitUntil(
scribe({
+ supabaseAdmin,
fileURL,
fileType: fileMeta.mime_type!,
duration: fileMeta.duration,
@@ -223,18 +228,24 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => {
const handleUpdate = webhookCallback(bot, 'std/http')
-Deno.serve(async (req) => {
- try {
- const url = new URL(req.url)
- if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) {
- return new Response('not allowed', { status: 405 })
- }
+// Deploy with verify_jwt = false
+// The bot is called by Telegram, so we verify the request with FUNCTION_SECRET in code.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
+ 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 await handleUpdate(req)
- } catch (err) {
- console.error(err)
- }
-})
+ supabaseAdmin = ctx.supabaseAdmin
+
+ return await handleUpdate(req)
+ } catch (err) {
+ console.error(err)
+ }
+ }),
+}
```
## Deploy to Supabase
diff --git a/apps/docs/content/guides/functions/examples/og-image.mdx b/apps/docs/content/guides/functions/examples/og-image.mdx
index ec076c16b7c..71cef4ad1ee 100644
--- a/apps/docs/content/guides/functions/examples/og-image.mdx
+++ b/apps/docs/content/guides/functions/examples/og-image.mdx
@@ -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 React from 'https://esm.sh/react@18.2.0'
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
+import React from 'https://esm.sh/react@18.2.0'
export default function handler(req: Request) {
return new ImageResponse(
@@ -46,9 +46,12 @@ export default function handler(req: Request) {
Create an `index.ts` file to execute the handler on incoming requests:
```ts index.ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
import handler from './handler.tsx'
console.log('Hello from og-image Function!')
-Deno.serve(handler)
+// Public image endpoint, so deploy with --no-verify-jwt.
+export default { fetch: withSupabase({ auth: 'none' }, handler) }
```
diff --git a/apps/docs/content/guides/functions/examples/push-notifications.mdx b/apps/docs/content/guides/functions/examples/push-notifications.mdx
index d36d99ef201..b346ebe70b7 100644
--- a/apps/docs/content/guides/functions/examples/push-notifications.mdx
+++ b/apps/docs/content/guides/functions/examples/push-notifications.mdx
@@ -53,7 +53,7 @@ Push notifications are an important part of any mobile app. They allow you to se
1. `supabase secrets set --env-file .env.local`
```ts supabase/functions/push/index.ts
- import { createClient } from 'npm:@supabase/supabase-js@2'
+ import { withSupabase } from 'npm:@supabase/server@^1'
console.log('Hello from Functions!')
@@ -71,39 +71,33 @@ Push notifications are an important part of any mobile app. They allow you to se
old_record: null | Notification
}
- const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
+ // Triggered by a Database Webhook, which authenticates with a secret key.
+ // Deploy with `verify_jwt = false`.
+ export default {
+ fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
+ const payload: WebhookPayload = await req.json()
+ const { data } = await ctx.supabaseAdmin
+ .from('profiles')
+ .select('expo_push_token')
+ .eq('id', payload.record.user_id)
+ .single()
- // If you want to use a different api key, change 'default' to your preferred key name
- const supabase = createClient(
- Deno.env.get('SUPABASE_URL')!,
- SUPABASE_SECRET_KEYS['default']
- )
+ const res = await fetch('https://exp.host/--/api/v2/push/send', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
+ },
+ body: JSON.stringify({
+ to: data?.expo_push_token,
+ sound: 'default',
+ body: payload.record.body,
+ }),
+ }).then((res) => res.json())
- Deno.serve(async (req) => {
- const payload: WebhookPayload = await req.json()
- const { data } = await supabase
- .from('profiles')
- .select('expo_push_token')
- .eq('id', payload.record.user_id)
- .single()
-
- const res = await fetch('https://exp.host/--/api/v2/push/send', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`,
- },
- body: JSON.stringify({
- to: data?.expo_push_token,
- sound: 'default',
- body: payload.record.body,
- }),
- }).then((res) => res.json())
-
- return new Response(JSON.stringify(res), {
- headers: { 'Content-Type': 'application/json' },
- })
- })
+ return Response.json(res)
+ }),
+ }
```
## Create the database webhook
@@ -169,7 +163,7 @@ Push notifications are an important part of any mobile app. They allow you to se
Add the following code to `supabase/functions/push/index.ts`:
```ts supabase/functions/push/index.ts
- import { createClient } from 'npm:@supabase/supabase-js@2'
+ import { withSupabase } from 'npm:@supabase/server@^1'
import { JWT } from 'npm:google-auth-library@9'
import serviceAccount from '../service-account.json' with { type: 'json' }
@@ -186,59 +180,53 @@ Push notifications are an important part of any mobile app. They allow you to se
schema: 'public'
}
- const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
+ // Triggered by a Database Webhook, which authenticates with a secret key.
+ // Deploy with `verify_jwt = false`.
+ export default {
+ fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
+ const payload: WebhookPayload = await req.json()
- // If you want to use a different api key, change 'default' to your preferred key name
- const supabase = createClient(
- Deno.env.get('SUPABASE_URL')!,
- SUPABASE_SECRET_KEYS['default']
- )
+ const { data } = await ctx.supabaseAdmin
+ .from('profiles')
+ .select('fcm_token')
+ .eq('id', payload.record.user_id)
+ .single()
- Deno.serve(async (req) => {
- const payload: WebhookPayload = await req.json()
+ const fcmToken = data!.fcm_token as string
- const { data } = await supabase
- .from('profiles')
- .select('fcm_token')
- .eq('id', payload.record.user_id)
- .single()
+ const accessToken = await getAccessToken({
+ clientEmail: serviceAccount.client_email,
+ privateKey: serviceAccount.private_key,
+ })
- const fcmToken = data!.fcm_token as string
-
- const accessToken = await getAccessToken({
- clientEmail: serviceAccount.client_email,
- privateKey: serviceAccount.private_key,
- })
-
- const res = await fetch(
- `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${accessToken}`,
- },
- body: JSON.stringify({
- message: {
- token: fcmToken,
- notification: {
- title: `Notification from Supabase`,
- body: payload.record.body,
- },
+ const res = await fetch(
+ `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${accessToken}`,
},
- }),
+ body: JSON.stringify({
+ message: {
+ token: fcmToken,
+ notification: {
+ title: `Notification from Supabase`,
+ body: payload.record.body,
+ },
+ },
+ }),
+ }
+ )
+
+ const resData = await res.json()
+ if (res.status < 200 || 299 < res.status) {
+ throw resData
}
- )
- const resData = await res.json()
- if (res.status < 200 || 299 < res.status) {
- throw resData
- }
-
- return new Response(JSON.stringify(resData), {
- headers: { 'Content-Type': 'application/json' },
- })
- })
+ return Response.json(resData)
+ }),
+ }
const getAccessToken = ({
clientEmail,
diff --git a/apps/docs/content/guides/functions/examples/semantic-search.mdx b/apps/docs/content/guides/functions/examples/semantic-search.mdx
index db717653b6c..47548c35d7d 100644
--- a/apps/docs/content/guides/functions/examples/semantic-search.mdx
+++ b/apps/docs/content/guides/functions/examples/semantic-search.mdx
@@ -37,28 +37,34 @@ create index on embeddings using hnsw (embedding vector_ip_ops);
You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table:
-```tsx
+```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
const model = new Supabase.ai.Session('gte-small')
-Deno.serve(async (req) => {
- const payload: WebhookPayload = await req.json()
- const { content, id } = payload.record
+// Triggered by a Database Webhook, which authenticates with a secret key.
+// Deploy with `verify_jwt = false`.
+export default {
+ fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
+ const payload: WebhookPayload = await req.json()
+ const { content, id } = payload.record
- // Generate embedding.
- const embedding = await model.run(content, {
- mean_pool: true,
- normalize: true,
- })
+ // Generate embedding.
+ const embedding = await model.run(content, {
+ mean_pool: true,
+ normalize: true,
+ })
- // Store in database.
- const { error } = await supabase
- .from('embeddings')
- .update({ embedding: JSON.stringify(embedding) })
- .eq('id', id)
- if (error) console.warn(error.message)
+ // Store in database.
+ const { error } = await ctx.supabaseAdmin
+ .from('embeddings')
+ .update({ embedding: JSON.stringify(embedding) })
+ .eq('id', id)
+ if (error) console.warn(error.message)
- return new Response('ok')
-})
+ return new Response('ok')
+ }),
+}
```
## Create a Database Function and RPC
@@ -98,32 +104,36 @@ $$;
You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts):
-```tsx
+```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
const model = new Supabase.ai.Session('gte-small')
-Deno.serve(async (req) => {
- const { search } = await req.json()
- if (!search) return new Response('Please provide a search param!')
- // Generate embedding for search term.
- const embedding = await model.run(search, {
- mean_pool: true,
- normalize: true,
- })
-
- // Query embeddings.
- const { data: result, error } = await supabase
- .rpc('query_embeddings', {
- embedding,
- match_threshold: 0.8,
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const { search } = await req.json()
+ if (!search) return new Response('Please provide a search param!')
+ // Generate embedding for search term.
+ const embedding = await model.run(search, {
+ mean_pool: true,
+ normalize: true,
})
- .select('content')
- .limit(3)
- if (error) {
- return Response.json(error)
- }
- return Response.json({ search, result })
-})
+ // Query embeddings.
+ const { data: result, error } = await ctx.supabase
+ .rpc('query_embeddings', {
+ embedding,
+ match_threshold: 0.8,
+ })
+ .select('content')
+ .limit(3)
+ if (error) {
+ return Response.json(error)
+ }
+
+ return Response.json({ search, result })
+ }),
+}
```
You now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions!
diff --git a/apps/docs/content/guides/functions/examples/send-emails.mdx b/apps/docs/content/guides/functions/examples/send-emails.mdx
index 60fc7926ad5..c9bcef0f723 100644
--- a/apps/docs/content/guides/functions/examples/send-emails.mdx
+++ b/apps/docs/content/guides/functions/examples/send-emails.mdx
@@ -30,6 +30,8 @@ Store the `RESEND_API_KEY` in your `.env` file.
Paste the following code into the `index.ts` file:
```tsx
+import { withSupabase } from 'npm:@supabase/server@^1'
+
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
const handler = async (_request: Request): Promise => {
@@ -49,15 +51,10 @@ const handler = async (_request: Request): Promise => {
const data = await res.json()
- return new Response(JSON.stringify(data), {
- status: 200,
- headers: {
- 'Content-Type': 'application/json',
- },
- })
+ return Response.json(data)
}
-Deno.serve(handler)
+export default { fetch: withSupabase({ auth: ['user', 'secret'] }, handler) }
```
### 3. Deploy and send email
@@ -69,7 +66,12 @@ supabase start
supabase functions serve --no-verify-jwt --env-file .env
```
-Test it: http://localhost:54321/functions/v1/resend
+The function accepts a signed-in user's JWT or a secret key, so it can be triggered from your app (via `supabase.functions.invoke`) or from a database function. Test it locally with a secret key:
+
+```bash
+curl -i --request POST 'http://localhost:54321/functions/v1/resend' \
+ --header 'apikey: '
+```
Deploy function to Supabase:
@@ -83,8 +85,6 @@ When you deploy to Supabase, make sure that your `RESEND_API_KEY` is set in [Edg
-Open the endpoint URL to send an email:
-
### 4. Try it yourself
Find the complete example on [GitHub](https://github.com/resendlabs/resend-supabase-edge-functions-example).
diff --git a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx
index ae6d901ebae..9a691e28233 100644
--- a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx
+++ b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx
@@ -24,6 +24,7 @@ Handle exceptions within your function and send them to Sentry.
```tsx
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
+import { withSupabase } from 'npm:@supabase/server@^1'
Sentry.init({
// https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn
@@ -39,25 +40,25 @@ Sentry.init({
Sentry.setTag('region', Deno.env.get('SB_REGION'))
Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID'))
-Deno.serve(async (req) => {
- try {
- const { name } = await req.json()
- // This will throw, as `name` in our example call will be `undefined`
- const data = {
- message: `Hello ${name}!`,
- }
+// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
+ try {
+ const { name } = await req.json()
+ // This will throw, as `name` in our example call will be `undefined`
+ const data = {
+ message: `Hello ${name}!`,
+ }
- return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
- } catch (e) {
- Sentry.captureException(e)
- // Flush Sentry before the running process closes
- await Sentry.flush(2000)
- return new Response(JSON.stringify({ msg: 'error' }), {
- status: 500,
- headers: { 'Content-Type': 'application/json' },
- })
- }
-})
+ return Response.json(data)
+ } catch (e) {
+ Sentry.captureException(e)
+ // Flush Sentry before the running process closes
+ await Sentry.flush(2000)
+ return Response.json({ msg: 'error' }, { status: 500 })
+ }
+ }),
+}
```
### 3. Deploy and test
diff --git a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx
index 63aed481548..9f78d84aedd 100644
--- a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx
+++ b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx
@@ -28,37 +28,38 @@ Here's the code of the Edge Function, you can change the response to handle the
```ts index.ts
import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js'
+import { withSupabase } from 'npm:@supabase/server@^1'
const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? ''
const botClient = new WebClient(slackBotToken)
console.log(`Slack URL verification function up and running!`)
-Deno.serve(async (req) => {
- try {
- const reqBody = await req.json()
- console.log(JSON.stringify(reqBody, null, 2))
- const { token, challenge, type, event } = reqBody
- if (type == 'url_verification') {
- return new Response(JSON.stringify({ challenge }), {
- headers: { 'Content-Type': 'application/json' },
- status: 200,
- })
- } else if (event.type == 'app_mention') {
- const { user, text, channel, ts } = event
- // Here you should process the text received and return a response:
- const response = await botClient.chat.postMessage({
- channel: channel,
- text: `Hello <@${user}>!`,
- thread_ts: ts,
- })
- return new Response('ok', { status: 200 })
+// Slack calls this endpoint, so deploy with --no-verify-jwt.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req) => {
+ try {
+ // Implement your Slack request signature verification here before trusting the payload
+ // (validate `x-slack-signature` / `x-slack-request-timestamp` with your signing secret).
+ const reqBody = await req.json()
+ console.log(JSON.stringify(reqBody, null, 2))
+ const { token, challenge, type, event } = reqBody
+
+ if (type == 'url_verification') {
+ return Response.json({ challenge })
+ } else if (event.type == 'app_mention') {
+ const { user, text, channel, ts } = event
+ // Here you should process the text received and return a response:
+ const response = await botClient.chat.postMessage({
+ channel: channel,
+ text: `Hello <@${user}>!`,
+ thread_ts: ts,
+ })
+ return new Response('ok', { status: 200 })
+ }
+ } catch (error) {
+ return Response.json({ error: error.message }, { status: 500 })
}
- } catch (error) {
- return new Response(JSON.stringify({ error: error.message }), {
- headers: { 'Content-Type': 'application/json' },
- status: 500,
- })
- }
-})
+ }),
+}
```
diff --git a/apps/docs/content/guides/functions/examples/upstash-redis.mdx b/apps/docs/content/guides/functions/examples/upstash-redis.mdx
index 5fdd926f830..630de49aca5 100644
--- a/apps/docs/content/guides/functions/examples/upstash-redis.mdx
+++ b/apps/docs/content/guides/functions/examples/upstash-redis.mdx
@@ -40,39 +40,43 @@ 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 { withSupabase } from 'npm:@supabase/server@^1'
console.log(`Function "upstash-redis-counter" up and running!`)
-Deno.serve(async (_req) => {
- try {
- const redis = new Redis({
- url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
- token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
- })
-
- const deno_region = Deno.env.get('DENO_REGION')
- if (deno_region) {
- // Increment region counter
- await redis.hincrby('supa-edge-counter', deno_region, 1)
- } else {
- // Increment localhost counter
- await redis.hincrby('supa-edge-counter', 'localhost', 1)
- }
-
- // Get all values
- const counterHash: Record | null = await redis.hgetall('supa-edge-counter')
- const counters = Object.entries(counterHash!)
- .sort(([, a], [, b]) => b - a) // sort desc
- .reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), {
- total: 0,
- regions: {},
+// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode.
+export default {
+ fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
+ try {
+ const redis = new Redis({
+ url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
+ token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
})
- return new Response(JSON.stringify({ counters }), { status: 200 })
- } catch (error) {
- return new Response(JSON.stringify({ error: error.message }), { status: 200 })
- }
-})
+ const deno_region = Deno.env.get('DENO_REGION')
+ if (deno_region) {
+ // Increment region counter
+ await redis.hincrby('supa-edge-counter', deno_region, 1)
+ } else {
+ // Increment localhost counter
+ await redis.hincrby('supa-edge-counter', 'localhost', 1)
+ }
+
+ // Get all values
+ const counterHash: Record | null = await redis.hgetall('supa-edge-counter')
+ const counters = Object.entries(counterHash!)
+ .sort(([, a], [, b]) => b - a) // sort desc
+ .reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), {
+ total: 0,
+ regions: {},
+ })
+
+ return Response.json({ counters })
+ } catch (error) {
+ return Response.json({ error: error.message }, { status: 500 })
+ }
+ }),
+}
```
## Run locally
diff --git a/apps/docs/content/guides/functions/kysely-postgres.mdx b/apps/docs/content/guides/functions/kysely-postgres.mdx
index a8930899f96..0d61b084322 100644
--- a/apps/docs/content/guides/functions/kysely-postgres.mdx
+++ b/apps/docs/content/guides/functions/kysely-postgres.mdx
@@ -198,6 +198,7 @@ import {
PostgresIntrospector,
PostgresQueryCompiler,
} from 'https://esm.sh/kysely@0.23.4'
+import { withSupabase } from 'npm:@supabase/server@^1'
import { PostgresDriver } from './DenoPostgresDriver.ts'
@@ -245,31 +246,36 @@ const db = new Kysely({
},
})
-Deno.serve(async (_req) => {
- try {
- // Run a query
- const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
+ try {
+ // Run a query
+ const animals = await db
+ .selectFrom('animals')
+ .select(['id', 'animal', 'created_at'])
+ .execute()
- // Neat, it's properly typed \o/
- console.log(animals[0].created_at.getFullYear())
+ // 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
- )
+ // Encode the result as pretty printed JSON
+ const body = JSON.stringify(
+ animals,
+ (key, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2
+ )
- // Return the response with the correct content type header
- return new Response(body, {
- status: 200,
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- },
- })
- } catch (err) {
- console.error(err)
- return new Response(String(err?.message ?? err), { status: 500 })
- }
-})
+ // Return the response with the correct content type header
+ return new Response(body, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ })
+ } catch (err) {
+ console.error(err)
+ return new Response(String(err?.message ?? err), { status: 500 })
+ }
+ }),
+}
```
diff --git a/apps/docs/content/guides/functions/routing.mdx b/apps/docs/content/guides/functions/routing.mdx
index 89e785455e5..3e112ee2d20 100644
--- a/apps/docs/content/guides/functions/routing.mdx
+++ b/apps/docs/content/guides/functions/routing.mdx
@@ -39,16 +39,20 @@ Here's a simple hello world example using some popular web frameworks:
```ts
-Deno.serve(async (req) => {
- if (req.method === 'GET') {
+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!')
+ }
+ const { name } = await req.json()
+ if (name) {
+ return new Response(`Hello ${name}!`)
+ }
return new Response('Hello World!')
- }
- const { name } = await req.json()
- if (name) {
- return new Response(`Hello ${name}!`)
- }
- return new Response('Hello World!')
-})
+ }),
+}
```
@@ -123,9 +127,11 @@ app.get('/hello-world', (c) => {
return new Response('Hello World!')
})
-Deno.serve(app.fetch)
+export default { fetch: app.fetch }
```
+To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
+
@@ -155,6 +161,8 @@ Keep in mind paths must be prefixed by function name. Route parameters can only
```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
interface Task {
id: string
name: string
@@ -204,42 +212,44 @@ async function deleteTask(id: string): Promise {
}
}
-Deno.serve(async (req) => {
- const url = new URL(req.url)
- const method = req.method
- // Extract the last part of the path as the command
- const command = url.pathname.split('/').pop()
- // Assuming the last part of the path is the task ID
- const id = command
- try {
- switch (method) {
- case 'GET':
- if (id) {
- return getTask(id)
- } else {
- return getAllTasks()
- }
- case 'POST':
- return createTask(req)
- case 'PUT':
- if (id) {
- return updateTask(id, req)
- } else {
- return new Response('Bad Request', { status: 400 })
- }
- case 'DELETE':
- if (id) {
- return deleteTask(id)
- } else {
- return new Response('Bad Request', { status: 400 })
- }
- default:
- return new Response('Method Not Allowed', { status: 405 })
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const url = new URL(req.url)
+ const method = req.method
+ // Extract the last part of the path as the command
+ const command = url.pathname.split('/').pop()
+ // Assuming the last part of the path is the task ID
+ const id = command
+ try {
+ switch (method) {
+ case 'GET':
+ if (id) {
+ return getTask(id)
+ } else {
+ return getAllTasks()
+ }
+ case 'POST':
+ return createTask(req)
+ case 'PUT':
+ if (id) {
+ return updateTask(id, req)
+ } else {
+ return new Response('Bad Request', { status: 400 })
+ }
+ case 'DELETE':
+ if (id) {
+ return deleteTask(id)
+ } else {
+ return new Response('Bad Request', { status: 400 })
+ }
+ default:
+ return new Response('Method Not Allowed', { status: 405 })
+ }
+ } catch (error) {
+ return new Response(`Internal Server Error: ${error}`, { status: 500 })
}
- } catch (error) {
- return new Response(`Internal Server Error: ${error}`, { status: 500 })
- }
-})
+ }),
+}
```
@@ -388,9 +398,11 @@ app.delete('/:id', async (c) => {
}
})
-Deno.serve(app.fetch)
+export default { fetch: app.fetch }
```
+To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
+
@@ -407,6 +419,6 @@ This works well for small apps with only a couple of routes:
<$CodeSample
path="/edge-functions/supabase/functions/restful-tasks/index.ts"
-lines={[[92, 116]]}
+lines={[[48, -1]]}
meta="restful-tasks/index.ts"
/>
diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx
index 05ce306d4df..8129b4e2af6 100644
--- a/apps/docs/content/guides/functions/websockets.mdx
+++ b/apps/docs/content/guides/functions/websockets.mdx
@@ -31,26 +31,28 @@ Here are some basic examples of setting up WebSocket servers using Deno and Node
```ts
-Deno.serve((req) => {
- const upgrade = req.headers.get('upgrade') || ''
+export default {
+ fetch: (req) => {
+ const upgrade = req.headers.get('upgrade') || ''
- if (upgrade.toLowerCase() != 'websocket') {
- return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
- }
+ if (upgrade.toLowerCase() != 'websocket') {
+ return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
+ }
- const { socket, response } = Deno.upgradeWebSocket(req)
+ 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.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')
+ socket.onerror = (e) => console.log('socket errored:', e.message)
+ socket.onclose = () => console.log('socket closed')
- return response
-})
+ return response
+ },
+}
```
@@ -123,7 +125,7 @@ WebSocket browser clients don't have the option to send custom headers. Because
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.
+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.
{
- const upgrade = req.headers.get('upgrade') || ''
- if (upgrade.toLowerCase() != 'WebSocket') {
- return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 })
- }
+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 })
+ }
- // Please be aware query params may be logged in some logging systems.
- const url = new URL(req.url)
- const jwt = url.searchParams.get('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 })
- }
+ if (!jwt) {
+ console.error('Auth token not provided')
+ return new Response('Auth token not provided', { status: 403 })
+ }
- const { error, data } = await supabase.auth.getClaims()
+ const { error, data } = await supabase.auth.getUser(jwt)
- if (error) {
- console.error(error)
- return new Response('Invalid token provided', { status: 403 })
- }
+ 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 })
- }
+ if (!data.user) {
+ console.error('user is not authenticated')
+ return new Response('User is not authenticated', { status: 403 })
+ }
- const { socket, response } = Deno.upgradeWebSocket(req)
+ 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.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')
+ socket.onerror = (e) => console.log('socket errored:', e.message)
+ socket.onclose = () => console.log('socket closed')
- return response
-})
+ return response
+ },
+}
```
@@ -199,47 +203,50 @@ const supabase = createClient(
SUPABASE_SECRET_KEYS['default']
)
-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 })
- }
+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 })
+ }
- // 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-', '')
+ // 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 jwtProtocol = customProtocols.find((p) => p.startsWith('jwt-'))
+ const jwt = jwtProtocol ? jwtProtocol.replace('jwt-', '') : null
- if (!jwt) {
- console.error('Auth token not provided')
- return new Response('Auth token not provided', { status: 403 })
- }
+ if (!jwt) {
+ console.error('Auth token not provided')
+ return new Response('Auth token not provided', { status: 403 })
+ }
- const { error, data } = await supabase.auth.getClaims()
- if (error) {
- console.error(error)
- return new Response('Invalid token 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 })
- }
+ if (!data.user) {
+ console.error('user is not authenticated')
+ return new Response('User is not authenticated', { status: 403 })
+ }
- const { socket, response } = Deno.upgradeWebSocket(req)
+ 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.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')
+ socket.onerror = (e) => console.log('socket errored:', e.message)
+ socket.onclose = () => console.log('socket closed')
- return response
-})
+ return response
+ },
+}
```
diff --git a/examples/edge-functions/supabase/functions/drizzle/index.ts b/examples/edge-functions/supabase/functions/drizzle/index.ts
index fede266f225..426347072b0 100644
--- a/examples/edge-functions/supabase/functions/drizzle/index.ts
+++ b/examples/edge-functions/supabase/functions/drizzle/index.ts
@@ -1,5 +1,6 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
+
import { countries } from '../_shared/schema.ts'
const connectionString = Deno.env.get('SUPABASE_DB_URL')!
@@ -7,8 +8,10 @@ const connectionString = Deno.env.get('SUPABASE_DB_URL')!
const client = postgres(connectionString, { prepare: false })
const db = drizzle(client)
-Deno.serve(async (_req) => {
- const allCountries = await db.select().from(countries)
+export default {
+ fetch: async (_req) => {
+ const allCountries = await db.select().from(countries)
- return Response.json(allCountries)
-})
+ return Response.json(allCountries)
+ },
+}
diff --git a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
index f8883ce3fb0..d3d51f0b2e9 100644
--- a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
+++ b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
@@ -13,36 +13,38 @@ const pool = new Pool(
1
)
-Deno.serve(async (_req) => {
- try {
- // Grab a connection from the pool
- const connection = await pool.connect()
-
+export default {
+ fetch: async (_req) => {
try {
- // Run a query
- const result = await connection.queryObject`SELECT * FROM animals`
- const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
+ // Grab a connection from the pool
+ const connection = await pool.connect()
- // Encode the result as pretty printed JSON
- const body = JSON.stringify(
- animals,
- (_key, value) => (typeof value === 'bigint' ? value.toString() : value),
- 2
- )
+ try {
+ // Run a query
+ const result = await connection.queryObject`SELECT * FROM animals`
+ const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
- // Return the response with the correct content type header
- return new Response(body, {
- status: 200,
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- },
- })
- } finally {
- // Release the connection back into the pool
- connection.release()
+ // Encode the result as pretty printed JSON
+ const body = JSON.stringify(
+ animals,
+ (_key, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2
+ )
+
+ // Return the response with the correct content type header
+ return new Response(body, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ })
+ } finally {
+ // Release the connection back into the pool
+ connection.release()
+ }
+ } catch (err) {
+ console.error(err)
+ return new Response(String(err?.message ?? err), { status: 500 })
}
- } catch (err) {
- console.error(err)
- return new Response(String(err?.message ?? err), { status: 500 })
- }
-})
+ },
+}
diff --git a/examples/edge-functions/supabase/functions/restful-tasks/index.ts b/examples/edge-functions/supabase/functions/restful-tasks/index.ts
index 0b4527abde8..fac385bf65d 100644
--- a/examples/edge-functions/supabase/functions/restful-tasks/index.ts
+++ b/examples/edge-functions/supabase/functions/restful-tasks/index.ts
@@ -2,15 +2,8 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
-import { createClient, SupabaseClient } from 'npm:supabase-js@2'
-// New approach (v2.95.0+)
-import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
-// For older versions, use hardcoded headers:
-// const corsHeaders = {
-// 'Access-Control-Allow-Origin': '*',
-// 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-// 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
-// }
+import { withSupabase } from 'npm:@supabase/server@^1'
+import type { SupabaseClient } from 'npm:@supabase/supabase-js@2'
interface Task {
name: string
@@ -21,109 +14,76 @@ async function getTask(supabaseClient: SupabaseClient, id: string) {
const { data: task, error } = await supabaseClient.from('tasks').select('*').eq('id', id)
if (error) throw error
- return new Response(JSON.stringify({ task }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
+ return Response.json({ task })
}
async function getAllTasks(supabaseClient: SupabaseClient) {
const { data: tasks, error } = await supabaseClient.from('tasks').select('*')
if (error) throw error
- return new Response(JSON.stringify({ tasks }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
+ return Response.json({ tasks })
}
async function deleteTask(supabaseClient: SupabaseClient, id: string) {
const { error } = await supabaseClient.from('tasks').delete().eq('id', id)
if (error) throw error
- return new Response(JSON.stringify({}), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
+ return Response.json({})
}
async function updateTask(supabaseClient: SupabaseClient, id: string, task: Task) {
const { error } = await supabaseClient.from('tasks').update(task).eq('id', id)
if (error) throw error
- return new Response(JSON.stringify({ task }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
+ return Response.json({ task })
}
async function createTask(supabaseClient: SupabaseClient, task: Task) {
const { error } = await supabaseClient.from('tasks').insert(task)
if (error) throw error
- return new Response(JSON.stringify({ task }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 200,
- })
+ return Response.json({ task })
}
-Deno.serve(async (req) => {
- const { url, method } = req
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const { url, method } = req
- // This is needed if you're planning to invoke your function from a browser.
- if (method === 'OPTIONS') {
- return new Response('ok', { headers: corsHeaders })
- }
+ try {
+ // ctx.supabase is scoped to the calling user, so your row-level-security
+ // (RLS) policies are applied.
+ const supabaseClient = ctx.supabase
- try {
- const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)
- // Create a Supabase client with the Auth context of the logged in user.
- const supabaseClient = createClient(
- // Supabase API URL - env var exported by default.
- Deno.env.get('SUPABASE_URL') ?? '',
- // Supabase publishable key - env var exported by default.
- SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
- // Create client with Auth context of the user that called the function.
- // This way your row-level-security (RLS) policies are applied.
- {
- global: {
- headers: { Authorization: req.headers.get('Authorization')! },
- },
+ // For more details on URLPattern, check https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API
+ const taskPattern = new URLPattern({ pathname: '/restful-tasks/:id' })
+ const matchingPath = taskPattern.exec(url)
+ const id = matchingPath ? matchingPath.pathname.groups.id : null
+
+ let task = null
+ if (method === 'POST' || method === 'PUT') {
+ const body = await req.json()
+ task = body.task
}
- )
- // For more details on URLPattern, check https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API
- const taskPattern = new URLPattern({ pathname: '/restful-tasks/:id' })
- const matchingPath = taskPattern.exec(url)
- const id = matchingPath ? matchingPath.pathname.groups.id : null
+ // call relevant method based on method and id
+ switch (true) {
+ case id && method === 'GET':
+ return getTask(supabaseClient, id as string)
+ case id && method === 'PUT':
+ return updateTask(supabaseClient, id as string, task)
+ case id && method === 'DELETE':
+ return deleteTask(supabaseClient, id as string)
+ case method === 'POST':
+ return createTask(supabaseClient, task)
+ case method === 'GET':
+ return getAllTasks(supabaseClient)
+ default:
+ return getAllTasks(supabaseClient)
+ }
+ } catch (error) {
+ console.error(error)
- let task = null
- if (method === 'POST' || method === 'PUT') {
- const body = await req.json()
- task = body.task
+ return Response.json({ error: error.message }, { status: 400 })
}
-
- // call relevant method based on method and id
- switch (true) {
- case id && method === 'GET':
- return getTask(supabaseClient, id as string)
- case id && method === 'PUT':
- return updateTask(supabaseClient, id as string, task)
- case id && method === 'DELETE':
- return deleteTask(supabaseClient, id as string)
- case method === 'POST':
- return createTask(supabaseClient, task)
- case method === 'GET':
- return getAllTasks(supabaseClient)
- default:
- return getAllTasks(supabaseClient)
- }
- } catch (error) {
- console.error(error)
-
- return new Response(JSON.stringify({ error: error.message }), {
- headers: { ...corsHeaders, 'Content-Type': 'application/json' },
- status: 400,
- })
- }
-})
+ }),
+}
diff --git a/examples/prompts/edge-functions.md b/examples/prompts/edge-functions.md
index 87447d7a997..49aa1df01e3 100644
--- a/examples/prompts/edge-functions.md
+++ b/examples/prompts/edge-functions.md
@@ -10,47 +10,115 @@ You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **h
## Guidelines
-1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)
+1. Try to use Web APIs and Deno's core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)
2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions.
3. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`.
-4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`.
-5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier.
-6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from "node:process". Use Node APIs when you find gaps in Deno APIs.
-7. Do NOT use `import { serve } from "https://deno.land/std@0.168.0/http/server.ts"`. Instead use the built-in `Deno.serve`.
-8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:
+4. For external imports, always define a version. For example, `npm:express` should be written as `npm:express@4.18.2`.
+5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from `deno.land/x`, `esm.sh` and `unpkg.com`. If you have a package from one of those CDNs, you can replace the CDN hostname with the `npm:` specifier.
+6. You can also use Node built-in APIs. You will need to import them using the `node:` specifier. For example, to import Node process: `import process from "node:process"`. Use Node APIs when you find gaps in Deno APIs.
+7. Do NOT use `import { serve } from "https://deno.land/std@0.168.0/http/server.ts"`, and do NOT use `Deno.serve`. Instead, export a default object with a `fetch` handler:
+
+ ```ts
+ export default {
+ fetch: async (req: Request) => {
+ return Response.json({ message: 'Hello world' })
+ },
+ }
+ ```
+
+ This is the request handler contract for Supabase Edge Functions, and it also runs unchanged on Cloudflare Workers and Bun. Always wrap this handler with `withSupabase` to secure and configure it (see guideline 8).
+
+8. Write your handler with `withSupabase` from `npm:@supabase/server@^1`. One wrapper gives you:
+ - Authentication: verifies the caller's credentials.
+ - Authorization: only lets through callers that match the `auth` mode you declare.
+ - Pre-configured clients on `ctx`: `ctx.supabase` (scoped to the caller's RLS) and `ctx.supabaseAdmin` (bypasses RLS).
+ - CORS handling, including preflight requests.
+
+ Your one decision is the `auth` mode:
+
+ ```ts
+ import { withSupabase } from 'npm:@supabase/server@^1'
+
+ export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const { data, error } = await ctx.supabase.from('countries').select('*')
+ if (error) throw error
+ return Response.json({ data })
+ }),
+ }
+ ```
+
+ Choose the `auth` mode by who calls the function:
+
+ | Caller | `auth` | `verify_jwt` | Client |
+ | ---------------------------------------------------- | --------------- | ---------------------- | ---------------------------------- |
+ | Signed-in user (JWT on `Authorization`) | `'user'` | `true` (default, omit) | `ctx.supabase` (RLS-scoped) |
+ | Cron, worker, `pg_net`, or another function | `'secret'` | `false` | `ctx.supabaseAdmin` (bypasses RLS) |
+ | Public client | `'publishable'` | `false` | `ctx.supabase` |
+ | Public endpoint or external webhook (verify in code) | `'none'` | `false` | `ctx.supabaseAdmin` if needed |
+
+ For any mode other than `'user'`, set `verify_jwt = false` for that function in `supabase/config.toml`:
+
+ ```toml
+ [functions.my-function]
+ verify_jwt = false
+ ```
+
+ `ctx.userClaims` holds the verified user identity. To accept only one named key, use `auth: 'secret:'` or `auth: 'publishable:'`. For a public endpoint, use `auth: 'none'`; you still get CORS handling and `ctx.supabaseAdmin`.
+
+9. The following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them:
- SUPABASE_URL
- SUPABASE_PUBLISHABLE_KEYS
- SUPABASE_SECRET_KEYS
- SUPABASE_DB_URL
-You then need to parse them with `JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)` or `JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)` to access the actual keys in the code. For example, assign the parsed map first with `const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)` and then index it with `SUPABASE_SECRET_KEYS['default']` to access the default secret key. 9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file` 10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly. 11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs. 12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.
+ `withSupabase` reads these for you, so prefer it over reading keys by hand. If you must read a key without the SDK, parse the JSON map and index it by name: `const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)`, then `SUPABASE_SECRET_KEYS['default']` for the default secret key. The publishable keys work the same way through `SUPABASE_PUBLISHABLE_KEYS`.
+
+10. To set other environment variables (ie. secrets) users can put them in an env file and run `supabase secrets set --env-file path/to/env-file`.
+11. A single Edge Function can handle multiple routes. It is recommended to use a library like Hono or Express to handle the routes as it's easier for developers to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly. For per-route Supabase auth with Hono, use the adapter from `npm:@supabase/server@^1/adapters/hono`.
+12. File write operations are ONLY permitted on the `/tmp` directory. You can use either Deno or Node File APIs.
+13. Use the `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking the response to a request. Do NOT assume it is available in the request / execution context.
## Example Templates
+### Recommended: Edge Function with `withSupabase`
+
+```ts
+import { withSupabase } from 'npm:@supabase/server@^1'
+
+export default {
+ fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
+ const { data, error } = await ctx.supabase.from('countries').select('*')
+ if (error) throw error
+ return Response.json({ data })
+ }),
+}
+```
+
### Simple Hello World Function
-```tsx
+```ts
interface reqPayload {
name: string
}
console.info('server started')
-Deno.serve(async (req: Request) => {
- const { name }: reqPayload = await req.json()
- const data = {
- message: `Hello ${name} from foo!`,
- }
+export default {
+ fetch: async (req: Request) => {
+ const { name }: reqPayload = await req.json()
+ const data = {
+ message: `Hello ${name} from foo!`,
+ }
- return new Response(JSON.stringify(data), {
- headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' },
- })
-})
+ return Response.json(data)
+ },
+}
```
### Example Function using Node built-in API
-```tsx
+```ts
import { randomBytes } from 'node:crypto'
import { createServer } from 'node:http'
import process from 'node:process'
@@ -73,7 +141,7 @@ server.listen(9999)
### Using npm packages in Functions
-```tsx
+```ts
import express from 'npm:express@4.18.2'
const app = express()
@@ -87,18 +155,15 @@ app.listen(8000)
### Generate embeddings using built-in @Supabase.ai API
-```tsx
+```ts
const model = new Supabase.ai.Session('gte-small')
-Deno.serve(async (req: Request) => {
- const params = new URL(req.url).searchParams
- const input = params.get('text')
- const output = await model.run(input, { mean_pool: true, normalize: true })
- return new Response(JSON.stringify(output), {
- headers: {
- 'Content-Type': 'application/json',
- Connection: 'keep-alive',
- },
- })
-})
+export default {
+ fetch: async (req: Request) => {
+ const params = new URL(req.url).searchParams
+ const input = params.get('text')
+ const output = await model.run(input, { mean_pool: true, normalize: true })
+ return Response.json(output)
+ },
+}
```