--- date: 2023-10-03 title: "Secure your Supabase functions with Unkey" description: "Learn how to use Unkey to secure your Supabase functions" author: james tags: ["product"] --- Supabase offers [edge functions](https://supabase.com/docs/guides/functions) built upon Deno. They have a variety of uses for applications like OpenAI or working with their storage product. In this post, we will show you how to use Unkey to secure your function in just a few lines of code. ## What is Unkey? Unkey is an open source API management platform that helps developers secure, manage, and scale their APIs. Unkey has built-in features that can make it easier than ever to provide an API to your end users, including: - Per key rate limiting - Limited usage keys - Time-based keys - Per key analytics ## Prerequisites 1. Create a [Supabase account](https://supabase.com) 2. Create a [Unkey account](https://unkey.dev) and follow our [Quickstart guide](https://unkey.dev/docs/quickstart). So you have an API key to verify. 3. Setup [Supabase CLI](https://supabase.com/docs/guides/cli/local-development) for local development. ## Create our project ### Create a project folder First, we need to create a folder. Let's call that `unkey-supabase`. This will be where our supabase functions exist going forward. ```bash mkdir unkey-supabase && cd unkey-supabase ``` ### Start Supabase services Now, we have a folder for our project. We can initialize and start Supabase for local development. ```bash supabase init ``` Make sure Docker is running. The `start` command uses Docker to start the Supabase services. This command may take a while to run if this is the first time using the CLI. ```bash supabase start ``` ### Create a Supabase function Now that Supabase is setup, we can create a Supabase function. This function will be where we secure it using Unkey. ```bash supabase functions new hello-world ``` This command creates a function stub in your Supabase folder at `./functions/hello-world/index.ts`. This stub will have a function that returns the name passed in as data for the request. ```tsx title="./functions/hello-world/index.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts" console.log("Hello from Functions!") serve(async (req) => { const { name } = await req.json() const data = { message: `Hello ${name}!`, } return new Response( JSON.stringify(data), { headers: { "Content-Type": "application/json" } }, ) }) ``` #### Test your Supabase function Before making any changes, let's ensure your Supabase function runs. Inside the function, you should see a cURL command similar to the following: ```bash curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ --header 'Content-Type: application/json' \ --data '{"name":"hello-world"}' ``` After invoking your Edge Function, you should see the response `{ "message":"Hello Functions!" }`. > If you receive an error Invalid JWT, find the `ANON_KEY` of your project in the Dashboard under Settings > API. ## Add Unkey to secure our Supabase function ### Add `verifyKey` to our function Now that we have a function, we must add Unkey to secure the endpoint. Supabase uses Deno, so instead of installing our npm package, we will use ESM CDN to provide the `verifyKey` function we need. ```jsx {2} title="./functions/hello-world/index.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { verifyKey } from "https://esm.sh/@unkey/api"; ``` ### What does `verifyKey` do? Unkey's `verifykey` lets you verify a key from your end users. We will return a result and you can decide whether to give the user access to a resource or not based upon that result. For example, a response could be: ```json { "result": { "valid": true, "ownerId": "james", "meta": { "hello": "world" } } } ``` ### Updating our Supabase function First, let's remove the boilerplate code from the function so we can work on adding Unkey. ```jsx title="./functions/hello-world/index.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { verifyKey } from "https://esm.sh/@unkey/api"; serve(async (req) => { }) ``` Next, we will wrap the `serve` function inside a try-catch. Just in case something goes wrong, we can handle that. ```jsx title="./functions/hello-world/index.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { verifyKey } from "https://esm.sh/@unkey/api"; serve(async (req) => { try { // handle our functions here. } catch (error) { // return a 500 error if there is an error with a message. return new Response(JSON.stringify({ error: error.message }), { status: 500, }); } }) ``` #### Check headers for API Key Inside our try, we can look for a header containing the user's API Key. In this example we will use `x-unkey-api-key` but you could call the header whatever you want. If there is no header will immediately return 401. ```jsx title="./functions/hello-world/index.ts" import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { verifyKey } from "https://esm.sh/@unkey/api"; serve(async (req) => { try { const token = req.headers.get("x-unkey-api-key"); if (!token) { return new Response("Unauthorized", { status: 401 }); } } catch (error) { // return a 500 error if there is an error with a message. return new Response(JSON.stringify({ error: error.message }), { status: 500, }); } }) ``` ### Verifying the key The `verifyKey` function returns a `result` and `error`, making the logic easy to handle. Below is a simplified example of the verification flow. ```tsx const { result, error } = await verifyKey("key_123"); if (error) { // handle potential network or bad request error // a link to our docs will be in the `error.docs` field console.error(error.message); return; } if (!result.valid) { // do not grant access return; } // process request console.log(result); ``` Now you have a basic understanding of verification, let's add this to our Supabase function. ```tsx title="./functions/hello-world/index.ts" serve(async (req) => { try { const token = req.headers.get("x-unkey-api-key"); if (!token) { return new Response("No API Key provided", { status: 401 }); } const { result, error } = await verifyKey(token); if (error) { // handle potential network or bad request error // a link to our docs will be in the `error.docs` field console.error(error.message); return new Response(JSON.stringify({ error: error.message }), { status: 400, }); } if (!result.valid) { // do not grant access return new Response(JSON.stringify({ error: "API Key is not valid for this request" }), { status: 401, }); } return new Response(JSON.stringify({ result }), { status: 200 }); } ``` ### Testing our Supabase function We can send a curl request to our endpoint to test this functionality. Below is an example of the curl to send. Remember, we now need to include our API key. ```bash curl -XPOST -H 'Authorization: Bearer ' \ -H 'x-unkey-api-key: ' \ -H "Content-type: application/json" 'http://localhost:54321/functions/v1/hello-world' ``` ## Adding CORS for added security Adding CORS allows us to call our function from the frontend and decide what headers can be passed to our function. Inside your `functions` folder, add a file called `cors.ts`. Inside this cors file, we will tell the Supabase function which headers and origins are allowed. ```tsx title="./functions/cors.ts" export const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, x-unkey-api-key, content-type", }; ``` ## Conclusion In this post, we have covered how to use Unkey with Supabase functions to secure them. You can check out the code for this project in our [Examples folder](https://github.com/unkeyed/examples/tree/main/supabase-functions)