├── .github └── workflows │ └── deploy.yaml ├── .vscode └── settings.json ├── README.md └── supabase ├── .gitignore ├── config.toml ├── functions ├── _shared │ └── cors.ts ├── cloudflare-turnstile │ └── index.ts ├── database-webhook │ ├── index.ts │ └── types.ts ├── discord-bot │ └── index.ts ├── hello │ └── index.ts ├── hf-image-captioning │ ├── index.ts │ └── types.ts ├── hf-inference │ └── index.ts ├── import_map.json ├── kysely-postgres │ ├── DenoPostgresDriver.ts │ ├── README.md │ └── index.ts ├── oak-server │ ├── README.md │ └── index.ts ├── og-image-storage-cdn │ ├── handler.tsx │ └── index.ts ├── og-image │ ├── handler.tsx │ └── index.ts ├── postgres-on-the-edge │ ├── README.md │ └── index.ts ├── resend │ ├── index.ts │ └── types.ts ├── screenshot │ └── index.ts ├── stripe-webhook │ ├── .env.example │ ├── README.md │ └── index.ts ├── telegram-bot │ ├── README.md │ └── index.ts ├── upstash-redis-counter │ └── index.ts ├── upstash-redis-ratelimit │ └── index.ts └── vercel-ai-openai-completion │ ├── README.md │ └── index.ts └── seed.sql /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Function 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} 15 | PROJECT_ID: bljghubhkofddfrezkhn 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: supabase/setup-cli@v1 21 | with: 22 | version: latest 23 | 24 | - run: supabase functions deploy --project-ref $PROJECT_ID --debug 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.unstable": true, 4 | "deno.importMap": "./supabase/functions/import_map.json" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Edgy Edge Functions - a Supabase Series](https://youtube.com/playlist?list=PL5S4mPUpp4OulD3olUW8Eq1IYKpUbk5Ob) 2 | 3 | Edgy Edge Functions is a video series where we explore [Supabase Edge Functions](https://supabase.com/edge-functions) and [Deno](https://deno.land/) functionality and features. 4 | 5 | If there's something you'd like to learn about, please [open an issue](https://github.com/thorwebdev/edgy-edge-functions/issues/new/choose) and let me know on [this Tweet thread](https://twitter.com/thorwebdev/status/1595719098863788032). 6 | 7 | Thanks for stopping by! Denosaur and I will see you soon \o/ 8 | 9 | Find more [Supabase Edge Functions Examples here](https://supabase.com/docs/guides/functions/examples)! 10 | 11 | ## Episodes 12 | 13 | 1. [ [Video](https://youtu.be/jZgyOJGWayQ) | [Code](./supabase/functions/og-image/) ] Generating OG Images 14 | 1. [ [Video](https://www.youtube.com/watch?v=wW6L52v9Ldo) | [Code](./supabase/functions/og-image-storage-cdn/) ] Caching OG Images with Storage CDN 15 | 1. [ [Video](https://www.youtube.com/watch?v=l2KlzGrhB6w) | [Code](./.github/workflows/deploy.yaml) ] Deploying from GitHub Actions 16 | 1. [ [Video](https://www.youtube.com/watch?v=6OMVWiiycLs) | [Code](./supabase/functions/stripe-webhook/) ] Handling Stripe Webhooks 17 | 1. [ [Video](https://www.youtube.com/watch?v=AWfE3a9J_uo) | [Code](./supabase/functions/telegram-bot/) ] Building a Telegram Bot 18 | 1. [ [Video](https://www.youtube.com/watch?v=OwW0znboh60) | [Code](./supabase/functions/cloudflare-turnstile/) ] Better CAPTCHA with Cloudflare Turnstile 19 | 1. [ [Video](https://www.youtube.com/watch?v=J24Bvo_m7DM) | [Code](./supabase/functions/discord-bot/) ] Creating Discord Slash Commands 20 | 1. [ [Video](https://www.youtube.com/watch?v=ILr3cneZuFk) | [Code](./supabase/functions/import_map.json) ] Import Maps 21 | 1. [ [Video](https://www.youtube.com/watch?v=cl7EuF1-RsY) | [Code](./supabase/functions/postgres-on-the-edge/) ] Postgres on the Edge 22 | 1. [ [Video](https://www.youtube.com/watch?v=-U6DJcjVvGo) | [Docs](https://supabase.com/docs/guides/functions/schedule-functions) ] Scheduling Functions 23 | 1. [ [Video](https://youtu.be/F505n0d9iuA) | [Code](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/postgres-on-the-edge) ] Caching Postgres on the Edge with PolyScale.ai 24 | 1. [ [Video](https://youtu.be/29p8kIqyU_Y) | [Code](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/openai) ] OpenAI with Edge Functions 25 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "edgy-edge-functions" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = [] 11 | # Extra schemas to add to the search_path of every request. 12 | extra_search_path = ["extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 14 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`. 63 | [auth.external.apple] 64 | enabled = false 65 | client_id = "" 66 | secret = "" 67 | # Overrides the default auth redirectUrl. 68 | redirect_uri = "" 69 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 70 | # or any other third-party OIDC providers. 71 | url = "" 72 | 73 | [functions.og-image] 74 | verify_jwt = false 75 | -------------------------------------------------------------------------------- /supabase/functions/_shared/cors.ts: -------------------------------------------------------------------------------- 1 | export const corsHeaders = { 2 | "Access-Control-Allow-Origin": "*", 3 | "Access-Control-Allow-Headers": 4 | "authorization, x-client-info, apikey, content-type", 5 | }; 6 | -------------------------------------------------------------------------------- /supabase/functions/cloudflare-turnstile/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | import { corsHeaders } from "../_shared/cors.ts"; 7 | 8 | console.log("Hello from Cloudflare Trunstile!"); 9 | 10 | function ips(req: Request) { 11 | return req.headers.get("x-forwarded-for")?.split(/\s*,\s*/); 12 | } 13 | 14 | serve(async (req) => { 15 | // This is needed if you're planning to invoke your function from a browser. 16 | if (req.method === "OPTIONS") { 17 | return new Response("ok", { headers: corsHeaders }); 18 | } 19 | 20 | const { token } = await req.json(); 21 | const clientIps = ips(req) || [""]; 22 | const ip = clientIps[0]; 23 | 24 | // Validate the token by calling the 25 | // "/siteverify" API endpoint. 26 | let formData = new FormData(); 27 | formData.append("secret", Deno.env.get("CLOUDFLARE_SECRET_KEY") ?? ""); 28 | formData.append("response", token); 29 | formData.append("remoteip", ip); 30 | 31 | const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; 32 | const result = await fetch(url, { 33 | body: formData, 34 | method: "POST", 35 | }); 36 | 37 | const outcome = await result.json(); 38 | console.log(outcome); 39 | if (outcome.success) { 40 | return new Response("success", { headers: corsHeaders }); 41 | } 42 | return new Response("failure", { headers: corsHeaders }); 43 | }); 44 | 45 | // To invoke: 46 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 47 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 48 | // --header 'Content-Type: application/json' \ 49 | // --data '{"name":"Functions"}' 50 | -------------------------------------------------------------------------------- /supabase/functions/database-webhook/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; 6 | import { Database } from "./types.ts"; 7 | 8 | console.log("Hello from `database-webhook` function!"); 9 | 10 | type AnimalRecord = Database["public"]["Tables"]["animals"]["Row"]; 11 | interface WebhookPayload { 12 | type: "INSERT" | "UPDATE" | "DELETE"; 13 | table: string; 14 | record: AnimalRecord; 15 | schema: "public"; 16 | old_record: null | AnimalRecord; 17 | } 18 | 19 | serve(async (req) => { 20 | const payload: WebhookPayload = await req.json(); 21 | console.log(payload.record.animal); 22 | 23 | return new Response("ok"); 24 | }); 25 | 26 | // To invoke: 27 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 28 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 29 | // --header 'Content-Type: application/json' \ 30 | // --data '{"name":"Functions"}' 31 | -------------------------------------------------------------------------------- /supabase/functions/database-webhook/types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[] 8 | 9 | export interface Database { 10 | public: { 11 | Tables: { 12 | animals: { 13 | Row: { 14 | animal: string | null 15 | created_at: string | null 16 | id: number 17 | } 18 | Insert: { 19 | animal?: string | null 20 | created_at?: string | null 21 | id?: number 22 | } 23 | Update: { 24 | animal?: string | null 25 | created_at?: string | null 26 | id?: number 27 | } 28 | } 29 | discord_promise_challenge: { 30 | Row: { 31 | email: string | null 32 | id: number 33 | inserted_at: string 34 | promise: string 35 | resolved: boolean | null 36 | submission: string | null 37 | updated_at: string 38 | user_id: string 39 | username: string 40 | } 41 | Insert: { 42 | email?: string | null 43 | id?: number 44 | inserted_at?: string 45 | promise: string 46 | resolved?: boolean | null 47 | submission?: string | null 48 | updated_at?: string 49 | user_id: string 50 | username: string 51 | } 52 | Update: { 53 | email?: string | null 54 | id?: number 55 | inserted_at?: string 56 | promise?: string 57 | resolved?: boolean | null 58 | submission?: string | null 59 | updated_at?: string 60 | user_id?: string 61 | username?: string 62 | } 63 | } 64 | kysely_test: { 65 | Row: { 66 | created_at: string | null 67 | id: number 68 | test: string | null 69 | } 70 | Insert: { 71 | created_at?: string | null 72 | id?: number 73 | test?: string | null 74 | } 75 | Update: { 76 | created_at?: string | null 77 | id?: number 78 | test?: string | null 79 | } 80 | } 81 | } 82 | Views: { 83 | [_ in never]: never 84 | } 85 | Functions: { 86 | install_available_extensions_and_test: { 87 | Args: Record 88 | Returns: boolean 89 | } 90 | } 91 | Enums: { 92 | [_ in never]: never 93 | } 94 | CompositeTypes: { 95 | [_ in never]: never 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /supabase/functions/discord-bot/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | // Sift is a small routing library that abstracts away details like starting a 6 | // listener on a port, and provides a simple function (serve) that has an API 7 | // to invoke a function for a specific path. 8 | import { json, serve, validateRequest } from "sift"; 9 | // TweetNaCl is a cryptography library that we use to verify requests 10 | // from Discord. 11 | import nacl from "https://cdn.skypack.dev/tweetnacl@v1.0.3?dts"; 12 | 13 | // For all requests to "/" endpoint, we want to invoke home() handler. 14 | serve({ 15 | "/discord-bot": home, 16 | }); 17 | 18 | // The main logic of the Discord Slash Command is defined in this function. 19 | async function home(request: Request) { 20 | // validateRequest() ensures that a request is of POST method and 21 | // has the following headers. 22 | const { error } = await validateRequest(request, { 23 | POST: { 24 | headers: ["X-Signature-Ed25519", "X-Signature-Timestamp"], 25 | }, 26 | }); 27 | if (error) { 28 | return json({ error: error.message }, { status: error.status }); 29 | } 30 | 31 | // verifySignature() verifies if the request is coming from Discord. 32 | // When the request's signature is not valid, we return a 401 and this is 33 | // important as Discord sends invalid requests to test our verification. 34 | const { valid, body } = await verifySignature(request); 35 | if (!valid) { 36 | return json( 37 | { error: "Invalid request" }, 38 | { 39 | status: 401, 40 | } 41 | ); 42 | } 43 | 44 | const { type = 0, data = { options: [] } } = JSON.parse(body); 45 | // Discord performs Ping interactions to test our application. 46 | // Type 1 in a request implies a Ping interaction. 47 | if (type === 1) { 48 | return json({ 49 | type: 1, // Type 1 in a response is a Pong interaction response type. 50 | }); 51 | } 52 | 53 | // Type 2 in a request is an ApplicationCommand interaction. 54 | // It implies that a user has issued a command. 55 | if (type === 2) { 56 | const { value } = data.options.find( 57 | (option: { name: string; value: string }) => option.name === "name" 58 | ); 59 | return json({ 60 | // Type 4 responds with the below message retaining the user's 61 | // input at the top. 62 | type: 4, 63 | data: { 64 | content: `Hello, ${value}!`, 65 | }, 66 | }); 67 | } 68 | 69 | // We will return a bad request error as a valid Discord request 70 | // shouldn't reach here. 71 | return json({ error: "bad request" }, { status: 400 }); 72 | } 73 | 74 | /** Verify whether the request is coming from Discord. */ 75 | async function verifySignature( 76 | request: Request 77 | ): Promise<{ valid: boolean; body: string }> { 78 | const PUBLIC_KEY = Deno.env.get("DISCORD_PUBLIC_KEY")!; 79 | // Discord sends these headers with every request. 80 | const signature = request.headers.get("X-Signature-Ed25519")!; 81 | const timestamp = request.headers.get("X-Signature-Timestamp")!; 82 | const body = await request.text(); 83 | const valid = nacl.sign.detached.verify( 84 | new TextEncoder().encode(timestamp + body), 85 | hexToUint8Array(signature), 86 | hexToUint8Array(PUBLIC_KEY) 87 | ); 88 | 89 | return { valid, body }; 90 | } 91 | 92 | /** Converts a hexadecimal string to Uint8Array. */ 93 | function hexToUint8Array(hex: string) { 94 | return new Uint8Array(hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16))); 95 | } 96 | -------------------------------------------------------------------------------- /supabase/functions/hello/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | 7 | console.log("Hello from Functions!"); 8 | 9 | serve(async (req) => { 10 | const { name } = await req.json(); 11 | const data = { 12 | message: `Hello ${name}!`, 13 | }; 14 | 15 | return new Response(JSON.stringify(data), { 16 | headers: { "Content-Type": "application/json" }, 17 | }); 18 | }); 19 | 20 | // To invoke: 21 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 22 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 23 | // --header 'Content-Type: application/json' \ 24 | // --data '{"name":"Functions"}' 25 | -------------------------------------------------------------------------------- /supabase/functions/hf-image-captioning/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; 6 | import { HfInference } from "https://esm.sh/@huggingface/inference@2.3.2"; 7 | import { createClient } from "@supabase/supabase-js"; 8 | import { Database } from "./types.ts"; 9 | 10 | console.log("Hello from `hf-image-captioning` function!"); 11 | 12 | const hf = new HfInference(Deno.env.get("HUGGINGFACE_ACCESS_TOKEN")); 13 | 14 | type SoRecord = Database["storage"]["Tables"]["objects"]["Row"]; 15 | interface WebhookPayload { 16 | type: "INSERT" | "UPDATE" | "DELETE"; 17 | table: string; 18 | record: SoRecord; 19 | schema: "public"; 20 | old_record: null | SoRecord; 21 | } 22 | 23 | serve(async (req) => { 24 | const payload: WebhookPayload = await req.json(); 25 | const soRecord = payload.record; 26 | const supabaseAdminClient = createClient( 27 | // Supabase API URL - env var exported by default when deployed. 28 | Deno.env.get("SUPABASE_URL") ?? "", 29 | // Supabase API SERVICE ROLE KEY - env var exported by default when deployed. 30 | Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "" 31 | ); 32 | 33 | // Construct image url from storage 34 | const { data, error } = await supabaseAdminClient.storage 35 | .from(soRecord.bucket_id!) 36 | .createSignedUrl(soRecord.path_tokens!.join("/"), 60); 37 | if (error) throw error; 38 | const { signedUrl } = data; 39 | 40 | // Run image captioning with Huggingface 41 | const imgDesc = await hf.imageToText({ 42 | data: await (await fetch(signedUrl)).blob(), 43 | model: "nlpconnect/vit-gpt2-image-captioning", 44 | }); 45 | 46 | // Store image caption in Database table 47 | await supabaseAdminClient 48 | .from("image_caption") 49 | .insert({ id: soRecord.id!, caption: imgDesc.generated_text }) 50 | .throwOnError(); 51 | 52 | return new Response("ok"); 53 | }); 54 | 55 | // To invoke: 56 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 57 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 58 | // --header 'Content-Type: application/json' \ 59 | // --data '{"name":"Functions"}' 60 | -------------------------------------------------------------------------------- /supabase/functions/hf-image-captioning/types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[]; 8 | 9 | export interface Database { 10 | public: { 11 | Tables: { 12 | image_caption: { 13 | Row: { 14 | caption: string; 15 | id: string; 16 | }; 17 | Insert: { 18 | caption: string; 19 | id: string; 20 | }; 21 | Update: { 22 | caption?: string; 23 | id?: string; 24 | }; 25 | }; 26 | }; 27 | Views: { 28 | [_ in never]: never; 29 | }; 30 | Functions: { 31 | [_ in never]: never; 32 | }; 33 | Enums: { 34 | [_ in never]: never; 35 | }; 36 | CompositeTypes: { 37 | [_ in never]: never; 38 | }; 39 | }; 40 | storage: { 41 | Tables: { 42 | buckets: { 43 | Row: { 44 | allowed_mime_types: string[] | null; 45 | avif_autodetection: boolean | null; 46 | created_at: string | null; 47 | file_size_limit: number | null; 48 | id: string; 49 | name: string; 50 | owner: string | null; 51 | public: boolean | null; 52 | updated_at: string | null; 53 | }; 54 | Insert: { 55 | allowed_mime_types?: string[] | null; 56 | avif_autodetection?: boolean | null; 57 | created_at?: string | null; 58 | file_size_limit?: number | null; 59 | id: string; 60 | name: string; 61 | owner?: string | null; 62 | public?: boolean | null; 63 | updated_at?: string | null; 64 | }; 65 | Update: { 66 | allowed_mime_types?: string[] | null; 67 | avif_autodetection?: boolean | null; 68 | created_at?: string | null; 69 | file_size_limit?: number | null; 70 | id?: string; 71 | name?: string; 72 | owner?: string | null; 73 | public?: boolean | null; 74 | updated_at?: string | null; 75 | }; 76 | }; 77 | migrations: { 78 | Row: { 79 | executed_at: string | null; 80 | hash: string; 81 | id: number; 82 | name: string; 83 | }; 84 | Insert: { 85 | executed_at?: string | null; 86 | hash: string; 87 | id: number; 88 | name: string; 89 | }; 90 | Update: { 91 | executed_at?: string | null; 92 | hash?: string; 93 | id?: number; 94 | name?: string; 95 | }; 96 | }; 97 | objects: { 98 | Row: { 99 | bucket_id: string | null; 100 | created_at: string | null; 101 | id: string; 102 | last_accessed_at: string | null; 103 | metadata: Json | null; 104 | name: string | null; 105 | owner: string | null; 106 | path_tokens: string[] | null; 107 | updated_at: string | null; 108 | version: string | null; 109 | }; 110 | Insert: { 111 | bucket_id?: string | null; 112 | created_at?: string | null; 113 | id?: string; 114 | last_accessed_at?: string | null; 115 | metadata?: Json | null; 116 | name?: string | null; 117 | owner?: string | null; 118 | path_tokens?: string[] | null; 119 | updated_at?: string | null; 120 | version?: string | null; 121 | }; 122 | Update: { 123 | bucket_id?: string | null; 124 | created_at?: string | null; 125 | id?: string; 126 | last_accessed_at?: string | null; 127 | metadata?: Json | null; 128 | name?: string | null; 129 | owner?: string | null; 130 | path_tokens?: string[] | null; 131 | updated_at?: string | null; 132 | version?: string | null; 133 | }; 134 | }; 135 | }; 136 | Views: { 137 | [_ in never]: never; 138 | }; 139 | Functions: { 140 | can_insert_object: { 141 | Args: { 142 | bucketid: string; 143 | name: string; 144 | owner: string; 145 | metadata: Json; 146 | }; 147 | Returns: undefined; 148 | }; 149 | extension: { 150 | Args: { 151 | name: string; 152 | }; 153 | Returns: string; 154 | }; 155 | filename: { 156 | Args: { 157 | name: string; 158 | }; 159 | Returns: string; 160 | }; 161 | foldername: { 162 | Args: { 163 | name: string; 164 | }; 165 | Returns: unknown; 166 | }; 167 | get_size_by_bucket: { 168 | Args: Record; 169 | Returns: { 170 | size: number; 171 | bucket_id: string; 172 | }[]; 173 | }; 174 | search: { 175 | Args: { 176 | prefix: string; 177 | bucketname: string; 178 | limits?: number; 179 | levels?: number; 180 | offsets?: number; 181 | search?: string; 182 | sortcolumn?: string; 183 | sortorder?: string; 184 | }; 185 | Returns: { 186 | name: string; 187 | id: string; 188 | updated_at: string; 189 | created_at: string; 190 | last_accessed_at: string; 191 | metadata: Json; 192 | }[]; 193 | }; 194 | }; 195 | Enums: { 196 | [_ in never]: never; 197 | }; 198 | CompositeTypes: { 199 | [_ in never]: never; 200 | }; 201 | }; 202 | } 203 | -------------------------------------------------------------------------------- /supabase/functions/hf-inference/index.ts: -------------------------------------------------------------------------------- 1 | import { HfInference } from "https://esm.sh/@huggingface/inference@2.3.2"; 2 | 3 | console.log("Hello from `hf-inference`!"); 4 | 5 | // Get your API token from 6 | const hf = new HfInference(Deno.env.get("HUGGINGFACE_ACCESS_TOKEN")); 7 | 8 | import { Application, Router } from "oak"; 9 | 10 | const router = new Router(); 11 | router 12 | // Note: path should be prefixed with function name 13 | .get("/hf-inference", (context) => { 14 | context.response.body = `Huggingface.js playground in Deno Edge Functions. Supported routes: ["/translation", "/textToSpeech", "/textToImage", '/imageToText']`; 15 | }) 16 | .get("/hf-inference/translation", async (context) => { 17 | console.log("Running `translation` with `t5-base` model."); 18 | const res = await hf.translation({ 19 | model: "t5-base", 20 | inputs: "My name is Wolfgang and I live in Berlin", 21 | }); 22 | 23 | context.response.body = res.translation_text; 24 | }) 25 | .get("/hf-inference/textToSpeech", async (context) => { 26 | console.log( 27 | "Running `textToSpeech` with `espnet/kan-bayashi_ljspeech_vits` model." 28 | ); 29 | const blob: Blob = await hf.textToSpeech({ 30 | model: "espnet/kan-bayashi_ljspeech_vits", 31 | inputs: "hello from supabase edge functions!", 32 | }); 33 | 34 | context.response.body = blob; 35 | context.response.headers.set("Content-Type", "audio/wav"); 36 | }) 37 | .get("/hf-inference/textToImage", async (context) => { 38 | console.log( 39 | "Running `textToImage` with `stabilityai/stable-diffusion-2` model." 40 | ); 41 | const blob: Blob = await hf.textToImage({ 42 | model: "stabilityai/stable-diffusion-2", 43 | inputs: "Postgres, the most powerful database in the world!", 44 | parameters: { 45 | negative_prompt: "blurry", 46 | }, 47 | }); 48 | 49 | context.response.body = blob; 50 | context.response.headers.set("Content-Type", "image/png"); 51 | }) 52 | .get("/hf-inference/imageToText", async (context) => { 53 | console.log( 54 | "Running `imageToText` with `nlpconnect/vit-gpt2-image-captioning` model." 55 | ); 56 | const imgDesc = await hf.imageToText({ 57 | data: await (await fetch("https://picsum.photos/300/300")).blob(), 58 | model: "nlpconnect/vit-gpt2-image-captioning", 59 | }); 60 | 61 | context.response.body = imgDesc.generated_text; 62 | }); 63 | 64 | const app = new Application(); 65 | app.use(router.routes()); 66 | app.use(router.allowedMethods()); 67 | 68 | await app.listen({ port: 8000 }); 69 | -------------------------------------------------------------------------------- /supabase/functions/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "og_edge": "https://deno.land/x/og_edge@0.0.4/mod.ts", 4 | "oak": "https://deno.land/x/oak@v11.1.0/mod.ts", 5 | "grammy": "https://deno.land/x/grammy@v1.8.3/mod.ts", 6 | "react": "https://esm.sh/react@18.2.0", 7 | "std/server": "https://deno.land/std@0.198.0/http/server.ts", 8 | "stripe": "https://esm.sh/stripe@11.1.0?target=deno", 9 | "sift": "https://deno.land/x/sift@0.6.0/mod.ts", 10 | "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.32.0", 11 | "postgres": "https://deno.land/x/postgres@v0.14.2/mod.ts", 12 | "@upstash/redis": "https://deno.land/x/upstash_redis@v1.19.3/mod.ts", 13 | "@upstash/ratelimit": "https://cdn.skypack.dev/@upstash/ratelimit@latest", 14 | "puppeteer": "https://deno.land/x/puppeteer@16.2.0/mod.ts" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /supabase/functions/kysely-postgres/DenoPostgresDriver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompiledQuery, 3 | DatabaseConnection, 4 | Driver, 5 | PostgresCursorConstructor, 6 | QueryResult, 7 | TransactionSettings, 8 | } from "https://esm.sh/kysely@0.23.4"; 9 | import { 10 | freeze, 11 | isFunction, 12 | } from "https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js"; 13 | import { extendStackTrace } from "https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js"; 14 | import { Pool, PoolClient } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; 15 | 16 | export interface PostgresDialectConfig { 17 | pool: Pool | (() => Promise); 18 | cursor?: PostgresCursorConstructor; 19 | onCreateConnection?: (connection: DatabaseConnection) => Promise; 20 | } 21 | 22 | const PRIVATE_RELEASE_METHOD = Symbol(); 23 | 24 | export class PostgresDriver implements Driver { 25 | readonly #config: PostgresDialectConfig; 26 | readonly #connections = new WeakMap(); 27 | #pool?: Pool; 28 | 29 | constructor(config: PostgresDialectConfig) { 30 | this.#config = freeze({ ...config }); 31 | } 32 | 33 | async init(): Promise { 34 | this.#pool = isFunction(this.#config.pool) 35 | ? await this.#config.pool() 36 | : this.#config.pool; 37 | } 38 | 39 | async acquireConnection(): Promise { 40 | const client = await this.#pool!.connect(); 41 | let connection = this.#connections.get(client); 42 | 43 | if (!connection) { 44 | connection = new PostgresConnection(client, { 45 | cursor: this.#config.cursor ?? null, 46 | }); 47 | this.#connections.set(client, connection); 48 | 49 | // The driver must take care of calling `onCreateConnection` when a new 50 | // connection is created. The `pg` module doesn't provide an async hook 51 | // for the connection creation. We need to call the method explicitly. 52 | if (this.#config?.onCreateConnection) { 53 | await this.#config.onCreateConnection(connection); 54 | } 55 | } 56 | 57 | return connection; 58 | } 59 | 60 | async beginTransaction( 61 | connection: DatabaseConnection, 62 | settings: TransactionSettings 63 | ): Promise { 64 | if (settings.isolationLevel) { 65 | await connection.executeQuery( 66 | CompiledQuery.raw( 67 | `start transaction isolation level ${settings.isolationLevel}` 68 | ) 69 | ); 70 | } else { 71 | await connection.executeQuery(CompiledQuery.raw("begin")); 72 | } 73 | } 74 | 75 | async commitTransaction(connection: DatabaseConnection): Promise { 76 | await connection.executeQuery(CompiledQuery.raw("commit")); 77 | } 78 | 79 | async rollbackTransaction(connection: DatabaseConnection): Promise { 80 | await connection.executeQuery(CompiledQuery.raw("rollback")); 81 | } 82 | 83 | async releaseConnection(connection: PostgresConnection): Promise { 84 | connection[PRIVATE_RELEASE_METHOD](); 85 | } 86 | 87 | async destroy(): Promise { 88 | if (this.#pool) { 89 | const pool = this.#pool; 90 | this.#pool = undefined; 91 | await pool.end(); 92 | } 93 | } 94 | } 95 | 96 | interface PostgresConnectionOptions { 97 | cursor: PostgresCursorConstructor | null; 98 | } 99 | 100 | class PostgresConnection implements DatabaseConnection { 101 | #client: PoolClient; 102 | #options: PostgresConnectionOptions; 103 | 104 | constructor(client: PoolClient, options: PostgresConnectionOptions) { 105 | this.#client = client; 106 | this.#options = options; 107 | } 108 | 109 | async executeQuery(compiledQuery: CompiledQuery): Promise> { 110 | try { 111 | const result = await this.#client.queryObject(compiledQuery.sql, [ 112 | ...compiledQuery.parameters, 113 | ]); 114 | 115 | if ( 116 | result.command === "INSERT" || 117 | result.command === "UPDATE" || 118 | result.command === "DELETE" 119 | ) { 120 | const numAffectedRows = BigInt(result.rowCount || 0); 121 | 122 | return { 123 | // TODO: remove. 124 | numUpdatedOrDeletedRows: numAffectedRows, 125 | numAffectedRows, 126 | rows: result.rows ?? [], 127 | } as any; 128 | } 129 | 130 | return { 131 | rows: result.rows ?? [], 132 | }; 133 | } catch (err) { 134 | throw extendStackTrace(err, new Error()); 135 | } 136 | } 137 | 138 | async *streamQuery( 139 | _compiledQuery: CompiledQuery, 140 | chunkSize: number 141 | ): AsyncIterableIterator> { 142 | if (!this.#options.cursor) { 143 | throw new Error( 144 | "'cursor' is not present in your postgres dialect config. It's required to make streaming work in postgres." 145 | ); 146 | } 147 | 148 | if (!Number.isInteger(chunkSize) || chunkSize <= 0) { 149 | throw new Error("chunkSize must be a positive integer"); 150 | } 151 | 152 | // stream not available 153 | return null; 154 | } 155 | 156 | [PRIVATE_RELEASE_METHOD](): void { 157 | this.#client.release(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /supabase/functions/kysely-postgres/README.md: -------------------------------------------------------------------------------- 1 | ## Test locally 2 | 3 | ```bash 4 | supabase functions serve --no-verify-jwt --env-file supabase/.env 5 | ``` 6 | 7 | Open http://localhost:54321/functions/v1/kysely-postgres 8 | -------------------------------------------------------------------------------- /supabase/functions/kysely-postgres/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.175.0/http/server.ts"; 6 | import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; 7 | import { 8 | Kysely, 9 | Generated, 10 | PostgresAdapter, 11 | PostgresIntrospector, 12 | PostgresQueryCompiler, 13 | } from "https://esm.sh/kysely@0.23.4"; 14 | // TODO: update once published to deno.land registry 15 | // https://github.com/barthuijgen/kysely-deno-postgres/pull/2 16 | import { PostgresDriver } from "./DenoPostgresDriver.ts"; 17 | 18 | console.log(`Function "kysely-postgres" up and running!`); 19 | 20 | interface AnimalTable { 21 | id: Generated; 22 | animal: string; 23 | created_at: Date; 24 | } 25 | 26 | // Keys of this interface are table names. 27 | interface Database { 28 | animals: AnimalTable; 29 | } 30 | 31 | // Create a database pool with one connection. 32 | const pool = new Pool( 33 | { 34 | tls: { caCertificates: [Deno.env.get("DB_SSL_CERT")!] }, 35 | database: "postgres", 36 | hostname: "db.bljghubhkofddfrezkhn.supabase.co", 37 | user: "postgres", 38 | port: 5432, 39 | password: Deno.env.get("DB_PASSWORD"), 40 | }, 41 | 1 42 | ); 43 | 44 | // You'd create one of these when you start your app. 45 | const db = new Kysely({ 46 | dialect: { 47 | createAdapter() { 48 | return new PostgresAdapter(); 49 | }, 50 | createDriver() { 51 | // You need a driver to be able to execute queries. In this example 52 | // we use the dummy driver that never does anything. 53 | return new PostgresDriver({ pool }); 54 | }, 55 | createIntrospector(db: Kysely) { 56 | return new PostgresIntrospector(db); 57 | }, 58 | createQueryCompiler() { 59 | return new PostgresQueryCompiler(); 60 | }, 61 | }, 62 | }); 63 | 64 | serve(async (_req) => { 65 | try { 66 | // Run a query 67 | const animals = await db 68 | .selectFrom("animals") 69 | .select(["id", "animal", "created_at"]) 70 | .execute(); 71 | 72 | // Neat, it's properly typed \o/ 73 | console.log(animals[0].created_at.getFullYear()); 74 | 75 | // Encode the result as pretty printed JSON 76 | const body = JSON.stringify( 77 | animals, 78 | (key, value) => (typeof value === "bigint" ? value.toString() : value), 79 | 2 80 | ); 81 | 82 | // Return the response with the correct content type header 83 | return new Response(body, { 84 | status: 200, 85 | headers: { 86 | "Content-Type": "application/json; charset=utf-8", 87 | }, 88 | }); 89 | } catch (err) { 90 | console.error(err); 91 | return new Response(String(err?.message ?? err), { status: 500 }); 92 | } 93 | }); 94 | 95 | // To invoke: navigate to http://localhost:54321/functions/v1/kysely-postgres 96 | -------------------------------------------------------------------------------- /supabase/functions/oak-server/README.md: -------------------------------------------------------------------------------- 1 | # Writing functions using Oak Server Middleware 2 | 3 | This example shows how you can write functions using Oak server middleware (https://oakserver.github.io/oak/) 4 | 5 | ## Run locally 6 | 7 | ```bash 8 | supabase functions serve --no-verify-jwt 9 | ``` 10 | 11 | Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/oak-server. 12 | 13 | ``` 14 | curl --location --request POST 'http://localhost:54321/functions/v1/oak-server' \ 15 | --header 'Authorization: Bearer YOUR_TOKEN' \ 16 | --header 'Content-Type: application/json' \ 17 | --data-raw '{ "name": "John Doe" }' 18 | ``` 19 | 20 | ## Deploy 21 | 22 | ```bash 23 | supabase functions deploy oak-server --no-verify-jwt 24 | ``` 25 | -------------------------------------------------------------------------------- /supabase/functions/oak-server/index.ts: -------------------------------------------------------------------------------- 1 | import { Application, Router } from 'oak' 2 | 3 | const router = new Router() 4 | router 5 | // Note: path should be prefixed with function name 6 | .get('/oak-server', (context) => { 7 | context.response.body = 'This is an example Oak server running on Edge Functions!' 8 | }) 9 | .post('/oak-server/greet', async (context) => { 10 | // Note: request body will be streamed to the function as chunks, set limit to 0 to fully read it. 11 | const result = context.request.body({ type: 'json', limit: 0 }) 12 | const body = await result.value 13 | const name = body.name || 'you' 14 | 15 | context.response.body = { msg: `Hey ${name}!` } 16 | }) 17 | .get('/oak-server/redirect', (context) => { 18 | context.response.redirect('https://www.example.com') 19 | }) 20 | 21 | const app = new Application() 22 | app.use(router.routes()) 23 | app.use(router.allowedMethods()) 24 | 25 | await app.listen({ port: 8000 }) 26 | -------------------------------------------------------------------------------- /supabase/functions/og-image-storage-cdn/handler.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ImageResponse } from "og_edge"; 3 | import { createClient } from "@supabase/supabase-js"; 4 | 5 | const STORAGE_URL = 6 | "https://bljghubhkofddfrezkhn.supabase.co/storage/v1/object/public/images/edgy-images/"; 7 | 8 | export default async function handler(req: Request) { 9 | const url = new URL(req.url); 10 | const name = url.searchParams.get("name"); 11 | 12 | const storageRes = await fetch(`${STORAGE_URL}${name}.png`); 13 | if (storageRes.ok) return storageRes; 14 | 15 | const generatedImage = new ImageResponse( 16 | ( 17 |
28 | Hello {name}! 29 |
30 | ) 31 | ); 32 | 33 | // Upload to Supabase storage 34 | const supabaseAdminClient = createClient( 35 | // Supabase API URL - env var exported by default when deployed. 36 | Deno.env.get("SUPABASE_URL") ?? "", 37 | // Supabase API SERVICE ROLE KEY - env var exported by default when deployed. 38 | Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "" 39 | ); 40 | 41 | const { data, error } = await supabaseAdminClient.storage 42 | .from("images") 43 | .upload(`edgy-images/${name}.png`, generatedImage.body!, { 44 | cacheControl: "3600", 45 | upsert: false, 46 | }); 47 | console.log({ data, error }); 48 | 49 | return generatedImage; 50 | } 51 | -------------------------------------------------------------------------------- /supabase/functions/og-image-storage-cdn/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | import handler from "./handler.tsx"; 7 | 8 | console.log("Hello from og-image-storage-cdn Function!"); 9 | 10 | serve(handler); 11 | 12 | // To invoke: 13 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 14 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 15 | // --header 'Content-Type: application/json' \ 16 | // --data '{"name":"Functions"}' 17 | -------------------------------------------------------------------------------- /supabase/functions/og-image/handler.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ImageResponse } from "og_edge"; 3 | 4 | export default function handler(req: Request) { 5 | return new ImageResponse( 6 | ( 7 |
18 | Hello GH Action! 19 |
20 | ) 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /supabase/functions/og-image/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | import handler from "./handler.tsx"; 7 | 8 | console.log("Hello from og-image Function!"); 9 | 10 | serve(handler); 11 | 12 | // To invoke: 13 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 14 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 15 | // --header 'Content-Type: application/json' \ 16 | // --data '{"name":"Functions"}' 17 | -------------------------------------------------------------------------------- /supabase/functions/postgres-on-the-edge/README.md: -------------------------------------------------------------------------------- 1 | ## Test locally 2 | 3 | ```bash 4 | supabase functions serve --no-verify-jwt --env-file supabase/.env 5 | ``` 6 | 7 | Open http://localhost:54321/functions/v1/postgres-on-the-edge 8 | -------------------------------------------------------------------------------- /supabase/functions/postgres-on-the-edge/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; 6 | import { serve } from "https://deno.land/std@0.114.0/http/server.ts"; 7 | 8 | console.log(`Function "postgres-on-the-edge" up and running!`); 9 | 10 | // Create a database pool with one connection. 11 | const pool = new Pool( 12 | { 13 | tls: { caCertificates: [Deno.env.get("DB_SSL_CERT")!] }, 14 | database: "postgres", 15 | hostname: "db.bljghubhkofddfrezkhn.supabase.co", 16 | user: "postgres", 17 | port: 5432, 18 | password: Deno.env.get("DB_PASSWORD"), 19 | }, 20 | 1 21 | ); 22 | 23 | serve(async (_req) => { 24 | try { 25 | // Grab a connection from the pool 26 | const connection = await pool.connect(); 27 | 28 | try { 29 | // Run a query 30 | const result = await connection.queryObject`SELECT * FROM animals`; 31 | const animals = result.rows; // [{ id: 1, name: "Lion" }, ...] 32 | 33 | // Encode the result as pretty printed JSON 34 | const body = JSON.stringify( 35 | animals, 36 | (key, value) => (typeof value === "bigint" ? value.toString() : value), 37 | 2 38 | ); 39 | 40 | // Return the response with the correct content type header 41 | return new Response(body, { 42 | status: 200, 43 | headers: { 44 | "Content-Type": "application/json; charset=utf-8", 45 | }, 46 | }); 47 | } finally { 48 | // Release the connection back into the pool 49 | connection.release(); 50 | } 51 | } catch (err) { 52 | console.error(err); 53 | return new Response(String(err?.message ?? err), { status: 500 }); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /supabase/functions/resend/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; 6 | import { Database } from "./types.ts"; 7 | 8 | console.log("Hello from `resend` function!"); 9 | 10 | type UserRecord = Database["auth"]["Tables"]["users"]["Row"]; 11 | interface WebhookPayload { 12 | type: "INSERT" | "UPDATE" | "DELETE"; 13 | table: string; 14 | record: null | UserRecord; 15 | schema: "public"; 16 | old_record: null | UserRecord; 17 | } 18 | 19 | serve(async (req) => { 20 | const payload: WebhookPayload = await req.json(); 21 | const newUser = payload.record; 22 | const deletedUser = payload.old_record; 23 | 24 | const res = await fetch("https://api.resend.com/emails", { 25 | method: "POST", 26 | headers: { 27 | "Content-Type": "application/json", 28 | Authorization: `Bearer ${Deno.env.get("RESEND_API_KEY")}`, 29 | }, 30 | body: JSON.stringify({ 31 | from: "SupaThor ", 32 | to: [deletedUser?.email ?? newUser?.email], 33 | subject: deletedUser 34 | ? "Sorry to see you go" 35 | : "Welcome to the SupaThorClub", 36 | html: deletedUser 37 | ? `Hey ${deletedUser.email}, we're very sad to see you go! Pleas come visit again soon!` 38 | : `Hey ${newUser?.email} we're glad to have you!`, 39 | }), 40 | }); 41 | 42 | const data = await res.json(); 43 | console.log({ data }); 44 | 45 | return new Response("ok"); 46 | }); 47 | 48 | // To invoke: 49 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ 50 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 51 | // --header 'Content-Type: application/json' \ 52 | // --data '{"name":"Functions"}' 53 | -------------------------------------------------------------------------------- /supabase/functions/resend/types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json | undefined } 7 | | Json[] 8 | 9 | export interface Database { 10 | auth: { 11 | Tables: { 12 | audit_log_entries: { 13 | Row: { 14 | created_at: string | null 15 | id: string 16 | instance_id: string | null 17 | ip_address: string 18 | payload: Json | null 19 | } 20 | Insert: { 21 | created_at?: string | null 22 | id: string 23 | instance_id?: string | null 24 | ip_address?: string 25 | payload?: Json | null 26 | } 27 | Update: { 28 | created_at?: string | null 29 | id?: string 30 | instance_id?: string | null 31 | ip_address?: string 32 | payload?: Json | null 33 | } 34 | Relationships: [] 35 | } 36 | flow_state: { 37 | Row: { 38 | auth_code: string 39 | authentication_method: string 40 | code_challenge: string 41 | code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"] 42 | created_at: string | null 43 | id: string 44 | provider_access_token: string | null 45 | provider_refresh_token: string | null 46 | provider_type: string 47 | updated_at: string | null 48 | user_id: string | null 49 | } 50 | Insert: { 51 | auth_code: string 52 | authentication_method: string 53 | code_challenge: string 54 | code_challenge_method: Database["auth"]["Enums"]["code_challenge_method"] 55 | created_at?: string | null 56 | id: string 57 | provider_access_token?: string | null 58 | provider_refresh_token?: string | null 59 | provider_type: string 60 | updated_at?: string | null 61 | user_id?: string | null 62 | } 63 | Update: { 64 | auth_code?: string 65 | authentication_method?: string 66 | code_challenge?: string 67 | code_challenge_method?: Database["auth"]["Enums"]["code_challenge_method"] 68 | created_at?: string | null 69 | id?: string 70 | provider_access_token?: string | null 71 | provider_refresh_token?: string | null 72 | provider_type?: string 73 | updated_at?: string | null 74 | user_id?: string | null 75 | } 76 | Relationships: [] 77 | } 78 | identities: { 79 | Row: { 80 | created_at: string | null 81 | email: string | null 82 | id: string 83 | identity_data: Json 84 | last_sign_in_at: string | null 85 | provider: string 86 | updated_at: string | null 87 | user_id: string 88 | } 89 | Insert: { 90 | created_at?: string | null 91 | email?: string | null 92 | id: string 93 | identity_data: Json 94 | last_sign_in_at?: string | null 95 | provider: string 96 | updated_at?: string | null 97 | user_id: string 98 | } 99 | Update: { 100 | created_at?: string | null 101 | email?: string | null 102 | id?: string 103 | identity_data?: Json 104 | last_sign_in_at?: string | null 105 | provider?: string 106 | updated_at?: string | null 107 | user_id?: string 108 | } 109 | Relationships: [ 110 | { 111 | foreignKeyName: "identities_user_id_fkey" 112 | columns: ["user_id"] 113 | referencedRelation: "users" 114 | referencedColumns: ["id"] 115 | } 116 | ] 117 | } 118 | instances: { 119 | Row: { 120 | created_at: string | null 121 | id: string 122 | raw_base_config: string | null 123 | updated_at: string | null 124 | uuid: string | null 125 | } 126 | Insert: { 127 | created_at?: string | null 128 | id: string 129 | raw_base_config?: string | null 130 | updated_at?: string | null 131 | uuid?: string | null 132 | } 133 | Update: { 134 | created_at?: string | null 135 | id?: string 136 | raw_base_config?: string | null 137 | updated_at?: string | null 138 | uuid?: string | null 139 | } 140 | Relationships: [] 141 | } 142 | mfa_amr_claims: { 143 | Row: { 144 | authentication_method: string 145 | created_at: string 146 | id: string 147 | session_id: string 148 | updated_at: string 149 | } 150 | Insert: { 151 | authentication_method: string 152 | created_at: string 153 | id: string 154 | session_id: string 155 | updated_at: string 156 | } 157 | Update: { 158 | authentication_method?: string 159 | created_at?: string 160 | id?: string 161 | session_id?: string 162 | updated_at?: string 163 | } 164 | Relationships: [ 165 | { 166 | foreignKeyName: "mfa_amr_claims_session_id_fkey" 167 | columns: ["session_id"] 168 | referencedRelation: "sessions" 169 | referencedColumns: ["id"] 170 | } 171 | ] 172 | } 173 | mfa_challenges: { 174 | Row: { 175 | created_at: string 176 | factor_id: string 177 | id: string 178 | ip_address: unknown 179 | verified_at: string | null 180 | } 181 | Insert: { 182 | created_at: string 183 | factor_id: string 184 | id: string 185 | ip_address: unknown 186 | verified_at?: string | null 187 | } 188 | Update: { 189 | created_at?: string 190 | factor_id?: string 191 | id?: string 192 | ip_address?: unknown 193 | verified_at?: string | null 194 | } 195 | Relationships: [ 196 | { 197 | foreignKeyName: "mfa_challenges_auth_factor_id_fkey" 198 | columns: ["factor_id"] 199 | referencedRelation: "mfa_factors" 200 | referencedColumns: ["id"] 201 | } 202 | ] 203 | } 204 | mfa_factors: { 205 | Row: { 206 | created_at: string 207 | factor_type: Database["auth"]["Enums"]["factor_type"] 208 | friendly_name: string | null 209 | id: string 210 | secret: string | null 211 | status: Database["auth"]["Enums"]["factor_status"] 212 | updated_at: string 213 | user_id: string 214 | } 215 | Insert: { 216 | created_at: string 217 | factor_type: Database["auth"]["Enums"]["factor_type"] 218 | friendly_name?: string | null 219 | id: string 220 | secret?: string | null 221 | status: Database["auth"]["Enums"]["factor_status"] 222 | updated_at: string 223 | user_id: string 224 | } 225 | Update: { 226 | created_at?: string 227 | factor_type?: Database["auth"]["Enums"]["factor_type"] 228 | friendly_name?: string | null 229 | id?: string 230 | secret?: string | null 231 | status?: Database["auth"]["Enums"]["factor_status"] 232 | updated_at?: string 233 | user_id?: string 234 | } 235 | Relationships: [ 236 | { 237 | foreignKeyName: "mfa_factors_user_id_fkey" 238 | columns: ["user_id"] 239 | referencedRelation: "users" 240 | referencedColumns: ["id"] 241 | } 242 | ] 243 | } 244 | refresh_tokens: { 245 | Row: { 246 | created_at: string | null 247 | id: number 248 | instance_id: string | null 249 | parent: string | null 250 | revoked: boolean | null 251 | session_id: string | null 252 | token: string | null 253 | updated_at: string | null 254 | user_id: string | null 255 | } 256 | Insert: { 257 | created_at?: string | null 258 | id?: number 259 | instance_id?: string | null 260 | parent?: string | null 261 | revoked?: boolean | null 262 | session_id?: string | null 263 | token?: string | null 264 | updated_at?: string | null 265 | user_id?: string | null 266 | } 267 | Update: { 268 | created_at?: string | null 269 | id?: number 270 | instance_id?: string | null 271 | parent?: string | null 272 | revoked?: boolean | null 273 | session_id?: string | null 274 | token?: string | null 275 | updated_at?: string | null 276 | user_id?: string | null 277 | } 278 | Relationships: [ 279 | { 280 | foreignKeyName: "refresh_tokens_session_id_fkey" 281 | columns: ["session_id"] 282 | referencedRelation: "sessions" 283 | referencedColumns: ["id"] 284 | } 285 | ] 286 | } 287 | saml_providers: { 288 | Row: { 289 | attribute_mapping: Json | null 290 | created_at: string | null 291 | entity_id: string 292 | id: string 293 | metadata_url: string | null 294 | metadata_xml: string 295 | sso_provider_id: string 296 | updated_at: string | null 297 | } 298 | Insert: { 299 | attribute_mapping?: Json | null 300 | created_at?: string | null 301 | entity_id: string 302 | id: string 303 | metadata_url?: string | null 304 | metadata_xml: string 305 | sso_provider_id: string 306 | updated_at?: string | null 307 | } 308 | Update: { 309 | attribute_mapping?: Json | null 310 | created_at?: string | null 311 | entity_id?: string 312 | id?: string 313 | metadata_url?: string | null 314 | metadata_xml?: string 315 | sso_provider_id?: string 316 | updated_at?: string | null 317 | } 318 | Relationships: [ 319 | { 320 | foreignKeyName: "saml_providers_sso_provider_id_fkey" 321 | columns: ["sso_provider_id"] 322 | referencedRelation: "sso_providers" 323 | referencedColumns: ["id"] 324 | } 325 | ] 326 | } 327 | saml_relay_states: { 328 | Row: { 329 | created_at: string | null 330 | for_email: string | null 331 | from_ip_address: unknown | null 332 | id: string 333 | redirect_to: string | null 334 | request_id: string 335 | sso_provider_id: string 336 | updated_at: string | null 337 | } 338 | Insert: { 339 | created_at?: string | null 340 | for_email?: string | null 341 | from_ip_address?: unknown | null 342 | id: string 343 | redirect_to?: string | null 344 | request_id: string 345 | sso_provider_id: string 346 | updated_at?: string | null 347 | } 348 | Update: { 349 | created_at?: string | null 350 | for_email?: string | null 351 | from_ip_address?: unknown | null 352 | id?: string 353 | redirect_to?: string | null 354 | request_id?: string 355 | sso_provider_id?: string 356 | updated_at?: string | null 357 | } 358 | Relationships: [ 359 | { 360 | foreignKeyName: "saml_relay_states_sso_provider_id_fkey" 361 | columns: ["sso_provider_id"] 362 | referencedRelation: "sso_providers" 363 | referencedColumns: ["id"] 364 | } 365 | ] 366 | } 367 | schema_migrations: { 368 | Row: { 369 | version: string 370 | } 371 | Insert: { 372 | version: string 373 | } 374 | Update: { 375 | version?: string 376 | } 377 | Relationships: [] 378 | } 379 | sessions: { 380 | Row: { 381 | aal: Database["auth"]["Enums"]["aal_level"] | null 382 | created_at: string | null 383 | factor_id: string | null 384 | id: string 385 | not_after: string | null 386 | updated_at: string | null 387 | user_id: string 388 | } 389 | Insert: { 390 | aal?: Database["auth"]["Enums"]["aal_level"] | null 391 | created_at?: string | null 392 | factor_id?: string | null 393 | id: string 394 | not_after?: string | null 395 | updated_at?: string | null 396 | user_id: string 397 | } 398 | Update: { 399 | aal?: Database["auth"]["Enums"]["aal_level"] | null 400 | created_at?: string | null 401 | factor_id?: string | null 402 | id?: string 403 | not_after?: string | null 404 | updated_at?: string | null 405 | user_id?: string 406 | } 407 | Relationships: [ 408 | { 409 | foreignKeyName: "sessions_user_id_fkey" 410 | columns: ["user_id"] 411 | referencedRelation: "users" 412 | referencedColumns: ["id"] 413 | } 414 | ] 415 | } 416 | sso_domains: { 417 | Row: { 418 | created_at: string | null 419 | domain: string 420 | id: string 421 | sso_provider_id: string 422 | updated_at: string | null 423 | } 424 | Insert: { 425 | created_at?: string | null 426 | domain: string 427 | id: string 428 | sso_provider_id: string 429 | updated_at?: string | null 430 | } 431 | Update: { 432 | created_at?: string | null 433 | domain?: string 434 | id?: string 435 | sso_provider_id?: string 436 | updated_at?: string | null 437 | } 438 | Relationships: [ 439 | { 440 | foreignKeyName: "sso_domains_sso_provider_id_fkey" 441 | columns: ["sso_provider_id"] 442 | referencedRelation: "sso_providers" 443 | referencedColumns: ["id"] 444 | } 445 | ] 446 | } 447 | sso_providers: { 448 | Row: { 449 | created_at: string | null 450 | id: string 451 | resource_id: string | null 452 | updated_at: string | null 453 | } 454 | Insert: { 455 | created_at?: string | null 456 | id: string 457 | resource_id?: string | null 458 | updated_at?: string | null 459 | } 460 | Update: { 461 | created_at?: string | null 462 | id?: string 463 | resource_id?: string | null 464 | updated_at?: string | null 465 | } 466 | Relationships: [] 467 | } 468 | users: { 469 | Row: { 470 | aud: string | null 471 | banned_until: string | null 472 | confirmation_sent_at: string | null 473 | confirmation_token: string | null 474 | confirmed_at: string | null 475 | created_at: string | null 476 | deleted_at: string | null 477 | email: string | null 478 | email_change: string | null 479 | email_change_confirm_status: number | null 480 | email_change_sent_at: string | null 481 | email_change_token_current: string | null 482 | email_change_token_new: string | null 483 | email_confirmed_at: string | null 484 | encrypted_password: string | null 485 | id: string 486 | instance_id: string | null 487 | invited_at: string | null 488 | is_sso_user: boolean 489 | is_super_admin: boolean | null 490 | last_sign_in_at: string | null 491 | phone: string | null 492 | phone_change: string | null 493 | phone_change_sent_at: string | null 494 | phone_change_token: string | null 495 | phone_confirmed_at: string | null 496 | raw_app_meta_data: Json | null 497 | raw_user_meta_data: Json | null 498 | reauthentication_sent_at: string | null 499 | reauthentication_token: string | null 500 | recovery_sent_at: string | null 501 | recovery_token: string | null 502 | role: string | null 503 | updated_at: string | null 504 | } 505 | Insert: { 506 | aud?: string | null 507 | banned_until?: string | null 508 | confirmation_sent_at?: string | null 509 | confirmation_token?: string | null 510 | confirmed_at?: string | null 511 | created_at?: string | null 512 | deleted_at?: string | null 513 | email?: string | null 514 | email_change?: string | null 515 | email_change_confirm_status?: number | null 516 | email_change_sent_at?: string | null 517 | email_change_token_current?: string | null 518 | email_change_token_new?: string | null 519 | email_confirmed_at?: string | null 520 | encrypted_password?: string | null 521 | id: string 522 | instance_id?: string | null 523 | invited_at?: string | null 524 | is_sso_user?: boolean 525 | is_super_admin?: boolean | null 526 | last_sign_in_at?: string | null 527 | phone?: string | null 528 | phone_change?: string | null 529 | phone_change_sent_at?: string | null 530 | phone_change_token?: string | null 531 | phone_confirmed_at?: string | null 532 | raw_app_meta_data?: Json | null 533 | raw_user_meta_data?: Json | null 534 | reauthentication_sent_at?: string | null 535 | reauthentication_token?: string | null 536 | recovery_sent_at?: string | null 537 | recovery_token?: string | null 538 | role?: string | null 539 | updated_at?: string | null 540 | } 541 | Update: { 542 | aud?: string | null 543 | banned_until?: string | null 544 | confirmation_sent_at?: string | null 545 | confirmation_token?: string | null 546 | confirmed_at?: string | null 547 | created_at?: string | null 548 | deleted_at?: string | null 549 | email?: string | null 550 | email_change?: string | null 551 | email_change_confirm_status?: number | null 552 | email_change_sent_at?: string | null 553 | email_change_token_current?: string | null 554 | email_change_token_new?: string | null 555 | email_confirmed_at?: string | null 556 | encrypted_password?: string | null 557 | id?: string 558 | instance_id?: string | null 559 | invited_at?: string | null 560 | is_sso_user?: boolean 561 | is_super_admin?: boolean | null 562 | last_sign_in_at?: string | null 563 | phone?: string | null 564 | phone_change?: string | null 565 | phone_change_sent_at?: string | null 566 | phone_change_token?: string | null 567 | phone_confirmed_at?: string | null 568 | raw_app_meta_data?: Json | null 569 | raw_user_meta_data?: Json | null 570 | reauthentication_sent_at?: string | null 571 | reauthentication_token?: string | null 572 | recovery_sent_at?: string | null 573 | recovery_token?: string | null 574 | role?: string | null 575 | updated_at?: string | null 576 | } 577 | Relationships: [] 578 | } 579 | } 580 | Views: { 581 | [_ in never]: never 582 | } 583 | Functions: { 584 | email: { 585 | Args: Record 586 | Returns: string 587 | } 588 | jwt: { 589 | Args: Record 590 | Returns: Json 591 | } 592 | role: { 593 | Args: Record 594 | Returns: string 595 | } 596 | uid: { 597 | Args: Record 598 | Returns: string 599 | } 600 | } 601 | Enums: { 602 | aal_level: "aal1" | "aal2" | "aal3" 603 | code_challenge_method: "s256" | "plain" 604 | factor_status: "unverified" | "verified" 605 | factor_type: "totp" | "webauthn" 606 | } 607 | CompositeTypes: { 608 | [_ in never]: never 609 | } 610 | } 611 | } 612 | 613 | -------------------------------------------------------------------------------- /supabase/functions/screenshot/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "std/server"; 2 | import puppeteer from "puppeteer"; 3 | 4 | serve(async (req) => { 5 | try { 6 | // Visit browserless.io to get your free API token 7 | const browser = await puppeteer.connect({ 8 | browserWSEndpoint: `wss://chrome.browserless.io?token=${Deno.env.get( 9 | "PUPPETEER_BROWSERLESS_IO_KEY" 10 | )}`, 11 | }); 12 | const page = await browser.newPage(); 13 | 14 | const url = 15 | new URL(req.url).searchParams.get("url") || "http://www.example.com"; 16 | 17 | await page.goto(url, { waitUntil: "networkidle2" }); 18 | const screenshot = await page.screenshot(); 19 | 20 | return new Response(screenshot, { 21 | headers: { "Content-Type": "image/png" }, 22 | }); 23 | } catch (e) { 24 | console.error(e); 25 | return new Response(JSON.stringify({ error: e.message }), { 26 | headers: { "Content-Type": "application/json" }, 27 | status: 500, 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /supabase/functions/stripe-webhook/.env.example: -------------------------------------------------------------------------------- 1 | STRIPE_API_KEY= 2 | STRIPE_WEBHOOK_SIGNING_SECRET= -------------------------------------------------------------------------------- /supabase/functions/stripe-webhook/README.md: -------------------------------------------------------------------------------- 1 | # stripe-webhooks 2 | 3 | Also check out our full Stripe Payments examples for [React Native (Expo)](https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions) and [Flutter](https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions). 4 | 5 | ## Setup env vars 6 | 7 | - `cp supabase/functions/stripe-webhook/.env.example supabase/functions/stripe-webhook/.env` 8 | 9 | ## Test locally 10 | 11 | - Terminal 1: 12 | - `supabase functions serve --no-verify-jwt stripe-webhook --env-file ./supabase/functions/stripe-webhook/.env` 13 | - Terminal 2: 14 | - `stripe listen --forward-to localhost:54321/functions/v1/` 15 | - Terminal 3 (optional): 16 | - `stripe trigger payment_intent.succeeded` 17 | 18 | ## Deploy 19 | 20 | - `supabase functions deploy --no-verify-jwt stripe-webhook` 21 | - `supabase secrets set --env-file ./supabase/functions/stripe-webhook/.env` 22 | -------------------------------------------------------------------------------- /supabase/functions/stripe-webhook/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | 7 | // Import via bare specifier thanks to the import_map.json file. 8 | import Stripe from "stripe"; 9 | 10 | const stripe = new Stripe(Deno.env.get("STRIPE_API_KEY") as string, { 11 | // This is needed to use the Fetch API rather than relying on the Node http 12 | // package. 13 | apiVersion: "2022-11-15", 14 | httpClient: Stripe.createFetchHttpClient(), 15 | }); 16 | // This is needed in order to use the Web Crypto API in Deno. 17 | const cryptoProvider = Stripe.createSubtleCryptoProvider(); 18 | 19 | console.log("Hello from Stripe Webhook!"); 20 | 21 | serve(async (request) => { 22 | const signature = request.headers.get("Stripe-Signature"); 23 | 24 | // First step is to verify the event. The .text() method must be used as the 25 | // verification relies on the raw request body rather than the parsed JSON. 26 | const body = await request.text(); 27 | let receivedEvent; 28 | try { 29 | receivedEvent = await stripe.webhooks.constructEventAsync( 30 | body, 31 | signature!, 32 | Deno.env.get("STRIPE_WEBHOOK_SIGNING_SECRET")!, 33 | undefined, 34 | cryptoProvider 35 | ); 36 | } catch (err) { 37 | return new Response(err.message, { status: 400 }); 38 | } 39 | console.log(receivedEvent); 40 | return new Response(JSON.stringify({ ok: true }), { status: 200 }); 41 | }); 42 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/README.md: -------------------------------------------------------------------------------- 1 | # [grammY](https://grammy.dev) on [Supabase Edge Functions](https://supabase.com/edge-functions) 2 | 3 | > Try it out: [@supabase_example_bot](https://t.me/supabase_example_bot) 4 | 5 | ## Deploying 6 | 7 | 1. Create the function: 8 | 9 | ```shell 10 | supabase functions deploy --no-verify-jwt telegram-bot 11 | ``` 12 | 13 | 2. Contact [@BotFather](https://t.me/BotFather) to create a bot and get its 14 | token. 15 | 3. Set the secrets: 16 | 17 | ```shell 18 | supabase secrets set BOT_TOKEN=your_token FUNCTION_SECRET=random_secret 19 | ``` 20 | 21 | 4. Set your bot’s webhook URL to 22 | `https://.functions.supabase.co/telegram-bot` (replacing 23 | `<...>` with respective values). To do that, you open the request URL in your 24 | browser: 25 | 26 | ```text 27 | https://api.telegram.org/bot/setWebhook?url=https://.functions.supabase.co/telegram-bot?secret= 28 | ``` 29 | -------------------------------------------------------------------------------- /supabase/functions/telegram-bot/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "std/server"; 6 | 7 | console.log(`Function "telegram-bot" up and running!`); 8 | 9 | import { Bot, webhookCallback } from "grammy"; 10 | 11 | const bot = new Bot(Deno.env.get("BOT_TOKEN") || ""); 12 | 13 | bot.command("start", (ctx) => ctx.reply("Welcome! Up and running.")); 14 | 15 | bot.command("ping", (ctx) => ctx.reply(`Pong! ${new Date()} ${Date.now()}`)); 16 | 17 | const handleUpdate = webhookCallback(bot, "std/http"); 18 | 19 | serve(async (req) => { 20 | try { 21 | const url = new URL(req.url); 22 | if (url.searchParams.get("secret") !== Deno.env.get("FUNCTION_SECRET")) { 23 | return new Response("not allowed", { status: 405 }); 24 | } 25 | 26 | return await handleUpdate(req); 27 | } catch (err) { 28 | console.error(err); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /supabase/functions/upstash-redis-counter/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "std/server"; 2 | import { Redis } from "@upstash/redis"; 3 | console.log(`Function "upstash-redis-counter" up and running!`); 4 | serve(async (_req) => { 5 | try { 6 | const redis = new Redis({ 7 | url: Deno.env.get("UPSTASH_REDIS_REST_URL")!, 8 | token: Deno.env.get("UPSTASH_REDIS_REST_TOKEN")!, 9 | }); 10 | const deno_region = Deno.env.get("DENO_REGION"); 11 | if (deno_region) { 12 | // Increment region counter 13 | await redis.hincrby("supa-edge-counter", deno_region, 1); 14 | } else { 15 | // Increment localhost counter 16 | await redis.hincrby("supa-edge-counter", "localhost", 1); 17 | } 18 | // Get all values 19 | const counterHash: Record | null = await redis.hgetall( 20 | "supa-edge-counter" 21 | ); 22 | const counters = Object.entries(counterHash!) 23 | .sort(([, a], [, b]) => b - a) // sort desc 24 | .reduce( 25 | (r, [k, v]) => ({ 26 | total: r.total + v, 27 | regions: { ...r.regions, [k]: v }, 28 | }), 29 | { 30 | total: 0, 31 | regions: {}, 32 | } 33 | ); 34 | return new Response(JSON.stringify({ counters }), { status: 200 }); 35 | } catch (error) { 36 | return new Response(JSON.stringify({ error: error.message }), { 37 | status: 200, 38 | }); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /supabase/functions/upstash-redis-ratelimit/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "std/server"; 2 | import { Redis } from "@upstash/redis"; 3 | import { Ratelimit } from "@upstash/ratelimit"; 4 | import { createClient } from "@supabase/supabase-js"; 5 | 6 | console.log(`Function "upstash-redis-counter" up and running!`); 7 | 8 | serve(async (req) => { 9 | try { 10 | // Create a Supabase client with the Auth context of the logged in user. 11 | const supabaseClient = createClient( 12 | // Supabase API URL - env var exported by default. 13 | Deno.env.get("SUPABASE_URL") ?? "", 14 | // Supabase API ANON KEY - env var exported by default. 15 | Deno.env.get("SUPABASE_ANON_KEY") ?? "", 16 | // Create client with Auth context of the user that called the function. 17 | // This way your row-level-security (RLS) policies are applied. 18 | { 19 | global: { 20 | headers: { Authorization: req.headers.get("Authorization")! }, 21 | }, 22 | } 23 | ); 24 | // Now we can get the session or user object 25 | const { 26 | data: { user }, 27 | } = await supabaseClient.auth.getUser(); 28 | if (!user) throw new Error("no user"); 29 | console.log(user.id); 30 | 31 | const redis = new Redis({ 32 | url: Deno.env.get("UPSTASH_REDIS_REST_URL")!, 33 | token: Deno.env.get("UPSTASH_REDIS_REST_TOKEN")!, 34 | }); 35 | 36 | // Create a new ratelimiter, that allows 10 requests per 10 seconds 37 | const ratelimit = new Ratelimit({ 38 | redis, 39 | limiter: Ratelimit.slidingWindow(2, "10 s"), 40 | analytics: true, 41 | }); 42 | 43 | // Use a constant string to limit all requests with a single ratelimit 44 | // Or use a userID, apiKey or ip address for individual limits. 45 | const identifier = user.id; 46 | const { success } = await ratelimit.limit(identifier); 47 | 48 | if (!success) { 49 | throw new Error("limit exceeded"); 50 | } 51 | 52 | return new Response(JSON.stringify({ success }), { status: 200 }); 53 | } catch (error) { 54 | return new Response(JSON.stringify({ error: error.message }), { 55 | status: 200, 56 | }); 57 | } 58 | }); 59 | 60 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/upstash-redis-ratelimit' \ 61 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjc4MDA4Mjk2LCJzdWIiOiJkZjQ3ZDU5My0zMjY1LTQ2OWEtOWI5OS1mZDk1OTdhOGU4YzciLCJlbWFpbCI6InRlc3RyQHRlc3Quc2ciLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7fSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTY3ODAwNDY5Nn1dLCJzZXNzaW9uX2lkIjoiMDNjMmFlYjUtMjk5MC00ZWU0LWIxYTYtZmU1Y2IwMGFhMjU3In0.WwqggrX34j0NINiulC9rsj-cd6HzV55ySxJkJlVsU4o' \ 62 | // --header 'Content-Type: application/json' \ 63 | // --data '{"name":"Functions"}' 64 | -------------------------------------------------------------------------------- /supabase/functions/vercel-ai-openai-completion/README.md: -------------------------------------------------------------------------------- 1 | # vercel ai test 2 | 3 | https://sdk.vercel.ai/docs/api-reference/use-completion 4 | 5 | ## setup 6 | 7 | - create .env file in `./supabase` folder 8 | - Add `OPENAI_API_KEY=` in your .env file 9 | 10 | ## run locally 11 | 12 | ```bash 13 | upabase functions serve --env-file ./supabase/.env 14 | ``` 15 | -------------------------------------------------------------------------------- /supabase/functions/vercel-ai-openai-completion/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; 6 | import { Configuration, OpenAIApi } from "https://esm.sh/openai-edge@1.0.0"; 7 | import { OpenAIStream, StreamingTextResponse } from "https://esm.sh/ai@2.0.1"; 8 | 9 | const apiConfig = new Configuration({ 10 | apiKey: Deno.env.get("OPENAI_API_KEY"), 11 | }); 12 | 13 | const openai = new OpenAIApi(apiConfig); 14 | 15 | serve(async (req) => { 16 | // Extract the `prompt` from the body of the request 17 | const { prompt } = await req.json(); 18 | 19 | // Request the OpenAI API for the response based on the prompt 20 | const response = await openai.createChatCompletion({ 21 | model: "gpt-3.5-turbo", 22 | stream: true, 23 | messages: [{ role: "user", content: prompt }], 24 | max_tokens: 200, 25 | temperature: 0.7, 26 | top_p: 1, 27 | frequency_penalty: 1, 28 | presence_penalty: 1, 29 | }); 30 | 31 | const stream = OpenAIStream(response); 32 | 33 | return new StreamingTextResponse(stream); 34 | }); 35 | 36 | // To invoke: 37 | // curl -i --location --request POST 'http://localhost:54321/functions/v1/vercel-ai-openai-completion' \ 38 | // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 39 | // --header 'Content-Type: application/json' \ 40 | // --data '{"prompt":"Given the following post content, detect if it has typo or not. Respond with a JSON array of typos [\"typo1\", \"typo2\", ...] or an empty [] if there is none. Only respond with an array. Post content: acceptible"}' 41 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thorwebdev/edgy-edge-functions/81d87aca378202c70655fedb1a98023794fe18ff/supabase/seed.sql --------------------------------------------------------------------------------