Supabase Edge Functions in Dart

Supabase Edge Functions in Dart

We will cover briefly:

  1. Setting up Supabase
  2. Create an Edge Function using Dart
  3. Deploying and consuming the endpoint

Setting up Supabase

Before diving into Supabase, let’s provide an introduction to what Supabase is.

Supabase is an open-source alternative to Firebase that provides a set of backend services, including authentication, database, and storage. 

Supabase offers some features like

  • Auth service: for user authentication.
  • Database service: standard PostgreSQL.
  • Realtime service: sending messages and states to clients.
  • Edge functions: Running server-side TypeScript functions, distributed globally at the edge
  • Storage service: Storing and serving files.

Setting up Supabase

Pre-Requisite: Docker (you need to have it installed on your machine)

  • We will install Supabase CLI — a tool to develop your project locally and deploy it to the Supabase Platform.
npm install supabase --save-dev

Note: We need to prefix each command with npx (when installing through npm)

To proceed, we will create a Supabase project, which is a simple process. However, you must first create an account with Supabase.

Once done, run the below command and initialize your project

npx supabase login

Initialize Supabase to set up the configuration locally using

npx supabase init

Make sure Docker is running. Run the following command to start the Supabase services

npx supabase start

After starting all the Supabase services, you will receive an output showing your local Supabase credentials which include URLs and keys. You can use these credentials in your local project.

Supabase credentials
Supabase credentials

You can use the npx supabase stop command to stop all services and reset your local database.

  • After the setup, you can access the local Dashboard by visiting
http://localhost:54323

Additionally, you can use any Postgres client to access the database directly through the URL 

postgresql://postgres:postgres@localhost:54322/postgres

Supabase Edge Functions using Dart

Supabase Edge Functions using Dart
Supabase Edge Functions using Dart

Supabase Edge Functions is a serverless computing platform that allows you to run custom code on the edge. These functions run on the edge nodes, which are located close to your users and can provide faster response times. 

With Supabase Edge Functions, you can build and deploy functions using TypeScript and JavaScript. Additionally, Edge Functions can interact with other Supabase services, including the database, authentication, and storage services.

Edge Functions are developed using Deno

Support for Edge Functions

Dart Edge is a project that focuses on executing Dart code on Edge functions for platforms like Cloudflare Workers, Vercel Edge Functions, and Supabase Edge Functions

To get started let’s first install the edge CLI

dart pub global activate edge

and create a project using

edge new supabase_functions <name_of_your_project>
Barebones structure
Barebones structure

This is the basic structure of the project which we get from running the above command and inside the main.dart we get a simple Supabase Edge function

Simple Edge Function
Simple Edge Function

For running the edge functions locally, we do the following

npx supabase start # start the supabase stack
npx supabase functions serve # start the Functions watcher

Architecture Overview

We will modify our edge functions to call the YouTube APIs from inside them. This is how our architecture looks like:

Architecture Diagram
Architecture Diagram
  • On the left-hand side, we have our client which is our website.
  • The website loads for the first time and calls our edge function VideoFetcher 
  • This function fetches the data from our Postgres data hosted in the Supabase account
  • The response is cached inside the browser’s local storage and the subsequent requests are served from the browser’s cache
  • On the right-hand side, we have another edge function VideoSaver 
  • This function runs on demand and internally calls the YouTube API.
  • The response is transformed as per our video_items (more info below) schema and saved inside Postgres.

We will be creating two endpoints — VideoFetcher and VideoSaver

Dart Edge Functions
Dart Edge Functions

VideoFetcher Edge Function

In this edge function, we simply query our video_items table which is present inside our Postgres database. The result is returned (in the form of JSON) back to the client.

This is the schema for the video_items 

Video Items schema
Video Items schema

To fetch data from a PostgreSQL table in a Supabase Edge function, we can use the Supabase Dart client library to query the database. This client is present inside the package supabase 

import 'package:supabase/supabase.dart';

final supabase = SupabaseClient(
  'https://your-project-url.supabase.co',
  'your-anon-key',
  httpClient: EdgeHttpClient(),
);

We need to import the library and initialize a client instance with our Supabase credentials. Note, since Supabase functions are developed using Deno which allows us to retrieve values as

final supabaseUrl = Deno.env.get('SUPABASE_URL');
final supabaseServiceRole = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');

We pass the httpClientto the Supabase client as EdgeHttpClient, which is available in the supabase_functions library.

Next, we can use the select method of the client instance to execute a SQL query against the PostgreSQL database. 

For example, the following code will fetch all rows from a video_items table:

const corsHeaders = {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
};

final data = await supabase.from('video_items').select();

return Response.json(
   data,
   headers: edge.Headers(corsHeaders),
);

The function returns a JSON a response along with the required CORS headers, which are necessary when calling the endpoint from a client

Tip: To use CORS headers, you must import the edge_runtime package and access the Headers parameter.

Deploying and Testing 

We run the following commands to compile and deploy our function on Supabase Edge functions.

# Builds our supabase function
edge build supabase_functions

# Deploys supabase function
npx supabase functions deploy dart_edge --project-ref <PROJECT> --no-verify-jwt

The --no-verify-jwt flag is used to skip the verification of JSON Web Tokens (JWTs). By default, when a user logs in or signs up with Supabase authentication, a JWT is returned to the client and is used to authenticate future requests. The server verifies the JWT before processing the request. 

However, using the --no-verify-jwt flag means that the server will not verify the JWT. 

Tip: Don’t use --no-verify-jwt when deploying in production 

We can test our function by using the curl as

curl --request POST 'https://<project>.functions.supabase.co/dart_edge' \
--header 'Content-Type: application/json'

and this gives us the response from the video_items table 🎉🎉

VideoSaver Edge Function

In this edge function, we will access the YouTube APIs and store the response in our table video_items

But before doing that, we need 

  1. Create a project on Google Cloud, refer here
  2. Enable the YouTube API, refer here
Pre-Requisite

Tip: To avoid receiving a high monthly bill for cloud services: you can limit the amount of usage allowed for the APIs.

The quota for the YouTube API
The quota for the YouTube API

Going through the API documentation, it appears that the Searchfeature is the most important one we need. Within this feature, there is a method called listwhich is described in the documentation.

Returns a collection of search results that match the query parameters specified in the API request. By default, a search result set identifies matching video, channel, and playlist resources, but you can also configure queries to only retrieve a specific type of resource.

The response from the above method looks like this

{
  "kind": "youtube#searchResult",
  "etag": etag,
  "id": {
    "kind": string,
    "videoId": string,
    "channelId": string,
    "playlistId": string
  },
  "snippet": {
    "publishedAt": datetime,
    "channelId": string,
    "title": string,
    "description": string,
    "thumbnails": {},
    "channelTitle": string,
    "liveBroadcastContent": string
  }
}

Note: The parameter definitions are described here

One of the important parameters above is the 

  • videoId : Its value will contain the ID that YouTube uses to uniquely identify a video that matches the search query.

Calling YouTube API

In order to interact with the Google services, authentication and Service account credentials are required. To acquire these credentials, our initial step is to produce a key within our cloud project.

Head over to the IAM and admin -> Service accounts -> Add KEY -> Create new key

IAM Keys
IAM Keys

This will generate a key and download it to your local machine. The values contained within this key will be necessary in order to obtain the Service account credentials.

Inside our edge function, we import the googleapis and googleapis_auth

# Generated Dart libraries for accessing Google APIs.
dart pub get googleapis

# Provides support for obtaining OAuth2 credentials to access Google APIs
dart pub get googleapis_auth
  • Next, we create a new file called video_fetcher.dart and import the above packages. 
  • We create a function called videoFetcher which will call the YouTube APIs
Future<List<SearchResult>?> videoFetcher() async {
  const channelId = '<CHANNEL_ID';

  final credentials = ServiceAccountCredentials.fromJson({
    "private_key_id": "<VALUE>",
    "private_key": "<VALUE>",
    "client_email": "<VALUE>",
    "client_id": "<VALUE>",
    'type': 'service_account',
  });

  final client = await clientViaServiceAccount(credentials, [
    'https://www.googleapis.com/auth/youtube.readonly',
  ]);

  final youtube = YouTubeApi(client);

  final searchResult = await youtube.search.list(
    ['id,snippet'],
    channelId: channelId,
    maxResults: 20,
    order: 'date',
    pageToken: nextPageToken.isNotEmpty ? nextPageToken : null,
    type: ['video'],
    eventType: 'none',
  );

  client.close();
  return searchResult.items!;
}
  • To start with, we need to obtain the channel IDfor the specific YouTube channel (a quick Google search will help) from which we desire to retrieve videos. 
  • Next, we obtain the service account credentials utilizing the parameters that were obtained from the previously downloaded file.
  • To authorize the API client with the Service account credentials, we must obtain OAuth2 credentials. This is achieved by calling the function clientViaServiceAccountwhich takes two parameters: the necessary Service account credentials and a list of scopes. In this particular scenario, we will be utilizing the youtube.readonlyscope. Additional information regarding scopes can be found here
  • We create a YouTube API a client that provides access to YouTube data, such as videos, playlists, and channels.
  • We are interested in calling the Searchfeature which contains a method called list giving us access to the matching videoresources

Note: The parameters mentioned above were adjusted to suit our specific use case. You have the option to modify them to fit your own requirements. As an example, we set the maxResults parameter to 20

  • To conclude, we close both the BrowserOAuth2Flow object and the HTTP Client that it’s utilizing by invoking the close method. Once this is done, any subsequent calls to clientViaUserConsent objects will fail.
  • And we return the search results back to the calling function

Invoking the VideoFetcher

Inside our dart_edge/video_cron endpoint we create a method called handleRequest which will invoke the above created videoFetcher function

final videoData = await handleRequest();

Future<List<SearchResult>?> handleRequest() async {
  final videoResponse = await http.runWithClient(() async {
    final data = await videoFetcher();
    return data;
  }, () => EdgeHttpClient());

  return videoResponse;
}

Within the function, we first call http.runWithClientwhich is a method that facilitates making requests with an HTTP client

Since we need to make an HTTP request, we import the package HTTP

dart pub add http

We call in our function videoFetcherand the results are sent back to the callee.

case '/dart_edge/video_cron':
   final videoData = await handleRequest();

   for (var video in videoData!) {
      final videoId = video.id?.videoId;
      final isVideoPresent = await doesRowExist(supabase, videoId!);

      if (!isVideoPresent) {
        await supabase.from('video_items').upsert(
           {
             'channelId': video.snippet?.channelId,
             'title': video.snippet?.title,
             'description': video.snippet?.description,
             'channelTitle': video.snippet?.channelTitle,
             'etag': video.etag,
             'videoId': videoId,
           },
         );
        }
       }

       final data = {
         'data': 'processed',
       };

   return Response.json(
      data,
      headers: edge.Headers(corsHeaders),
      status: HttpStatus.accepted,
   );

The videoData received is in the form of a List of search results. We loop over each item and then get its videoId

Sample Data inside the table
Sample Data inside the table

The next step involves the creation of a function doesRowExistwhich verifies if a row with a specific condition exists in the table. In this case, we are checking whether a row with the videoId exists, as it is a unique identifier for every video result. We can use the eq operator which is called with the videoId as its argument

Future<bool> doesRowExist(SupabaseClient supabase, String videoId) async {
  final res = await supabase
        .from('video_items')
        .select()
        .eq('videoId', videoId)
        .single();

  final id = res['videoId'] as String;
  return id == videoId;
}

In case the video does not exist, we proceed to insert the data such as channelId, title, description, etc . inside our table with the help of supabase client instances upsert method.

upsert method is used to insert a new record or update an existing record in a table. It is a combination of insert and update operations. If a record with the specified primary key already exists in the table, it is updated, otherwise a new record is inserted.

After adding the video, we create a data object with a processed key-value pair and return it as a JSON response with CORS headers. 

The status code of the response is set to HttpStatus.accepted, which means the request has been accepted and will be processed later.

Testing the function

We can test our function by using the curl as

curl --request POST 'https://<project>.functions.supabase.co/dart_edge/video_cron' \
--header 'Content-Type: application/json'

and this saves the latest 20 videos from the Supabase YT channel inside the video_items table 🎉🎉

Integrating with client

We create a react app and integrate our dart edge endpoints inside it. And the final result (SupaVidFetcher) looks like this. 

The client request first hits the dart edge endpoint and then the result is cached in the local storage of the browser. The user can also search for videos by entering text into the search bar.

URL: supabase.flatteredwithflutter.com

SupaVidFetcher