Full-stack Dart with Flutter, Supabase and Dart Edge
Tags: dart, flutter, supabase, edge
Published: 2023-05-13 09:03:55

## Introduction

[Supabase](https://supabase.com) has recently had its [7th Launch Week](https://supabase.com/launch-week), with lots of new features and functionality shipped by the Supabase team as well as [the community](https://supabase.com/blog/launch-week-7-community-highlights).

One of the community highlights was [Dart Edge](https://docs.dartedge.dev/) for Supabase Edge Functions. Built by [Invertase](https://invertase.io), 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](https://supabase.com/docs/guides/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:

```bash
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](https://supabase.com/docs/guides/cli#installation).

Now, let's set up a Dart Edge project:

```bash
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:

```dart
// 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:

```bash
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](http://localhost:54321/functions/v1/dart_edge)

To deploy the function, we can build it and then run the following command:

```bash
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.

```bash
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:

```dart
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:

1. Return the counter's current value if we make a `GET` request.
    
2. 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.

```sql
-- 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:

```dart
// 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.

```bash
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:

```yaml
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:

```dart
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:

```dart
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](https://github.com/dartling/edge_counter).

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 at [dartling.dev](https://dartling.dev)!