diff --git a/apps/docs/pages/guides/functions/examples/rate-limiting.mdx b/apps/docs/pages/guides/functions/examples/rate-limiting.mdx index 32486fadc48..6c99592a8fc 100644 --- a/apps/docs/pages/guides/functions/examples/rate-limiting.mdx +++ b/apps/docs/pages/guides/functions/examples/rate-limiting.mdx @@ -18,7 +18,7 @@ export const meta = { [Upstash](https://upstash.com/) provides an HTTP/REST based Redis client which is ideal for serverless use-cases and therefore works well with Supabase Edge Functions. -Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-counter). +Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-ratelimit). export const Page = ({ children }) => diff --git a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/README.md b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/README.md new file mode 100644 index 00000000000..86fe57b6387 --- /dev/null +++ b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/README.md @@ -0,0 +1,33 @@ +# Rate limiting with Upstash Redis in Supabase Edge Functions + +[Redis](https://redis.io/docs/about/) is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. It is optimized for atomic operations like incrementing a value, for example for a view counter or rate limiting. We can even rate limit based on the user ID from Supabase Auth! + +[Upstash](https://upstash.com/) provides an HTTP/REST based Redis client which is ideal for serverless use-cases and therefore works well with Supabase Edge Functions. + +## Redis database setup + +Create a Redis database using the [Upstash Console](https://console.upstash.com/) or [Upstash CLI](https://github.com/upstash/cli). + +Select the `Global` type to minimize the latency from all edge locations. Copy the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` to your .env file. You'll find them under **Details > REST API > .env**. + +```bash +cp supabase/.env.local.example supabase/.env.local +``` + +## Run locally + +Make sure you have the latest version of the [Supabase CLI installed](https://supabase.com/docs/guides/cli#installation). + +```bash +supabase start +supabase functions serve --env-file supabase/.env.local +``` + +Navigate to http://localhost:54321/functions/v1/upstash-redis-ratelimit. + +## Deploy + +```bash +supabase functions deploy upstash-redis-ratelimit +supabase secrets set --env-file supabase/.env.local +``` diff --git a/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts new file mode 100644 index 00000000000..0f934d1864a --- /dev/null +++ b/examples/edge-functions/supabase/functions/upstash-redis-ratelimit/index.ts @@ -0,0 +1,63 @@ +import { serve } from "std/server"; +import { Redis } from "@upstash/redis"; +import { Ratelimit } from "@upstash/ratelimit"; +import { createClient } from "@supabase/supabase-js"; + +console.log(`Function "upstash-redis-counter" up and running!`); + +serve(async (req) => { + try { + // 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 ANON KEY - env var exported by default. + Deno.env.get("SUPABASE_ANON_KEY") ?? "", + // 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")! }, + }, + } + ); + // Now we can get the session or user object + const { + data: { user }, + } = await supabaseClient.auth.getUser(); + 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 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 'Content-Type: application/json' \ +// --data '{"name":"Functions"}'