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.
Creating Edge Functions locally is pretty straight forward.
supabase functions new my-function-name
../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.
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'});
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;
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.
// .vscode/settings.json
{
"deno.enable": true,
"deno.unstable": true,
"deno.enablePaths": [
"supabase/functions"
]
}
// Preferences -> Languages & Frameworks -> Deno -> Init command
{
"lint": true,
"unstable": true,
"importMap": "import_map.json",
"config": "deno.json",
"enablePaths": ["./supabase/functions"]
}
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
});
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";
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
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")
.
To use custom environment variables in production, add them to your linked project by running supabase secrets set MY_FUNCTION_SECRET=supersecret
.
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.
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!