Secure your Supabase functions with Unkey

Learn how to use Unkey to secure your Supabase functions

Written by

James Perkins

Published on

Supabase offers edge 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
  2. Create a Unkey account and follow our Quickstart guide. So you have an API key to verify.
  3. Setup Supabase CLI 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.

 1
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.

 1
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.

 1
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.

 1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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:

 1
 2
 3
 4
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.

 1
 2
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
{
  "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.

 1
 2
 3
 4
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.

 1
 2
 3
curl -XPOST -H 'Authorization: Bearer <SUPBASE_BEARER_TOKEN>' \
-H 'x-unkey-api-key: <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.

 1
 2
 3
 4
 5
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

Protect your API.
Start today.

2500 verifications and 100K successful rate‑limited requests per month. No CC required.