Home/Articles/Working with Supabase Edge Functions for Basejump

Supabase

Working with Supabase Edge Functions for Basejump

As a NextJS enthusiast, I often rely on it for most of my projects. However, recently, I have been exploring other frameworks like Remix for web and SwiftUI for iOS. It's got me thinking how cool it would be to leverage Basejump more as a headless starter kit, that way you could bring your own front end.

Unfortunately, when the first version of Basejump was created, Supabase did not support developing with multiple Edge Functions at once. It made working with it locally nearly impossible. But since launch week 7, Supabase now provides full support for local Edge Functions. I decided to take the weekend and migrate Basejump over so that I could begin the process of making a headless version. Here's what I learned.

You can follow along the Basejump code changes from this article here .

What we'll cover

Creating and running Edge Functions locally

Creating Edge Functions locally is pretty straight forward.

  1. Make sure you have the latest version of the Supabase CLI installed
  2. Run supabase functions new my-function-name.
  3. Your function will be created inside the ./supabase/functions/my-function-name folder.

Your functions will be run automatically when you run supabase start. You can also run them manually with supabase functions serve. Running them manually allows you to pass additional configuration such as local environment variables or an import map.

Calling Edge Functions from your front end

When you create a new Edge Function, Supabase will automatically create a new route for you. Those routes are callable via the API, but there are also some nice convenience functions in the SDK's that wire up the authentication for you.


import {supabase} from "./supabaseClient";

const {data, error} = supabase.functions.invoke('my-function-name', {param1: 'value1', param2: 'value2'});

Sharing code between Edge Functions

Your Edge Functions will all be located inside of your ./supabase/functions folder. Because functions are written in Deno, you'll want to maintain a _shared folder for any shared code leveraged between functions. This is particularly useful for things like your supabase client. Here's an example of a service/admin client

import {createClient} from "https://esm.sh/@supabase/supabase-js@2";
import {Database} from "./types/supabase.ts";

const supabaseAdmin = createClient<Database>(
    Deno.env.get("SUPABASE_URL") as string,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") as string
);

export default supabaseAdmin;

Setting up your IDE for Deno support

Supabase functions are written in Deno (or recently Dart, but this guide focuses primarily on Deno). Leveraging Deno gives the Supabase team some nice security benefits out of the box - unfortunately it also creates a bit of a learning curve for folks new to Deno. Likely the first thing you'll notice is that your IDE will show all sorts of lint issues - this is particularly the case if you're sharing a project root between a frontend client and your Supabase folder.

Setting up VS Code for Deno

  1. Install the Deno plugin for VS Code.
  2. Update your local project file to configure explicit paths for Deno. This will allow you to use normal JS/Typescript linting in the rest of your project
// .vscode/settings.json
{
  "deno.enable": true,
  "deno.unstable": true,
  "deno.enablePaths": [
    "supabase/functions"
  ]
}

Setting up Webstorm for Deno

  1. Install the Deno plugin for Webstorm.
  2. Update your local project file to configure explicit paths for Deno. This will allow you to use normal JS/Typescript linting in the rest of your project
// Preferences -> Languages & Frameworks -> Deno -> Init command
{
  "lint": true,
  "unstable": true,
  "importMap": "import_map.json",
  "config": "deno.json",
  "enablePaths": [
    "./supabase/functions"
  ]
}
  1. Enable Deno support for your project
  2. Restart Webstorm

Setting up your functions to support CORS

By default, Supabase functions do not support CORS. The JS client, however, makes cross domain requests - so you'll need to get that handled before you can start working with your functions. You'll also want to make sure to set a suitable CORS cache header to avoid unnecessary requests.

// supabase/functions/_shared/cors.ts
export const corsHeaders = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
    "Access-Control-Max-Age": 86400
};

Next, you'll import this into each of your functions and leverage it for OPTIONS requests

// supabase/functions/my-function-name/index.ts
import {corsHeaders} from "../_shared/cors.ts";

serve(async (req) => {
    if (req.method === "OPTIONS") {
        return new Response("ok", {headers: corsHeaders});
    }
    // the rest of your function
});

Working with NPM dependencies

Deno has no package manager, instead it imports dependencies directly from the web. That can cause some challenges when working with NPM modules which depend on your node_modules folder to resolve dependencies.

There are some CDNs that attempt to resolve this, but I've had mixed results. Ideally, it's best to avoid using external dependencies as much as possible. But when you do need to, there are some options.

The most popular CDN for importing npm modules as Deno packages is esm.sh. You can import any package from npm using the following format:

import {createClient} from "https://esm.sh/@supabase/supabase-js@2";

Working with a local import map

To clean up some of your inputs and help manage versions a bit better, you can create an import_map.json file located at ./supabase/functions/import_map.json. This will allow you to import your dependencies like so:

// supabase/functions/import_map.json
{
  "imports": {
    "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2"
  }
}
import {createClient} from "@supabase/supabase-js";

Your import_map.json file will be automatically loaded by Supabase as long as it's located in the ./supabase/functions folder. If you have a custom location, you can pass it in as a flag when starting your functions locally or deploying them.

supabase functions serve --import-map ./my-custom-location/import_map.json
supabase functions deploy my-function-name --project-ref MY-PROJECT-ID --import-map ./my-custom-location/import_map.json

Using environment variables locally and in production

By default, Supabase will inject some useful environment variables into your Edge Functions. You can check them out here. To add your own, create a file at ./supabase/.env in the following format:

MY_FUNCTION_SECRET=supersecret

Next, startup your functions with supabase functions serve --env-file ./.env. You can now access your environment variables in your functions with Deno.env.get("MY_FUNCTION_SECRET").

Using environment variables in production

To use custom environment variables in production, add them to your linked project by running supabase secrets set MY_FUNCTION_SECRET=supersecret.

Deploying Edge Functions to Supabase

To deploy your functions, run supabase functions deploy my-function-name --project-ref MY-PROJECT-ID. You can get your project ID from the Supabase dashboard. Functions must be deployed one at a time.

Additional Resources

I found the following guides helpful when working through the migration - particularly the documentation and Youtube series. Feel free to ping me on Twitter If you have more to add!