examples: migrate Edge Functions to @supabase/server (#46890)

- extends/supersedes: #46665 
- towards COM-269

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Standardized Edge Function entrypoints across examples to a consistent
`export default` shape, with runtime-provided admin access for
storage/database operations.
  * Updated public endpoint handling to use appropriate auth modes.
* **Bug Fixes**
* Improved error handling to return structured JSON responses with
correct HTTP status codes for invalid requests and failures.
  * Harmonized local invocation examples to use the right header format.
* **Chores**
* Updated example `verify_jwt` settings to disable JWT verification for
public/demo endpoints.
* **Documentation**
  * Fixed README typo and refreshed invocation curl examples.
* **Tests**
  * None.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Tomas Pozo <tomaspozogarzon@gmail.com>
This commit is contained in:
VaibhavandTomas Pozo authored and GitHub committed 2026-06-26 15:04:46 -05:00
1 parent 5fc0c86007
commit d5fde192d5
54 files changed
+1481 -1565

No files matched your search

@@ -13,7 +13,7 @@ file_size_limit = "50MiB"
[functions.image_gen]
enabled = true
verify_jwt = true
verify_jwt = false
# import_map = "./functions/image_gen/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -1,82 +1,77 @@
// AWS SDK issue: https://github.com/aws/aws-sdk-js-v3/issues/6134
// 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 { decode } from 'npm:base64-arraybuffer'
import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3'
import { withSupabase } from 'npm:@supabase/server@^1'
import { decode } from 'npm:base64-arraybuffer@^1'
console.log('Hello from Amazon Bedrock!')
Deno.serve(async (req) => {
prepareVirtualFile('./aws/config')
prepareVirtualFile('./aws/credentials')
// 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')
const client = new BedrockRuntimeClient({
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: '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 SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseClient = createClient(
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL')!,
// Supabase API SECRET KEY - env var exported by default.
SUPABASE_SECRET_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.supabaseAdmin.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.supabaseAdmin.storage.from('images').getPublicUrl(upload.path!)
return Response.json(data)
}
return Response.json(response)
}),
}
/* To invoke locally:
@@ -85,7 +80,7 @@ Deno.serve(async (req) => {
3. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/image_gen' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
--header 'Content-Type: application/json' \
--data '{"prompt":"A beautiful picture of a bird"}'
*/
+2 -2
View File
@@ -7,7 +7,7 @@ Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/r
This demo consists of three parts:
1. A [`generate-embedding`](./supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](./supabase/migrations/20240408072601_embeddings.sql) table.
2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an edge function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
3. A [`search` edge function](./supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
## Deploy
@@ -24,7 +24,7 @@ Run a search via curl POST request:
```bash
curl -i --location --request POST 'https://<PROJECT-REF>.supabase.co/functions/v1/search' \
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
--header 'apikey: <SUPABASE_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data '{"search":"vehicles"}'
```
@@ -33,7 +33,7 @@ sql_paths = ['./seed.sql']
[functions.generate-embedding]
enabled = true
# verify_jwt = true
verify_jwt = false
# import_map = "./functions/generate-embedding/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -41,7 +41,7 @@ enabled = true
[functions.search]
enabled = true
# verify_jwt = true
verify_jwt = false
# import_map = "./functions/search/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -1,6 +1,7 @@
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { withSupabase } from 'npm:@supabase/server@^1'
import { Database, Tables } from '../_shared/database.types.ts'
type EmbeddingsRecord = Tables<'embeddings'>
@@ -11,38 +12,38 @@ interface WebhookPayload {
schema: 'public'
old_record: null | EmbeddingsRecord
}
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient<Database>(
Deno.env.get('SUPABASE_URL')!,
SUPABASE_SECRET_KEYS['default']!
)
const model = new Supabase.ai.Session('gte-small')
Deno.serve(async (req) => {
const payload: WebhookPayload = await req.json()
const { content, id } = payload.record
// Called with a secret key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const payload: WebhookPayload = await req.json()
const { content, id } = payload.record
// Check if content has changed.
if (content === payload?.old_record?.content) {
return new Response('ok - no change')
}
// Check if content has changed.
if (content === payload?.old_record?.content) {
return Response.json({ status: 'ok - no change' })
}
// Generate embedding
const embedding = await model.run(content, {
mean_pool: true,
normalize: true,
})
// Store in DB
const { error } = await supabase
.from('embeddings')
.update({
embedding: JSON.stringify(embedding),
// Generate embedding
const embedding = await model.run(content, {
mean_pool: true,
normalize: true,
})
.eq('id', id)
if (error) console.warn(error.message)
return new Response('ok - updated')
})
// Store in DB
const { error } = await ctx.supabaseAdmin
.from('embeddings')
.update({
embedding: JSON.stringify(embedding),
})
.eq('id', id)
if (error) {
console.warn(error.message)
return Response.json({ error: error.message }, { status: 500 })
}
return Response.json({ status: 'ok - updated' })
}),
}
@@ -1,40 +1,38 @@
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { withSupabase } from 'npm:@supabase/server@^1'
import { Database } from '../_shared/database.types.ts'
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient<Database>(
Deno.env.get('SUPABASE_URL')!,
SUPABASE_SECRET_KEYS['default']!
)
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: JSON.stringify(embedding),
match_threshold: 0.8,
// Called with a secret key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const { search } = await req.json()
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,
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.supabaseAdmin
.rpc('query_embeddings', {
embedding: JSON.stringify(embedding),
match_threshold: 0.8,
})
.select('content')
.limit(3)
if (error) {
return Response.json(error)
}
return Response.json({ search, result })
}),
}
/* To invoke locally:
@@ -43,7 +41,7 @@ Deno.serve(async (req) => {
3. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/search' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'apikey: <SUPABASE_SECRET_KEY>' \
--header 'Content-Type: application/json' \
--data '{"search":"vehicles"}'
@@ -1,6 +1,6 @@
-- Insert your <PROJECT-REF> and <SUPABASE_PUBLISHABLE_KEY> below, then uncomment the trigger creation and run `supabase db push`
-- Insert your <PROJECT-REF> and <SUPABASE_SECRET_KEY> below, then uncomment the trigger creation and run `supabase db push`
-- Alternatively, head to the Database Webhook settings https://supabase.com/dashboard/project/_/database/hooks
-- Select "Create a new Hook" > Table "public.embeddings" > check "INSERT" & "Update" > Supabase Edge Functions > Add auth header with service key
-- Select "Create a new Hook" > Table "public.embeddings" > check "INSERT" & "Update" > Supabase Edge Functions > Add apiKey header with secret key
-- CREATE TRIGGER "on_inserted_or_updated_embedding"
-- AFTER INSERT
@@ -9,7 +9,7 @@
-- EXECUTE FUNCTION supabase_functions.http_request (
-- 'https://<PROJECT-REF>.supabase.co/functions/v1/generate-embedding',
-- 'POST',
-- '{"Content-type":"application/json","apikey":"<SUPABASE_PUBLISHABLE_KEY>"}',
-- '{"Content-type":"application/json","apikey":"<SUPABASE_SECRET_KEY>"}',
-- '{}',
-- '5000'
-- );
@@ -24,7 +24,7 @@
-- EXECUTE FUNCTION supabase_functions.http_request (
-- 'http://kong:8000/functions/v1/generate-embedding',
-- 'POST',
-- '{"Content-type":"application/json","apikey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"}',
-- '{"Content-type":"application/json","apikey":"<SUPABASE_SECRET_KEY>"}',
-- '{}',
-- '5000'
-- );
@@ -24,7 +24,7 @@ enabled = false
[functions.llamafile]
enabled = true
# verify_jwt = true
verify_jwt = false
# import_map = "./functions/llamafile/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -32,7 +32,7 @@ enabled = true
[functions.llamafile-stream]
enabled = true
# verify_jwt = true
verify_jwt = false
# import_map = "./functions/llamafile-stream/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -40,7 +40,7 @@ enabled = true
[functions.openai-sdk]
enabled = true
# verify_jwt = true
verify_jwt = false
# import_map = "./functions/openai-sdk/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -1,59 +1,65 @@
// https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart
import { withSupabase } from 'npm:@supabase/server@^1'
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
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') ?? ''
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
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: true,
}
)) as AsyncGenerator<any>
const body = new ReadableStream({
async pull(ctrl) {
try {
const item = await output.next()
if (item.done) {
console.log('done')
ctrl.close()
return
}
ctrl.enqueue('data: ')
ctrl.enqueue(JSON.stringify(item.value))
ctrl.enqueue('\r\n\r\n')
} catch (err) {
console.error(err)
ctrl.close()
// 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: true,
}
},
})
)) as AsyncGenerator<any>
return new Response(body.pipeThrough(new TextEncoderStream()), {
headers: {
'Content-Type': 'text/event-stream',
},
})
})
const body = new ReadableStream({
async pull(ctrl) {
try {
const item = await output.next()
if (item.done) {
console.log('done')
ctrl.close()
return
}
ctrl.enqueue('data: ')
ctrl.enqueue(JSON.stringify(item.value))
ctrl.enqueue('\r\n\r\n')
} catch (err) {
console.error(err)
ctrl.close()
}
},
})
return new Response(body.pipeThrough(new TextEncoderStream()), {
headers: {
'Content-Type': 'text/event-stream',
},
})
}),
}
/**
Run locally:
@@ -61,7 +67,7 @@ Deno.serve(async (req: Request) => {
supabase functions serve --env-file supabase/functions/.env
curl --get "http://localhost:54321/functions/v1/llamafile-stream" \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--data-urlencode "prompt=Who are you?"
--data-urlencode "prompt=Who are you?" \
-H "apikey: <SUPABASE_PUBLISHABLE_KEY>"
*/
@@ -1,35 +1,41 @@
// https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#quickstart
import { withSupabase } from 'npm:@supabase/server@^1'
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
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') ?? ''
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
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)
}),
}
/**
Run locally:
@@ -37,7 +43,7 @@ Deno.serve(async (req: Request) => {
supabase functions serve --env-file supabase/functions/.env
curl --get "http://localhost:54321/functions/v1/llamafile" \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--data-urlencode "prompt=Who are you?"
--data-urlencode "prompt=Who are you?" \
-H "apikey: <SUPABASE_PUBLISHABLE_KEY>"
*/
@@ -3,71 +3,75 @@
// This enables autocomplete, go to definition, etc.
// Setup type definitions for built-in Supabase Runtime APIs
import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts'
import OpenAI from 'npm:openai@^6'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log('Hello from openai-sdk compatible!')
Deno.serve(async (req) => {
const client = new OpenAI()
const { prompt } = await req.json()
const stream = true
// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'publishable' }, async (req) => {
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,
})
}
console.log(chatCompletion)
console.log(chatCompletion)
return Response.json(chatCompletion)
})
return Response.json(chatCompletion)
}),
}
/* To invoke locally:
supabase functions serve --env-file supabase/functions/.env
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/openai-sdk' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
--header 'Content-Type: application/json' \
--data '{"prompt":"Who are you?"}'
@@ -39,40 +39,77 @@ public = true
objects_path = "./buckets/images"
[functions.auth-hook-react-email-resend]
verify_jwt = false
[functions.background-upload-storage]
verify_jwt = false
[functions.browser-with-cors]
verify_jwt = true
[functions.cloudflare-turnstile]
verify_jwt = false
[functions.custom-jwt-validation]
verify_jwt = false
[functions.connect-supabase]
[functions.discord-bot]
[functions.elevenlabs-speech-to-text]
verify_jwt = false
[functions.elevenlabs-text-to-speech]
verify_jwt = true
[functions.file-upload-storage]
[functions.get-tshirt-competition]
verify_jwt = false
[functions.github-action-deploy]
verify_jwt = true
[functions.huggingface-image-captioning]
verify_jwt = false
[functions.image-manipulation]
verify_jwt = true
[functions.location]
verify_jwt = true
[functions.oak-server]
[functions.og-image-with-storage-cdn]
verify_jwt = false
[functions.openai]
verify_jwt = true
[functions.openai-image-generation]
verify_jwt = false
[functions.opengraph]
verify_jwt = true
[functions.postgres-on-the-edge]
[functions.puppeteer]
verify_jwt = true
[functions.simple-mcp-server]
verify_jwt = false
entrypoint = "./functions/mcp/simple-mcp-server/index.ts"
[functions.read-storage]
[functions.restful-tasks]
[functions.select-from-table-with-auth-rls]
[functions.send-email-resend]
verify_jwt = false
[functions.send-email-smtp]
verify_jwt = true
[functions.sentry]
verify_jwt = false
[functions.sentryfied]
verify_jwt = true
[functions.slack-bot-mention]
verify_jwt = false
[functions.streams]
verify_jwt = true
[functions.stripe-webhooks]
verify_jwt = false
[functions.telegram-bot]
verify_jwt = false
[functions.tweet-to-image]
verify_jwt = false
[functions.upstash-redis-counter]
verify_jwt = true
[functions.upstash-redis-ratelimit]
verify_jwt = true
[functions.kysely-postgres]
verify_jwt = true
import_map = "./functions/import_map.json"
[functions.wasm-modules]
verify_jwt = true
static_files = [ "./functions/wasm-modules/add-wasm/pkg/*.wasm"]
@@ -7,8 +7,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
@@ -11,8 +11,8 @@ import {
Row,
Section,
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 SignUpEmailProps {
username: string
@@ -1,102 +1,106 @@
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 { Webhook } from 'npm:standardwebhooks@^1'
import { render } from 'npm:@react-email/render@^2'
import { withSupabase } from 'npm:@supabase/server@^1'
import React from 'npm:react@^19'
import { Resend } from 'npm:resend@^6'
import { MagicLinkEmail } from './_templates/magic-link.tsx'
import { SignUpEmail } from './_templates/sign-up.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 })
}
// Called by Supabase Auth as a webhook (verified via standardwebhooks
// signature below), so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, async (req, _ctx) => {
if (req.method !== 'POST') {
return Response.json({ error: '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
user_metadata: {
username: string
lang: string
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
user_metadata: {
username: string
lang: 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
}
}
email_data: {
token: string
token_hash: string
redirect_to: string
email_action_type: string
site_url: string
token_new: string
token_hash_new: string
let html: string
if (email_action_type === 'signup') {
html = await render(
React.createElement(SignUpEmail, {
username: user['user_metadata'].username,
lang: user['user_metadata'].lang,
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
token,
token_hash,
redirect_to,
email_action_type,
})
)
} else if (email_action_type == 'login') {
html = await render(
React.createElement(MagicLinkEmail, {
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
token,
token_hash,
redirect_to,
email_action_type,
})
)
} else {
// TODO implement reset_password
throw new Error('Reset Password not implemented')
}
}
let html: string
const { error } = await resend.emails.send({
from: 'welcome <onboarding@resend.dev>',
to: [user.email],
subject: 'Supa Custom MagicLink!',
html,
})
if (error) {
throw error
}
} catch (error) {
console.log(error)
const httpCode =
typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined
const message = error instanceof Error ? error.message : 'Unauthorized'
if (email_action_type === 'signup') {
html = await renderAsync(
React.createElement(SignUpEmail, {
username: user['user_metadata'].username,
lang: user['user_metadata'].lang,
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
token,
token_hash,
redirect_to,
email_action_type,
})
)
} else if (email_action_type == 'login') {
html = await renderAsync(
React.createElement(MagicLinkEmail, {
supabase_url: Deno.env.get('SUPABASE_URL') ?? '',
token,
token_hash,
redirect_to,
email_action_type,
})
)
} else {
// TODO implement reset_password
throw new Error('Reset Password not implemented')
}
const { error } = await resend.emails.send({
from: 'welcome <onboarding@resend.dev>',
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,
return Response.json(
{
error: {
http_code: httpCode,
message,
},
},
}),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
)
}
{
status: 401,
}
)
}
const responseHeaders = new Headers()
responseHeaders.set('Content-Type', 'application/json')
return new Response(JSON.stringify({}), {
status: 200,
headers: responseHeaders,
})
})
return Response.json({}, { status: 200 })
}),
}
@@ -1,12 +1,10 @@
import { createClient } from 'npm:supabase-js@2'
import OpenAI from 'https://deno.land/x/openai@v4.68.2/mod.ts'
import OpenAI from 'npm:openai@^6'
import { withSupabase } from 'npm:@supabase/server@^1'
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
const client = new OpenAI({ apiKey: Deno.env.get('OPENAI_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']!)
type StorageFileApi = ReturnType<typeof supabase.storage.from>
type StorageFileApi = ReturnType<SupabaseClient['storage']['from']>
type StorageUploadPromise = ReturnType<StorageFileApi['upload']>
class MyBackgroundTaskEvent extends Event {
@@ -23,52 +21,49 @@ globalThis.addEventListener('myBackgroundTask', async (event) => {
console.log({ data, error })
})
Deno.serve(async (req) => {
const url = new URL(req.url)
const params = new URLSearchParams(url.search)
const text = params.get('text')
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
const url = new URL(req.url)
const params = new URLSearchParams(url.search)
const text = params.get('text')
if (!text) {
return new Response(JSON.stringify({ error: 'Text parameter is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
try {
const response = await client.audio.speech.create({
model: 'tts-1',
voice: 'nova',
input: text,
})
const stream = response.body
if (!stream) {
throw new Error('No stream')
if (!text) {
return Response.json({ error: 'Text parameter is required' }, { status: 400 })
}
// Branch stream to Supabase Storage
const [browserStream, storageStream] = stream.tee()
// Upload to Supabase Storage
const storageUploadPromise = supabase.storage
.from('videos')
.upload(`audio-stream_${Date.now()}.mp3`, storageStream, {
contentType: 'audio/mp3',
try {
const response = await client.audio.speech.create({
model: 'tts-1',
voice: 'nova',
input: text,
})
const event = new MyBackgroundTaskEvent(storageUploadPromise)
globalThis.dispatchEvent(event)
return new Response(browserStream, {
headers: {
'Content-Type': 'audio/mp3',
},
})
} catch (error) {
console.log('error', { error })
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
})
const stream = response.body
if (!stream) {
throw new Error('No stream')
}
// Branch stream to Supabase Storage
const [browserStream, storageStream] = stream.tee()
// Upload to Supabase Storage
const storageUploadPromise = ctx.supabaseAdmin.storage
.from('videos')
.upload(`audio-stream_${Date.now()}.mp3`, storageStream, {
contentType: 'audio/mp3',
})
const event = new MyBackgroundTaskEvent(storageUploadPromise)
globalThis.dispatchEvent(event)
return new Response(browserStream, {
headers: {
'Content-Type': 'audio/mp3',
},
})
} catch (error) {
console.log('error', { error })
return Response.json({ error: error.message }, { status: 500 })
}
}),
}
@@ -2,40 +2,29 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
// For @supabase/supabase-js v2.95.0+, import CORS headers directly from the SDK:
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
// For older versions, use a shared cors.ts file:
// import { corsHeaders } from '../_shared/cors.ts'
import { withSupabase } from 'npm:@supabase/server@^1'
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 })
}
// Authenticated endpoint, so deploy with verify_jwt = true.
// withSupabase handles CORS automatically.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
try {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
try {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
return Response.json(data)
} catch (error) {
return Response.json({ error: error.message }, { status: 400 })
}
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,
})
}
})
}),
}
// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/browser-with-cors' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'
@@ -2,7 +2,7 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { corsHeaders } from '../_shared/cors.ts'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log(`Function "cloudflare-turnstile" up and running!`)
@@ -10,51 +10,43 @@ 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 })
}
// Public endpoint, so deploy with verify_jwt = false.
// withSupabase handles CORS automatically.
export default {
fetch: withSupabase({ auth: 'none' }, async (req, _ctx) => {
try {
const { token } = await req.json()
if (!token) throw new Error('Missing token!')
try {
const { token } = await req.json()
if (!token) throw new Error('Missing token!')
const clientIps = ips(req) || ['']
const clientIps = ips(req) || ['']
// Validate the token by calling the
// "/siteverify" API endpoint.
const formData = new FormData()
formData.append('secret', Deno.env.get('CLOUDFLARE_TURNSTILE_SECRET_KEY') ?? '')
formData.append('response', token)
formData.append('remoteip', clientIps[0])
// Validate the token by calling the
// "/siteverify" API endpoint.
const formData = new FormData()
formData.append('secret', Deno.env.get('CLOUDFLARE_TURNSTILE_SECRET_KEY') ?? '')
formData.append('response', token)
formData.append('remoteip', clientIps[0])
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(JSON.stringify(outcome), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
const result = await fetch(url, {
body: formData,
method: 'POST',
})
}
throw new Error('Turnstile validation failed!')
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
const outcome = await result.json()
console.log(outcome)
if (outcome.success) {
return Response.json(outcome)
}
throw new Error('Turnstile validation failed!')
} catch (error) {
return Response.json({ error: error.message }, { status: 400 })
}
}),
}
// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/cloudflare-turnstile' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
// --header 'Content-Type: application/json' \
// --data '{"token":"cf-turnstile-response"}'
@@ -1,19 +1,25 @@
// Using 'default' Supabase auth middleware
// Change to a specific provider by importing like that:
// import { AuthMiddleware } from "../_shared/jwt/clerk.ts";
import { withSupabase } from 'npm:@supabase/server@^1'
import { AuthMiddleware } from '../_shared/jwt/default.ts'
interface reqPayload {
name: string
}
Deno.serve((r) =>
AuthMiddleware(r, async (req) => {
const { name }: reqPayload = await req.json()
const data = {
message: `Hello ${name} from foo!`,
}
// Auth is handled by the custom AuthMiddleware below, so deploy with
// verify_jwt = false. withSupabase only handles CORS here.
export default {
fetch: withSupabase({ auth: 'none' }, (req) =>
AuthMiddleware(req, async (r) => {
const { name }: reqPayload = await r.json()
const data = {
message: `Hello ${name} from foo!`,
}
return Response.json(data)
})
)
return Response.json(data)
})
),
}
@@ -4,38 +4,40 @@
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'npm:supabase-js@2'
import { Bot, webhookCallback } from 'npm:grammy@^1'
import { withSupabase } from 'npm:@supabase/server@^1'
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
declare const EdgeRuntime: {
waitUntil(promise: Promise<unknown>): void
}
console.log(`Function "elevenlabs-scribe-bot" up and running!`)
import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5'
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts'
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({
bot,
fileURL,
fileType,
duration,
chatId,
messageId,
username,
supabaseAdmin,
}: {
bot: Bot
fileURL: string
fileType: string
duration: number
chatId: number
messageId: number
username: string
supabaseAdmin: SupabaseClient
}) {
let transcript: string | null = null
let languageCode: string | null = null
@@ -60,7 +62,7 @@ async function scribe({
reply_parameters: { message_id: messageId },
})
} catch (error) {
errorMsg = error.message
errorMsg = error instanceof Error ? error.message : 'Unknown error'
console.log(errorMsg)
await bot.api.sendMessage(chatId, 'Sorry, there was an error. Please try again.', {
reply_parameters: { message_id: messageId },
@@ -77,68 +79,77 @@ async function scribe({
error: errorMsg,
}
console.log({ logLine })
await supabase.from('transcription_logs').insert({ ...logLine, transcript })
await supabaseAdmin.from('transcription_logs').insert({ ...logLine, transcript })
}
// Use beforeunload event handler to be notified when function is about to shutdown
addEventListener('beforeunload', (ev) => {
console.log('Function will be shutdown due to', ev.detail?.reason)
const reason = (ev as Event & { detail?: { reason?: string } }).detail?.reason
console.log('Function will be shutdown due to', reason)
// save state or log the current progress
})
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 80\\+ languages with super high accuracy\\!
\nTry it out by sending or forwarding me a voice message, video, or audio file\\!
\n[Learn more about Scribe](https://elevenlabs.io/speech-to-text) or [build your own bot](https://elevenlabs.io/docs/cookbooks/speech-to-text/telegram-bot)\\!
`
bot.command('start', (ctx) => ctx.reply(startMessage.trim(), { parse_mode: 'MarkdownV2' }))
bot.on([':voice', ':audio', ':video'], async (ctx) => {
try {
// console.log(ctx);
const file = await ctx.getFile()
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio
// console.log({ fileURL, fileMeta });
if (!fileMeta) {
return ctx.reply('No video|audio|voice metadata found. Please try again.')
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
const bot = new Bot(telegramBotToken || '')
const supabaseAdmin = ctx.supabaseAdmin
bot.command('start', (ctx) => ctx.reply(startMessage.trim(), { parse_mode: 'MarkdownV2' }))
bot.on([':voice', ':audio', ':video'], async (ctx) => {
try {
// console.log(ctx);
const file = await ctx.getFile()
const fileURL = `https://api.telegram.org/file/bot${telegramBotToken}/${file.file_path}`
const fileMeta = ctx.message?.video ?? ctx.message?.voice ?? ctx.message?.audio
// console.log({ fileURL, fileMeta });
if (!fileMeta) {
return ctx.reply('No video|audio|voice metadata found. Please try again.')
}
// Run the transcription in the background.
EdgeRuntime.waitUntil(
scribe({
bot,
fileURL,
fileType: fileMeta.mime_type!,
duration: fileMeta.duration,
chatId: ctx.chat.id,
messageId: ctx.message?.message_id!,
username: ctx.from?.username || '',
supabaseAdmin,
})
)
// Reply to the user immediately to let them know we received their file.
return ctx.reply('Received. Scribing...')
} catch (error) {
console.error(error)
return ctx.reply(
'Sorry, there was an error getting the file. Please try again with a smaller file!'
)
}
})
const handleUpdate = webhookCallback(bot, 'std/http')
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)
return new Response('Internal Server Error', { status: 500 })
}
// Run the transcription in the background.
EdgeRuntime.waitUntil(
scribe({
fileURL,
fileType: fileMeta.mime_type!,
duration: fileMeta.duration,
chatId: ctx.chat.id,
messageId: ctx.message?.message_id!,
username: ctx.from?.username || '',
})
)
// Reply to the user immediately to let them know we received their file.
return ctx.reply('Received. Scribing...')
} catch (error) {
console.error(error)
return ctx.reply(
'Sorry, there was an error getting the file. Please try again with a smaller file!'
)
}
})
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 })
}
return await handleUpdate(req)
} catch (err) {
console.error(err)
}
})
}),
}
@@ -1,19 +1,26 @@
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'npm:supabase-js@2'
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')!)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
import { withSupabase } from 'npm:@supabase/server@^1'
import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2'
import { ElevenLabsClient } from 'npm:elevenlabs@^1'
import * as hash from 'npm:object-hash@^3'
declare const EdgeRuntime: {
waitUntil(promise: Promise<unknown>): void
}
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
async function uploadAudioToStorage(
supabaseAdmin: SupabaseClient,
stream: ReadableStream,
requestHash: string
) {
const { data, error } = await supabaseAdmin.storage
.from('audio')
.upload(`${requestHash}.mp3`, stream, {
contentType: 'audio/mp3',
@@ -22,68 +29,66 @@ async function uploadAudioToStorage(stream: ReadableStream, requestHash: string)
console.log('Storage upload result', { data, error })
}
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'
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
// You can add extra checks here, for example validating the request origin.
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)
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)
// 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 (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' },
})
}
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,
})
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()
},
})
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()
// Branch stream to Supabase Storage
const [browserStream, storageStream] = stream.tee()
// Upload to Supabase Storage in the background
EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash))
// Upload to Supabase Storage in the background
EdgeRuntime.waitUntil(uploadAudioToStorage(ctx.supabaseAdmin, 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' },
})
}
})
// 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 })
}
}),
}
@@ -2,11 +2,7 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { createClient } from 'npm:supabase-js@2'
// New approach (v2.95.0+)
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
// For older versions:
// import { corsHeaders } from '../_shared/cors.ts'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log(`Function "get-tshirt-competition" up and running!`)
@@ -25,72 +21,54 @@ function turnEmailToCount(email: string): string {
)}`
}
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 })
}
// Public challenge endpoint, so deploy with verify_jwt = false.
export default {
fetch: withSupabase<any>({ auth: 'none' }, async (req, ctx) => {
try {
const url = new URL(req.url)
const email = url.searchParams.get('email')
const twitter = url.searchParams.get('twitter')
const size = url.searchParams.get('size')
const answer = url.searchParams.get('answer')
try {
const url = new URL(req.url)
const email = url.searchParams.get('email')
const twitter = url.searchParams.get('twitter')
const size = url.searchParams.get('size')
const answer = url.searchParams.get('answer')
if (
!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email ?? '') ||
!answer ||
!twitter ||
!size
) {
throw new Error(
`Please provide valid 'email', 'twitter', 'size', and 'answer' params. HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
)
}
if (answer !== countEmailSegments(email!)) {
throw new Error(
`Sorry, that's wrong, please try again! HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
)
}
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseAdminClient = createClient(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Submit email to draw
const { error } = await supabaseAdminClient.from('get-tshirt-competition-2').upsert(
{
email,
twitter,
size,
},
{ onConflict: 'email' }
)
if (error) {
console.log(error)
throw new Error(error.details)
}
return new Response(
`Thanks for playing! ${turnEmailToCount(email!)} has been added to the draw \\o/`,
{
headers: { ...corsHeaders, 'Content-Type': 'text/plain' },
status: 200,
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email ?? '') || !answer || !twitter || !size) {
throw new Error(
`Please provide valid 'email', 'twitter', 'size', and 'answer' params. HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
)
}
)
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
if (answer !== countEmailSegments(email!)) {
throw new Error(
`Sorry, that's wrong, please try again! HINT: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts`
)
}
// Submit email to draw
const { error } = await ctx.supabaseAdmin.from('get-tshirt-competition-2').upsert(
{
email,
twitter,
size,
},
{ onConflict: 'email' }
)
if (error) {
console.log(error)
throw new Error(error.details)
}
return new Response(
`Thanks for playing! ${turnEmailToCount(email!)} has been added to the draw \\o/`,
{
headers: { 'Content-Type': 'text/plain' },
status: 200,
}
)
} catch (error) {
return Response.json({ error: error.message }, { status: 400 })
}
}),
}
// To invoke:
// curl -i --location --request GET 'http://localhost:54321/functions/v1/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&size=2XL&answer=20'
@@ -2,16 +2,21 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { withSupabase } from 'npm:@supabase/server@^1'
console.log('Hello from Functions!')
Deno.serve((_req) => {
const data = {
message: `I was deployed via GitHub Actions!`,
}
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (_req, _ctx) => {
const data = {
message: `I was deployed via GitHub Actions!`,
}
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
})
})
return Response.json(data)
}),
}
// To invoke: http://localhost:54321/functions/v1/github-action-deploy
// To invoke:
// curl -i --location --request GET 'http://localhost:54321/functions/v1/github-action-deploy' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
@@ -1,5 +1,6 @@
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
import { createClient } from 'npm:supabase-js@2'
import { HfInference } from 'npm:@huggingface/inference@^4'
import { withSupabase } from 'npm:@supabase/server@^1'
import { Database } from './types.ts'
console.log('Hello from `huggingface-image-captioning` function!')
@@ -15,35 +16,31 @@ interface WebhookPayload {
old_record: null | SoRecord
}
Deno.serve(async (req) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseAdminClient = createClient<Database>(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
// Construct image url from storage
const { data, error } = await supabaseAdminClient.storage
.from(soRecord.bucket_id!)
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
if (error) throw error
const { signedUrl } = data
// Construct image url from storage
const { data, error } = await ctx.supabaseAdmin.storage
.from(soRecord.bucket_id!)
.createSignedUrl(soRecord.path_tokens!.join('/'), 60)
if (error) throw error
const { signedUrl } = data
// Run image captioning with Huggingface
const imgDesc = await hf.imageToText({
data: await (await fetch(signedUrl)).blob(),
model: 'nlpconnect/vit-gpt2-image-captioning',
})
// Run image captioning with Huggingface
const imgDesc = await hf.imageToText({
data: await (await fetch(signedUrl)).blob(),
model: 'nlpconnect/vit-gpt2-image-captioning',
})
// Store image caption in Database table
await supabaseAdminClient
.from('image_caption')
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
.throwOnError()
// Store image caption in Database table
await ctx.supabaseAdmin
.from('image_caption')
.insert({ id: soRecord.id!, caption: imgDesc.generated_text })
.throwOnError()
return new Response('ok')
})
return Response.json({ status: 'ok' })
}),
}
@@ -1,28 +1,34 @@
// This is an example showing how to use Magick WASM to do image manipulations in Edge Functions.
//
import {
ImageMagick,
initializeImageMagick,
MagickFormat,
} from 'npm:@imagemagick/magick-wasm@0.0.30'
import { ImageMagick, initializeImageMagick, MagickFormat } from 'npm:@imagemagick/magick-wasm@^0'
import { withSupabase } from 'npm:@supabase/server@^1'
const wasmBytes = await Deno.readFile(
new URL('magick.wasm', import.meta.resolve('npm:@imagemagick/magick-wasm@0.0.30'))
new URL('magick.wasm', import.meta.resolve('npm:@imagemagick/magick-wasm@^0'))
)
await initializeImageMagick(wasmBytes)
Deno.serve(async (req) => {
const formData = await req.formData()
const content = await formData.get('file').bytes()
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
const formData = await req.formData()
const file = formData.get('file')
if (!(file instanceof Blob)) {
return Response.json({ error: 'file is required' }, { status: 400 })
}
const content = await file.bytes()
let result = ImageMagick.read(content, (img): Uint8Array => {
// resize the image
img.resize(500, 300)
// add a blur of 60x5
img.blur(60, 5)
let result = ImageMagick.read(content, (img): Uint8Array => {
// resize the image
img.resize(500, 300)
// add a blur of 60x5
img.blur(60, 5)
return img.write((data) => data)
})
return img.write((data) => data)
})
return new Response(result, { headers: { 'Content-Type': 'image/png' } })
})
return new Response(Uint8Array.from(result), {
headers: { 'Content-Type': 'image/png' },
})
}),
}
@@ -2,7 +2,6 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { Pool } from 'postgres'
import {
Generated,
Kysely,
@@ -10,6 +9,9 @@ import {
PostgresIntrospector,
PostgresQueryCompiler,
} from 'kysely'
import { withSupabase } from 'npm:@supabase/server@^1'
import { Pool } from 'postgres'
import { PostgresDriver } from './DenoPostgresDriver.ts'
console.log(`Function "kysely-postgres" up and running!`)
@@ -58,32 +60,42 @@ const db = new Kysely<Database>({
},
})
Deno.serve(async (_req) => {
try {
// Run a query
const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()
// Authenticated endpoint, so deploy with verify_jwt = true.
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/
if (animals[0]) {
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('Internal Server Error', { status: 500 })
}
}),
}
// To invoke: navigate to http://localhost:54321/functions/v1/kysely-postgres
// To invoke:
// curl -i --location --request GET 'http://localhost:54321/functions/v1/kysely-postgres' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
@@ -2,27 +2,30 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { withSupabase } from 'npm:@supabase/server@^1'
function ips(req: Request) {
return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/)
}
Deno.serve(async (req) => {
const clientIps = ips(req) || ['']
const res = await fetch(
`https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`,
{
headers: { 'Content-Type': 'application/json' },
}
)
if (res.ok) {
const { city, country } = await res.json()
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, _ctx) => {
const clientIps = ips(req) || ['']
const res = await fetch(
`https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`,
{
headers: { 'Content-Type': 'application/json' },
}
)
if (res.ok) {
const { city, country } = await res.json()
return new Response(JSON.stringify(`You're accessing from ${city}, ${country}`), {
headers: { 'Content-Type': 'application/json' },
})
} else {
return new Response(await res.text(), {
status: 400,
})
}
})
return Response.json(`You're accessing from ${city}, ${country}`)
} else {
return new Response(await res.text(), {
status: 400,
})
}
}),
}
@@ -1,9 +1,10 @@
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPTransport } from '@hono/mcp'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { Hono } from 'hono'
import { withSupabase } from 'npm:@supabase/server@^1'
import { z } from 'zod'
// Create Hono app
@@ -35,4 +36,7 @@ app.all('/', async (c) => {
return transport.handleRequest(c)
})
Deno.serve(app.fetch)
// Public endpoint, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, app.fetch),
}
@@ -1,7 +1,5 @@
import React from 'https://esm.sh/react@18.2.0?deno-std=0.177.0'
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'
import { ImageResponse } from 'npm:@vercel/og@^0'
import React from 'npm:react@19'
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw6'
const BACKGROUND_IMAGE_STD = `${STORAGE_URL}/lw6_ticket_regular.png`
@@ -13,7 +11,7 @@ const SUPA_CHECKMARK_GOLD = `${STORAGE_URL}/supaverified_gold.png?v=3`
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
export async function handler(req: Request) {
export async function handler(req: Request, ctx) {
const url = new URL(req.url)
const ticketNumber = url.searchParams.get('ticketNumber')
const username = url.searchParams.get('username') ?? url.searchParams.get('amp;username')
@@ -21,10 +19,7 @@ export async function handler(req: Request) {
const golden = url.searchParams.get('golden') ?? url.searchParams.get('amp;golden')
if (!username || !ticketNumber || !name) {
return new Response(JSON.stringify({ error: 'missing params' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
return Response.json({ error: 'missing params' }, { status: 400 })
}
try {
@@ -195,17 +190,8 @@ export async function handler(req: Request) {
}
)
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseAdminClient = createClient(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Upload image to storage.
const { error } = await supabaseAdminClient.storage
const { error } = await ctx.supabaseAdmin.storage
.from('images')
.upload(`lw6/tickets/${username}.png`, generatedImage.body!, {
contentType: 'image/png',
@@ -216,9 +202,6 @@ export async function handler(req: Request) {
return await fetch(`${STORAGE_URL}/tickets/${username}.png?v=3`)
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
return Response.json({ error: error.message }, { status: 400 })
}
}
@@ -2,8 +2,12 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { withSupabase } from 'npm:@supabase/server@^1'
import { handler } from './handler.tsx'
console.log(`Function "og-image-with-storage-cdn" up and running!`)
Deno.serve(handler)
export default {
fetch: withSupabase({ auth: 'secret' }, handler),
}
@@ -2,142 +2,111 @@
// - `generated-images` bucket already created.
// - `OPENAI_API_KEY` environment variable set.
import { createClient } from 'npm:supabase-js@2'
import OpenAI, { toFile } from 'jsr:@openai/openai@4.96.2'
import OpenAI, { toFile } from 'jsr:@openai/openai@^6'
import { withSupabase } from 'npm:@supabase/server@^1'
// Configure COS headers for the function
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
try {
const { prompt, imageUrls }: { prompt: string; imageUrls: string[] } = await req.json()
Deno.serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, {
headers: corsHeaders,
})
}
try {
const { prompt, imageUrls }: { prompt: string; imageUrls: string[] } = await req.json()
if (!prompt || !imageUrls || imageUrls.length === 0) {
return new Response(
JSON.stringify({
error: 'Prompt and at least one image URL are required',
}),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
}
// Create OpenAI client
const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY'),
})
// Get the images from the provided URLs
const imagePromises = imageUrls.map(async (url: string) => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch image from ${url}`)
if (!prompt || !imageUrls || imageUrls.length === 0) {
return Response.json(
{
error: 'Prompt and at least one image URL are required',
},
{ status: 400 }
)
}
return await response.blob()
})
const imageBlobs = await Promise.all(imagePromises)
const images = await Promise.all(
imageBlobs.map(
async (blob, index) =>
await toFile(blob, `image-${index}.png`, {
type: blob.type || 'image/png',
})
)
)
// Call OpenAI API using the client
console.log('Calling OpenAI API with prompt:', prompt)
const response = await openai.images.edit({
model: 'gpt-image-1',
image: images,
prompt: prompt,
})
if (!response.data || response.data.length === 0) {
throw new Error('No image data received from OpenAI API')
}
// Convert base64 to blob
const base64Data = response.data[0].b64_json!
const binaryData = atob(base64Data)
const uint8Array = new Uint8Array(binaryData.length)
for (let i = 0; i < binaryData.length; i++) {
uint8Array[i] = binaryData.charCodeAt(i)
}
const generatedImageBlob = new Blob([uint8Array], { type: 'image/png' })
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
// Upload the generated image to Supabase Storage
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') || '',
SUPABASE_SECRET_KEYS['default'] || ''
)
// Create a unique identifier for this generation
const timestamp = new Date().toISOString()
const randomId = crypto.randomUUID()
const outputImageName = `generated-${timestamp}-${randomId}.png`
const { error: uploadError } = await supabaseClient.storage
.from('generated-images')
.upload(outputImageName, generatedImageBlob, {
contentType: 'image/png',
upsert: true,
// Create OpenAI client
const openai = new OpenAI({
apiKey: Deno.env.get('OPENAI_API_KEY'),
})
if (uploadError) {
console.error('Supabase Storage upload error:', uploadError)
return new Response(
JSON.stringify({
error: 'Failed to upload generated image to storage',
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
// Get the images from the provided URLs
const imagePromises = imageUrls.map(async (url: string) => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch image from ${url}`)
}
return await response.blob()
})
const imageBlobs = await Promise.all(imagePromises)
const images = await Promise.all(
imageBlobs.map(
async (blob, index) =>
await toFile(blob, `image-${index}.png`, {
type: blob.type || 'image/png',
})
)
)
}
// Get the public URL for the uploaded image
const {
data: { publicUrl },
} = supabaseClient.storage.from('generated-images').getPublicUrl(outputImageName)
// Call OpenAI API using the client
console.log('Calling OpenAI API with prompt:', prompt)
const response = await openai.images.edit({
model: 'gpt-image-1',
image: images,
prompt: prompt,
})
return new Response(
JSON.stringify({
if (!response.data || response.data.length === 0) {
throw new Error('No image data received from OpenAI API')
}
// Convert base64 to blob
const base64Data = response.data[0].b64_json!
const binaryData = atob(base64Data)
const uint8Array = new Uint8Array(binaryData.length)
for (let i = 0; i < binaryData.length; i++) {
uint8Array[i] = binaryData.charCodeAt(i)
}
const generatedImageBlob = new Blob([uint8Array], { type: 'image/png' })
// Create a unique identifier for this generation
const timestamp = new Date().toISOString()
const randomId = crypto.randomUUID()
const outputImageName = `generated-${timestamp}-${randomId}.png`
// Upload the generated image to Supabase Storage
const { error: uploadError } = await ctx.supabaseAdmin.storage
.from('generated-images')
.upload(outputImageName, generatedImageBlob, {
contentType: 'image/png',
upsert: true,
})
if (uploadError) {
console.error('Supabase Storage upload error:', uploadError)
return Response.json(
{
error: 'Failed to upload generated image to storage',
},
{ status: 500 }
)
}
// Get the public URL for the uploaded image
const {
data: { publicUrl },
} = ctx.supabaseAdmin.storage.from('generated-images').getPublicUrl(outputImageName)
return Response.json({
success: true,
generatedImageUrl: publicUrl,
message: 'Image processed and stored successfully',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
} catch (error) {
console.error('Error processing image:', error)
return new Response(
JSON.stringify({
error: (error as Error)?.message || 'Unknown error occurred',
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
}
})
})
} catch (error) {
console.error('Error processing image:', error)
return Response.json(
{
error: (error as Error)?.message || 'Unknown error occurred',
},
{ status: 500 }
)
}
}),
}
@@ -1,23 +1,25 @@
import 'https://deno.land/x/xhr@0.3.0/mod.ts'
import { CreateCompletionRequest } from 'https://esm.sh/openai@3.1.0'
import { withSupabase } from 'npm:@supabase/server@^1'
Deno.serve(async (req) => {
const { query } = await req.json()
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req) => {
const { query } = await req.json()
const completionConfig: CreateCompletionRequest = {
model: 'text-davinci-003',
prompt: query,
max_tokens: 256,
temperature: 0,
stream: true,
}
const completionConfig = {
model: 'text-davinci-003',
prompt: query,
max_tokens: 256,
temperature: 0,
stream: true,
}
return fetch('https://api.openai.com/v1/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(completionConfig),
})
})
return fetch('https://api.openai.com/v1/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(completionConfig),
})
}),
}
@@ -2,8 +2,13 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { withSupabase } from 'npm:@supabase/server@^1'
import { handler } from './handler.tsx'
console.log(`Function "opengraph" up and running!`)
Deno.serve(handler)
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, handler),
}
@@ -1,29 +1,30 @@
import puppeteer from 'https://deno.land/x/puppeteer@16.2.0/mod.ts'
import puppeteer from 'npm:puppeteer@^25'
import { withSupabase } from 'npm:@supabase/server@^1'
Deno.serve(async (req) => {
try {
console.log(`wss://chrome.browserless.io?token=${Deno.env.get('PUPPETEER_BROWSERLESS_IO_KEY')}`)
// Visit browserless.io to get your free API token
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://chrome.browserless.io?token=${Deno.env.get(
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req) => {
try {
const browserWSEndpoint = `wss://chrome.browserless.io?token=${Deno.env.get(
'PUPPETEER_BROWSERLESS_IO_KEY'
)}`,
})
const page = await browser.newPage()
)}`
// Visit browserless.io to get your free API token
const browser = await puppeteer.connect({
browserWSEndpoint,
})
const page = await browser.newPage()
const url = new URL(req.url).searchParams.get('url') || 'http://www.example.com'
const url = new URL(req.url).searchParams.get('url') || 'http://www.example.com'
await page.goto(url)
const screenshot = await page.screenshot()
await page.goto(url)
const screenshot = await page.screenshot()
return new Response(screenshot, {
headers: { 'Content-Type': 'image/png' },
})
} catch (e) {
console.error(e)
return new Response(JSON.stringify({ error: e.message }), {
headers: { 'Content-Type': 'application/json' },
status: 500,
})
}
})
return new Response(screenshot as BodyInit, {
headers: { 'Content-Type': 'image/png' },
})
} catch (e) {
console.error(e)
return Response.json({ error: e.message }, { status: 500 })
}
}),
}
@@ -2,58 +2,26 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { createClient } 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',
// }
import { withSupabase } from 'npm:@supabase/server@^1'
Deno.serve(async (req) => {
// read a text file from storage and print its contents
// This is needed if you're planning to invoke your function from a browser.
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
// read a text file from storage and print its contents
try {
const { data, error } = await ctx.supabase.storage.from('my-bucket').download('sample.txt')
if (error) throw error
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 API 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')! },
},
}
)
// file contents are returned as a blob, we can convert it to utf-8 text by calling text() method.
const contents = await data.text()
const { data, error } = await supabaseClient.storage.from('my-bucket').download('sample.txt')
if (error) throw error
// prints out the contents of the file
console.log(contents)
// file contents are returned as a blob, we can convert it to utf-8 text by calling text() method.
const contents = await data.text()
return Response.json({ contents })
} catch (error) {
console.error(error)
// prints out the contents of the file
console.log(contents)
return new Response(JSON.stringify({ contents }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error) {
console.error(error)
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
return Response.json({ error: error.message }, { status: 400 })
}
}),
}
@@ -2,63 +2,27 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { createClient } from 'npm:supabase-js@2'
// New approach (v2.95.0+)
import { corsHeaders } from 'jsr:@supabase/supabase-js@2/cors'
// For older versions:
// import { corsHeaders } from '../_shared/cors.ts'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
Deno.serve(async (req: Request) => {
// This is needed if you're planning to invoke your function from a browser.
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
try {
// ctx.supabase runs queries as the authenticated user, so RLS applies.
// ctx.userClaims holds the verified user identity.
const { data, error } = await ctx.supabase.from('users').select('*')
if (error) throw error
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 API 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')! },
},
}
)
return Response.json({ user: ctx.userClaims, data })
} catch (error) {
return Response.json({ error: error.message }, { status: 400 })
}
}),
}
// First get the token from the Authorization header
const token = req.headers.get('Authorization').replace('Bearer ', '')
// Now we can get the session or user object
const {
data: { user },
} = await supabaseClient.auth.getUser(token)
// And we can run queries in the context of our authenticated user
const { data, error } = await supabaseClient.from('users').select('*')
if (error) throw error
return new Response(JSON.stringify({ user, 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,
})
}
})
// To invoke:
// To invoke (auth: 'user' requires a signed-in user's access token):
// curl -i --location --request POST 'http://localhost:54321/functions/v1/select-from-table-with-auth-rls' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'
@@ -1,28 +1,26 @@
import { withSupabase } from 'npm:@supabase/server@^1'
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
const handler = async (_request: Request): Promise<Response> => {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${RESEND_API_KEY}`,
},
body: JSON.stringify({
from: 'onboarding@resend.dev',
to: 'delivered@resend.dev',
subject: 'hello world',
html: '<strong>it works!</strong>',
}),
})
// Accepts a signed-in user's JWT or a secret key, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${RESEND_API_KEY}`,
},
body: JSON.stringify({
from: 'onboarding@resend.dev',
to: 'delivered@resend.dev',
subject: 'hello world',
html: '<strong>it works!</strong>',
}),
})
const data = await res.json()
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)
@@ -2,7 +2,8 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import nodemailer from 'npm:nodemailer@6.9.10'
import { withSupabase } from 'npm:@supabase/server@^1'
import nodemailer from 'npm:nodemailer@^9'
const transport = nodemailer.createTransport({
host: Deno.env.get('SMTP_HOSTNAME')!,
@@ -16,41 +17,39 @@ const transport = nodemailer.createTransport({
console.log(`Function "send-email-smtp" up and running!`)
Deno.serve(async (_req) => {
try {
await new Promise<void>((resolve, reject) => {
transport.sendMail(
{
from: Deno.env.get('SMTP_FROM')!,
to: 'testr@test.de',
subject: `Hello from Supabase Edge Functions`,
text: `Hello Functions \\o/`,
},
(error) => {
if (error) {
return reject(error)
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
try {
await new Promise<void>((resolve, reject) => {
transport.sendMail(
{
from: Deno.env.get('SMTP_FROM')!,
to: 'testr@test.de',
subject: `Hello from Supabase Edge Functions`,
text: `Hello Functions \\o/`,
},
(error) => {
if (error) {
return reject(error)
}
resolve()
}
resolve()
}
)
})
} catch (error) {
return new Response(error.message, { status: 500 })
}
return new Response(
JSON.stringify({
done: true,
}),
{
headers: { 'Content-Type': 'application/json' },
)
})
} catch (error) {
return Response.json({ error: error.message }, { status: 500 })
}
)
})
return Response.json({
done: true,
})
}),
}
// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/send-email-smtp' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'
@@ -1,16 +1,12 @@
import * as Sentry from 'npm:@sentry/deno@^10'
import { withSupabase } from 'npm:@supabase/server@^1'
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
console.log('Hello from the Sentry Functions Challenge!')
import { createClient } from 'npm:supabase-js@2'
import * as Sentry from 'https://deno.land/x/sentry@7.102.0/index.mjs'
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
Sentry.init({
dsn: Deno.env.get('SENTRY_DSN'),
integrations: [],
@@ -23,47 +19,47 @@ 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 {
if (req.method === 'GET') {
return new Response(
'What is Supabase not? POST {answer, twitter} to this URL! Need a hint? 👉 https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/sentry/index.ts'
)
}
let answer: string
let twitter: string
// Public challenge endpoint, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
try {
const params = await req.json()
if (!params?.answer || !params?.twitter) throw new Error('no answer')
answer = params.answer
twitter = params.twitter
} catch (_) {
return new Response('You need to send a JSON body with {answer, twitter}!')
}
if (answer.toLowerCase() !== 'postgres') {
return new Response(
`You are correct! But that means you've failed the challenge. Please try again!`
)
} else {
throw { twitter }
}
} catch (e) {
Sentry.captureException(e)
const { error } = await supabase
.from('sentry_functions_challenge')
.insert({ twitter: e.twitter })
if (error) console.log(e.twitter, error.message)
return new Response(
JSON.stringify({
msg: `Congrats, you are wrong https://itsjustpostgres.com/ 🎉 @${e.twitter} has been added to the draw!`,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
if (req.method === 'GET') {
return new Response(
'What is Supabase not? POST {answer, twitter} to this URL! Need a hint? 👉 https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/sentry/index.ts'
)
}
)
}
})
let answer: string
let twitter: string
try {
const params = await req.json()
if (!params?.answer || !params?.twitter) throw new Error('no answer')
answer = params.answer
twitter = params.twitter
} catch (_) {
return new Response('You need to send a JSON body with {answer, twitter}!')
}
if (answer.toLowerCase() !== 'postgres') {
return new Response(
`You are correct! But that means you've failed the challenge. Please try again!`
)
} else {
throw { twitter }
}
} catch (e) {
Sentry.captureException(e)
const { error } = await ctx.supabaseAdmin
.from('sentry_functions_challenge')
.insert({ twitter: e.twitter })
if (error) console.log(e.twitter, error.message)
return Response.json(
{
msg: `Congrats, you are wrong https://itsjustpostgres.com/ 🎉 @${e.twitter} has been added to the draw!`,
},
{ status: 500 }
)
}
}),
}
/* To invoke locally:
@@ -71,7 +67,6 @@ Deno.serve(async (req) => {
2. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/sentry' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'Content-Type: application/json' \
--data '{"answer":"mysql", "twitter":"thorwebdev"}'
@@ -1,32 +1,31 @@
import * as Sentry from 'https://deno.land/x/sentry/index.mjs'
import * as Sentry from 'npm:@sentry/deno@^10'
import { withSupabase } from 'npm:@supabase/server@^1'
Sentry.init({
dsn: SENTRY_DSN,
dsn: Deno.env.get('SENTRY_DSN'),
integrations: [],
debug: true,
// Performance Monitoring
tracesSampleRate: 1.0,
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 1.0,
})
// Set region and execution_id as custom tags
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()
const data = {
message: `Hello ${name}!`,
}
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
try {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } })
} catch (e) {
Sentry.captureException(e)
return new Response(JSON.stringify({ msg: 'error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
})
return Response.json(data)
} catch (e) {
Sentry.captureException(e)
return Response.json({ msg: 'error' }, { status: 500 })
}
}),
}
@@ -1,32 +1,35 @@
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') ?? ''
const botClient = new WebClient(slackBotToken)
console.log(`Slack URL verification function up and running!`)
Deno.serve(async (req) => {
try {
const { token, challenge, type, event } = await req.json()
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 })
// Public endpoint, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, async (req, ctx) => {
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 { challenge, type, event } = await req.json()
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:
await botClient.chat.postMessage({
channel: channel,
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 })
}
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { 'Content-Type': 'application/json' },
status: 500,
})
}
})
}),
}
@@ -1,24 +1,29 @@
import { withSupabase } from 'npm:@supabase/server@^1'
const msg = new TextEncoder().encode('data: hello\r\n\r\n')
Deno.serve((_) => {
let timerId: number | undefined
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, (req, ctx) => {
let timerId: number | undefined
const body = new ReadableStream({
start(controller) {
timerId = setInterval(() => {
controller.enqueue(msg)
}, 1000)
},
cancel() {
if (typeof timerId === 'number') {
clearInterval(timerId)
}
},
})
const body = new ReadableStream({
start(controller) {
timerId = setInterval(() => {
controller.enqueue(msg)
}, 1000)
},
cancel() {
if (typeof timerId === 'number') {
clearInterval(timerId)
}
},
})
return new Response(body, {
headers: {
'Content-Type': 'text/event-stream',
},
})
})
return new Response(body, {
headers: {
'Content-Type': 'text/event-stream',
},
})
}),
}
@@ -2,37 +2,36 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
// Import via bare specifier thanks to the import_map.json file.
import Stripe from 'https://esm.sh/stripe@14?target=denonext'
import Stripe from 'npm:stripe@^22'
import { withSupabase } from 'npm:@supabase/server@^1'
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string, {
// This is needed to use the Fetch API rather than relying on the Node http
// package.
apiVersion: '2024-11-20',
})
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string)
// This is needed in order to use the Web Crypto API in Deno.
const cryptoProvider = Stripe.createSubtleCryptoProvider()
console.log('Hello from Stripe Webhook!')
Deno.serve(async (request) => {
const signature = request.headers.get('Stripe-Signature')
// Stripe verifies the request via its signature, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, async (req) => {
const signature = req.headers.get('Stripe-Signature')
// First step is to verify the event. The .text() method must be used as the
// verification relies on the raw request body rather than the parsed JSON.
const body = await request.text()
let receivedEvent
try {
receivedEvent = await stripe.webhooks.constructEventAsync(
body,
signature!,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
undefined,
cryptoProvider
)
} catch (err) {
return new Response(err.message, { status: 400 })
}
console.log(`🔔 Event received: ${receivedEvent.id}`)
return new Response(JSON.stringify({ ok: true }), { status: 200 })
})
// First step is to verify the event. The .text() method must be used as the
// verification relies on the raw request body rather than the parsed JSON.
const body = await req.text()
let receivedEvent
try {
receivedEvent = await stripe.webhooks.constructEventAsync(
body,
signature!,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
undefined,
cryptoProvider
)
} catch (err) {
return new Response(err.message, { status: 400 })
}
console.log(`🔔 Event received: ${receivedEvent.id}`)
return Response.json({ ok: true })
}),
}
@@ -1,11 +1,12 @@
import { Bot, webhookCallback } from 'npm:grammy@^1'
import { withSupabase } from 'npm:@supabase/server@^1'
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
console.log(`Function "telegram-bot" up and running!`)
import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.8.3/mod.ts'
const bot = new Bot(Deno.env.get('TELEGRAM_BOT_TOKEN') || '')
bot.command('start', (ctx) => ctx.reply('Welcome! Up and running.'))
@@ -14,15 +15,19 @@ bot.command('ping', (ctx) => ctx.reply(`Pong! ${new Date()} ${Date.now()}`))
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 })
}
// Telegram is verified via the secret query param, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, 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 })
}
return await handleUpdate(req)
} catch (err) {
console.error(err)
}
})
return await handleUpdate(req)
} catch (err) {
console.error(err)
return new Response('Internal Server Error', { status: 500 })
}
}),
}
@@ -1,93 +0,0 @@
import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0'
import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'
import { getTweets } from './getTweet.ts'
import Tweet from './Tweet.tsx'
const STORAGE_URL =
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
// Load custom font
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
export async function handler(req: Request) {
const url = new URL(req.url)
const tweetId = url.searchParams.get('tweetId')
if (!tweetId)
return new Response(JSON.stringify({ error: 'missing tweetId param' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
try {
// Try to get image from Supabase Storage CDN.
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
if (storageResponse.ok) return storageResponse
// Else, generate image and upload to storage.
const fontData = await font
const tweets = await getTweets([tweetId])
const tweet = tweets[0]
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
const generatedImage = new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1c1c1c',
color: '#EDEDED',
}}
>
<Tweet {...tweet} />
</div>,
{
width: 1200,
height: 630,
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
// Default to 'twemoji'
emoji: 'twemoji',
fonts: [
{
name: 'Circular',
data: fontData,
style: 'normal',
},
],
}
)
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabaseAdminClient = createClient(
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Upload image to storage.
const { error } = await supabaseAdminClient.storage
.from('images')
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
contentType: 'image/png',
cacheControl: '31536000',
upsert: false,
})
if (error) throw error
return generatedImage
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
}
@@ -2,8 +2,89 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { handler } from './handler.tsx'
import { ImageResponse } from 'npm:@vercel/og@^0'
import React from 'npm:react@19'
import { withSupabase } from 'npm:@supabase/server@^1'
import { getTweets } from './getTweet.ts'
import Tweet from './Tweet.tsx'
const STORAGE_URL =
'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/tweet-to-image'
// Load custom font
const FONT_URL = `${STORAGE_URL}/CircularStd-Book.otf`
const font = fetch(new URL(FONT_URL, import.meta.url)).then((res) => res.arrayBuffer())
console.log(`Function "tweet-to-image" up and running!`)
Deno.serve(handler)
async function handler(req: Request, ctx) {
const url = new URL(req.url)
const tweetId = url.searchParams.get('tweetId')
if (!tweetId) return Response.json({ error: 'missing tweetId param' }, { status: 400 })
try {
// Try to get image from Supabase Storage CDN.
const storageResponse = await fetch(`${STORAGE_URL}/${tweetId}.png`)
if (storageResponse.ok) return storageResponse
// Else, generate image and upload to storage.
const fontData = await font
const tweets = await getTweets([tweetId])
const tweet = tweets[0]
console.log('formattedTweets', JSON.stringify(tweets, null, 2))
const generatedImage = new ImageResponse(
React.createElement(
'div',
{
style: {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1c1c1c',
color: '#EDEDED',
},
},
React.createElement(Tweet, { ...tweet })
),
{
width: 1200,
height: 630,
// Supported options: 'twemoji', 'blobmoji', 'noto', 'openmoji', 'fluent', 'fluentFlat'
// Default to 'twemoji'
emoji: 'twemoji',
fonts: [
{
name: 'Circular',
data: fontData,
style: 'normal',
},
],
}
)
// Upload image to storage.
const { error } = await ctx.supabaseAdmin.storage
.from('images')
.upload(`tweet-to-image/${tweetId}.png`, generatedImage.body!, {
contentType: 'image/png',
cacheControl: '31536000',
upsert: false,
})
if (error) throw error
return generatedImage
} catch (error) {
return Response.json({ error: error.message }, { status: 400 })
}
}
// Uploads to Storage with a secret key, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'secret' }, handler),
}
@@ -2,47 +2,51 @@
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
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!`)
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')!,
})
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, 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)
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<string, number> | 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 })
}
}),
}
// Get all values
const counterHash: Record<string, number> | 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 new Response(JSON.stringify({ counters }), { status: 200 })
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 200,
})
}
})
// To invoke, navigate to 'http://localhost:54321/functions/v1/upstash-redis-counter'.
// To invoke:
// curl -i --location --request GET 'http://localhost:54321/functions/v1/upstash-redis-counter' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>'
@@ -1,67 +1,41 @@
import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts'
import { Ratelimit } from 'https://cdn.skypack.dev/@upstash/ratelimit@0.4.4'
import { createClient } from 'npm:supabase-js@2'
import { Ratelimit } from 'npm:@upstash/ratelimit@^2'
import { Redis } from 'npm:@upstash/redis@^1'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log(`Function "upstash-redis-counter" up and running!`)
Deno.serve(async (req) => {
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')! },
},
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
try {
const redis = new Redis({
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
})
// Create a new ratelimiter, that allows 10 requests per 10 seconds
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(2, '10 s'),
analytics: true,
})
// Use a constant string to limit all requests with a single ratelimit
// Or use a userID, apiKey or ip address for individual limits.
const identifier = ctx.userClaims?.id
const { success } = await ratelimit.limit(identifier)
if (!success) {
throw new Error('limit exceeded')
}
)
// First get the token from the Authorization header
const token = req.headers.get('Authorization').replace('Bearer ', '')
// Now we can get the session or user object
const {
data: { user },
} = await supabaseClient.auth.getUser(token)
if (!user) throw new Error('no user')
console.log(user.id)
const redis = new Redis({
url: Deno.env.get('UPSTASH_REDIS_REST_URL')!,
token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!,
})
// Create a new ratelimiter, that allows 10 requests per 10 seconds
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(2, '10 s'),
analytics: true,
})
// Use a constant string to limit all requests with a single ratelimit
// Or use a userID, apiKey or ip address for individual limits.
const identifier = user.id
const { success } = await ratelimit.limit(identifier)
if (!success) {
throw new Error('limit exceeded')
return Response.json({ success })
} catch (error) {
return Response.json({ error: error.message })
}
return new Response(JSON.stringify({ success }), { status: 200 })
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 200,
})
}
})
}),
}
// curl -i --location --request POST 'http://localhost:54321/functions/v1/upstash-redis-ratelimit' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc4MDA4Mjk2LCJzdWIiOiJkZjQ3ZDU5My0zMjY1LTQ2OWEtOWI5OS1mZDk1OTdhOGU4YzciLCJlbWFpbCI6InRlc3RyQHRlc3Quc2ciLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3ODAwNDY5Nn1dLCJzZXNzaW9uX2lkIjoiMDNjMmFlYjUtMjk5MC00ZWU0LWIxYTYtZmU1Y2IwMGFhMjU3In0.WwqggrX34j0NINiulC9rsj-cd6HzV55ySxJkJlVsU4o' \
// --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'
@@ -1,8 +1,11 @@
import { withSupabase } from 'npm:@supabase/server@^1'
import { add } from './add-wasm/pkg/add_wasm.js'
Deno.serve(async (req) => {
const { a, b } = await req.json()
return new Response(JSON.stringify({ result: add(a, b) }), {
headers: { 'Content-Type': 'application/json' },
})
})
// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
fetch: withSupabase({ auth: 'user' }, async (req) => {
const { a, b } = await req.json()
return Response.json({ result: add(a, b) })
}),
}
@@ -1,32 +1,24 @@
import { withSupabase } from 'npm:@supabase/server@^1'
const ALLOWED_ORIGINS = ['http://localhost:8000']
const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.join(','),
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type, range, if-match',
'Access-Control-Expose-Headers': 'range, accept-ranges, etag',
'Access-Control-Max-Age': '300',
// Public tile proxy, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'none' }, (req) => {
// Restrict which origins may read the private tiles.
const origin = req.headers.get('Origin')
if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
return new Response('Not Allowed', { status: 405 })
}
const reqUrl = new URL(req.url)
const url = `${Deno.env.get('SUPABASE_URL')}/storage/v1/object/authenticated${reqUrl.pathname}`
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const { method, headers } = req
// Add Auth header so Storage serves the private object.
const modHeaders = new Headers(headers)
modHeaders.append('authorization', `Bearer ${SUPABASE_SECRET_KEYS['default']!}`)
return fetch(url, { method, headers: modHeaders })
}),
}
Deno.serve((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 })
}
// Check origin
const origin = req.headers.get('Origin')
if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
return new Response('Not Allowed', { status: 405 })
}
const reqUrl = new URL(req.url)
const url = `${Deno.env.get('SUPABASE_URL')}/storage/v1/object/authenticated${reqUrl.pathname}`
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const { method, headers } = req
// Add Auth header
const modHeaders = new Headers(headers)
modHeaders.append('authorization', `Bearer ${SUPABASE_SECRET_KEYS['default']!}`)
return fetch(url, { method, headers: modHeaders })
})
@@ -22,7 +22,7 @@ public = true
[functions.create-upload-token]
enabled = true
verify_jwt = true
verify_jwt = false
import_map = "./functions/create-upload-token/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
@@ -1,31 +1,27 @@
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'jsr:@supabase/supabase-js'
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
import { withSupabase } from 'npm:@supabase/server@^1'
Deno.serve(async (req) => {
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? ''
const SUPABASE_SECRET_KEY = SUPABASE_SECRET_KEYS['default'] ?? ''
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => {
try {
const { filename } = await req.json()
if (!filename) {
return Response.json({ error: 'Missing filename' }, { status: 400 })
}
const supabase = createClient(SUPABASE_URL, SUPABASE_SECRET_KEY)
const { data, error } = await ctx.supabaseAdmin.storage
.from('uploads')
.createSignedUploadUrl(filename)
try {
const { filename } = await req.json()
if (!filename) {
return new Response('Missing filename', { status: 400 })
if (error) {
return Response.json({ error: error.message }, { status: 500 })
}
return Response.json({ token: data.token })
} catch (error) {
return Response.json({ error: (error as Error).message }, { status: 500 })
}
const { data, error } = await supabase.storage.from('uploads').createSignedUploadUrl(filename)
if (error) {
return new Response(error.message, { status: 500 })
}
return new Response(JSON.stringify({ token: data.token }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
return new Response((error as Error).message, { status: 500 })
}
})
}),
}
@@ -15,6 +15,10 @@ extra_search_path = ["public", "extensions"]
# for accidental or malicious requests.
max_rows = 1000
# Triggered by a Database Webhook with a secret key, so skip the platform JWT check.
[functions.push]
verify_jwt = false
[db]
# Port to use for the local database URL.
port = 54322
@@ -1,7 +1,7 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { withSupabase } from 'npm:@supabase/server@^1'
console.log('Hello from Functions!')
@@ -19,37 +19,39 @@ interface WebhookPayload {
old_record: null | Notification
}
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase<any>({ 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()
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()
if (!data?.expo_push_token) {
return Response.json({ error: 'Expo push token not found' }, { status: 404 })
}
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())
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)
}),
}
// To invoke:
// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \
// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
// curl -i --location --request POST 'http://localhost:54321/functions/v1/push' \
// --header 'apikey: <SUPABASE_SECRET_KEY>' \
// --header 'Content-Type: application/json' \
// --data '{"name":"Functions"}'