Introduction
Supabase has recently had its 7th Launch Week, with lots of new features and functionality shipped by the Supabase team as well as the community.
One of the community highlights was Dart Edge for Supabase Edge Functions. Built by Invertase, Dart Edge allows you to write your edge functions in Dart. It supports Vercel, Cloudflare Workers, and now Supabase Edge Functions, which until recently only supported writing functions with TypeScript. However, it is still experimental!
Being able to write our edge functions with Dart is amazing news for Flutter developers. It allows us to write Dart for both the front-end and any back-end code we need, which also means we can share Dart code, such as the request/response models. And while Supabase's database functions can be quite powerful, an edge function can do additional things such as call third-party APIs and services, and even bypass RLS policies if necessary.
In this post, we will explore Dart Edge with Supabase Edge Functions. We will build a simple app with Dart on the full stack, using a Flutter app front-end and a Dart Edge function as our back-end. We'll keep things simple by starting with the classic Flutter counter app but using an edge function to get the current count and increment it.
Set up an edge function
First, we need both the Supabase CLI and edge CLI. For the edge CLI, simply run:
dart pub global activate edge
The Supabase CLI can be installed through NPM or either Homebrew or Scoop depending on your operating system, so check out the documentation here.
Now, let's set up a Dart Edge project:
edge new supabase_functions edge_functions
cd edge_functions
supabase init
edge_functions
is the name of our project and the name of the newly created directory. We run supabase init
to generate the Supabase config.
The newly generated edge_functions
directory is a typical Dart project with a pubspec.yaml
file with the required dependencies. It has a main.dart
file with a simple response to get us started:
// edge_functions/lib/main.dart
void main() {
SupabaseFunctions(fetch: (request) {
return Response("Hello from Supabase Edge Functions!");
});
}
To run the edge function locally, we need to build the function and run Supabase:
edge build supabase_functions
supabase start
supabase functions serve dart_edge --no-verify-jwt
We can also use the --dev
flag when running edge build
to recompile when we make any changes to the code.
By default, the edge function is expected to have a valid authorization header. We pass the --no-verify-jwt
flag so we can run the function without one.
We can now access the local edge function at http://localhost:54321/functions/v1/dart_edge
To deploy the function, we can build it and then run the following command:
supabase functions deploy dart_edge
Connect to Supabase from the edge
The current edge function only returns some text in the response. In a real app, we would likely want to interact with our database, make some third-party service calls, or both.
dart pub add supabase
dart pub add edge_http_client
The supabase
package has all we need to interact with the Supabase database, but we also need to use a special HTTP client.
We can now instantiate a Supabase client:
final supabase = SupabaseClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
httpClient: EdgeHttpClient(),
);
A couple of notes on the above snippet:
The Supabase credentials are already available by default in the environment variables.
The service role key bypasses Row Level Security, meaning we can access and edit all data of all users. Since this is for an edge function, it's fine, but we could also use the anon key (
SUPABASE_ANON_KEY
environment variable) if that's a concern.
We now have access to the Supabase database from our edge function. Let's modify the function's logic to do two things:
Return the counter's current value if we make a
GET
request.Increment the counter by 1 if we make a
POST
request.
We'll first need to create the database table that will hold our increments. These SQL statements are added to the seed.sql
file.
-- edge_functions/supabase/seed.sql
create table increments (
id bigint generated by default as identity primary key,
amount int default 0 not null
);
create function get_count()
returns numeric as $$
select sum(amount) from increments;
$$ language sql;
We also create a SQL function that returns the current count by summing up all the increments.
Now let's update our edge function's main
method:
// edge_functions/lib/model/counter_response.dart
class CounterResponse {
final int count;
CounterResponse(this.count);
factory CounterResponse.fromJson(Map<String, dynamic> json) =>
CounterResponse(json['count'] as int);
Map<String, dynamic> get toJson => {'count': count};
}
// edge_functions/lib/main.dart
void main() {
SupabaseFunctions(fetch: (request) async {
final client = SupabaseClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
httpClient: EdgeHttpClient(),
);
if (request.method == 'POST') {
await client.from('increments').insert({'amount': 1});
}
final count = await client.rpc('count').single() as int;
final response = CounterResponse(count);
return Response.json(response.toJson);
});
}
If the request is a POST
, we increment the count by 1. For all requests, we fetch the current count and return it in the response. To fetch the count, we're calling a SQL function within an edge function!
Call the function from Flutter
With the edge function ready to go, we will now create the Flutter counter app, and update it so that the count display and increment button work by calling the edge function.
flutter create app
We'll be calling the function with the http
package, but we're also going to need to add the edge_functions
package as a local dependency. This is so we can reuse the CounterResponse
model.
Here are the current dependencies (pubspec.yaml
) for the Flutter app:
dependencies:
flutter:
sdk: flutter
counter_edge_functions:
path: '../edge_functions'
http: ^0.13.5
At the moment of writing, there seems to be a dependency issue due to the path
package, so I had to remove flutter_test
as a dev dependency to get this to work. A nicer fix for this would be to move the model to a separate local package instead and have it as a dependency for both the edge function package and the Flutter app.
Let's also encapsulate the interactions with the edge function to a repository:
import 'dart:convert';
import 'package:counter_edge_functions/model/counter_response.dart';
import 'package:http/http.dart' as http;
class CounterRepository {
static final _functionUrl =
Uri.parse('http://localhost:54321/functions/v1/dart_edge');
Future<CounterResponse> get count async {
final response = await http.get(_functionUrl);
final body =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
return CounterResponse.fromJson(body);
}
Future<CounterResponse> increment() async {
final response = await http.post(_functionUrl);
final body =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
return CounterResponse.fromJson(body);
}
}
The final piece is to call the repository methods from our widgets. There are not that many changes compared to the generated counter app code from the template, but here is the final result:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final repository = CounterRepository();
return MaterialApp(
title: 'Edge Counter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// Fetch the initial count from the Edge Function.
home: FutureBuilder<CounterResponse>(
future: repository.count,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox();
}
return MyHomePage(
title: 'Edge Counter',
initialCount: snapshot.data?.count ?? 0,
onIncrement: repository.increment,
);
},
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
required this.title,
required this.initialCount,
required this.onIncrement,
});
final String title;
final int initialCount;
final Future<CounterResponse> Function() onIncrement;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late int _counter;
// Increment through the Edge Function, and update with the latest count.
Future<void> _incrementCounter() async {
final response = await widget.onIncrement();
setState(() {
_counter = response.count;
});
}
@override
void initState() {
super.initState();
_counter = widget.initialCount;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Users have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
We wrap the home page in a FutureBuilder
to start with the current counter value, and call the increment method on button click. The count persists across restarts, and if we were using a deployed function rather than a local one, we could even share the app with friends and have everyone increment it at the same time!
Wrapping up
In this post, we talked about the experimental Dart Edge, allowing us to write Dart on the full stack - by using Flutter and Supabase. We created a very simple edge function that interacts with our database, and an even simpler Flutter counter app that calls the edge function to increment the counter.
You can find the full source code here.
If you found this helpful and would like to be notified of future tutorials, please follow the blog or subscribe to the newsletter with your email below!