FlutterFlow + PowerSync

Integration guide for creating offline-first apps with FlutterFlow and PowerSync with Supabase as the backend.

This is our new and improved guide that only requires using Custom Actions to integrate with PowerSync. Using GitHub is not required.

The guide takes you through building a basic app from scratch. The app lets you manage a list of items. You should then be able to use this knowledge to build/extend your own app.

Used in conjunction with FlutterFlow, PowerSync enables developers to build offline-first apps that are robust in poor network conditions and that have highly responsive frontends while relying on Supabase for their backend. This guide provides instructions for how to configure PowerSync for use with your FlutterFlow project that has Supabase integration enabled.

Guide Overview

Before you proceed, this guide assumes that you have already signed up for free accounts with both Supabase and PowerSync. If you haven't signed up for a PowerSync account yet, click here (and if you haven't signed up for Supabase yet, click here). This guide also assumes that you already have Flutter set up.

This guide also requires FlutterFlow Local Run, so be sure to download and install that.

This guide takes 30-40 minutes to complete.

  1. Configure Supabase and PowerSync prerequisites

  2. Initialize your FlutterFlow project

  3. Build a sign-in screen

  4. Initialize PowerSync

  5. Reading data

  6. Creating data

  7. Deleting data

  8. Signing out

  9. Securing your app

    1. Enable RLS in Supabase

    2. Update Sync Rules in PowerSync

Configure Supabase

  1. Create a new project in Supabase.

  2. PowerSync uses the Postgres Write Ahead Log (WAL) to replicate data changes in order to keep PowerSync SDK clients up to date.

    Run the below SQL statement in your Supabase SQL Editor:

create table
  public.lists (
    id uuid not null default gen_random_uuid (),
    created_at timestamp with time zone not null default now(),
    name text not null,
    owner_id uuid not null,
    constraint lists_pkey primary key (id),
    constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade
  ) tablespace pg_default
  1. Create a Postgres publication using the SQL Editor. This will enable data to be replicated from Supabase so that your FlutterFlow app can download it.

create publication powersync for table public.lists;

Note: this guide uses the default postgres user in your Supabase account for replicating changes to PowerSync, since elevating custom roles to replication has been disabled in Supabase. If you want to use a custom role for this purpose, contact the Supabase support team.

Note: this is a static list of tables. If you add additional tables to your schema, they must also be added to this publication.

Configure PowerSync

Connect PowerSync to Your Supabase

  1. In the PowerSync dashboard Project tree, click on "Create new instance":

  1. Give your instance a name, such as "Supabase Testing".

  2. Under the "General" tab, you can change the default cloud region from US to EU or JP if desired (more cloud regions are available, contact us if you need a different region).

  3. Under the "DB Connections" tab, click on the + icon.

  4. Now we get the connection details from Supabase:

    • In your Supabase dashboard, navigate to "Project Settings" -> "Database" -> "Connection string" and select the "URI" tab.

    • Uncheck the "Display connection pooler" checkbox. PowerSync needs to connect to the database directly and cannot use the pooler.

    • Copy the connection string. The hostname should be db.<PROJECT-ID>.supabase.co, and not, for example, aws-0-us-west-1.pooler.supabase.com.

    • Paste this URI in PowerSync the instance URI field.

    • Enter the password for the postgres user in your Supabase database: (Supabase also refers to this password as the database password or project password).

    • PowerSync has the Supabase CA certificate pre-configured — verify-full SSL mode can be used directly, without any custom certificates.

  5. Click "Test Connection" and fix any errors.

  6. Under the "Client Auth" tab, enable "Use Supabase Auth".

  7. Click "Save".

PowerSync deploys and configures an isolated cloud environment for you, which will take a few minutes to complete.

Configure Sync Rules

Sync Rules allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For the demo app, we're going to specify that each user can only see their own to-do lists and list items.

1. To update your Sync Rules, open the sync-rules.yaml file.

  1. Replace the sync-rules.yaml file's contents with the below:

This will sync the entire table to all users - we will refine this later
bucket_definitions:
    global:
        data: 
            - SELECT * FROM lists

For additional information on PowerSync's Sync Rules, refer to the Sync Rules documentation.

If you're wondering how Sync Rules relate to Supabase Postgres RLS, see this subsection.

Initialize Your FlutterFlow Project

  1. Create a new Blank app, give it a name, and disable Firebase.

  2. Under "App Settings" -> "Integrations", enable Supabase. Enter your "API URL" and "Anon Key" and click "Get Schema".

  3. Under "App Values" -> "Constants", click "Add App Constant".

    1. For Constant Name, enter PowerSyncUrl.

    2. For Constant Value, follow these steps:

      1. Click "Edit instance".

      2. Hover over "Instance URL" to copy the value:

You should now see this under App Constants:

Build A Sign-In Screen

  1. Under Pages, click "Add Page, Component or Flow".

  2. Select the Auth1 template and name the page "Login".

  3. Under "App Settings" -> "App Settings" -> "Authentication":

    1. Enable Authentication.

    2. Set Supabase as the Authentication Type.

    3. Set the Login page you just created as the Entry Page.

    4. Set HomePage as the Logged In Page:

  4. In your Supabase Dashboard, under "Authentication", click on "Add User" -> "Create new user" and create a user for yourself to test with:

  5. Launch your app on a physical or simulator device:

Checkpoint: you should now be able to log into the app using the Supabase user account you just created. After logging in you should see a blank screen.

Initialize PowerSync

  1. Click on "Custom Code" -> "Add" -> "Action".

  2. Name the Custom Action initpowersync.

    1. NOTE: use all lowercase for this Custom Action is important due to naming conversion that FF performs behind the scenes.

  3. Import your schema:

    1. Paste this into your Custom Action code on line 27 after the equals sign.

    2. Due to a limitation in FF, you now need to prefix each instance of Schema, Column and Table with powersync.

    3. Your custom action schema definition should now look like this:

  4. Under "Action Settings" on the right, add this dependency into "Pubspec Dependencies": powersync: ^1.4.0

  5. Still in Custom Actions, under "Custom Files" click on main.dart and set your new Custom Action as a Final Action and click Save.

Checkpoint: You should now be able to validate that PowerSync is initializing correctly by taking these steps:

  1. Stop any running simulator app sessions

  2. Restart the app by clicking "Test", and sign in

  3. Click on "Open Device Logs"

  4. You should see this kind of log message:

flutter: [PowerSync] FINE: 2024-04-16 13:47:52.259974: Credentials: PowerSyncCredentials<endpoint: https://659c4a069113052073717700.powersync.journeyapps.com userId: null expiresAt: null>
flutter: [PowerSync] FINE: 2024-04-16 13:47:52.607802: Applied checkpoint 2

Reading Data

We will now create our first UI and bind it to the data in the local SQLite database on the device.

Create a Custom Action to Stream all Lists

For watched (realtime) queries in FlutterFlow, you need 2x Custom Actions per table. For delete, update and insert queries you only need 1x Custom Action. We are working to see if we can aleviate this constraint.

  1. Create a new Custom Action and call it watchLists and paste the below code:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:async';

Future<void> watchLists(
    Future Function(List<ListsRow>? result) callback) async {
  var stream = db.watch('SELECT * FROM lists');
  listsSubscription?.cancel(); //it's important to clean up any existing subscriptions otherwise app performance will degrade
  listsSubscription = stream.listen((data) {
    callback(
        data.map((json) => ListsRow(Map<String, dynamic>.from(json))).toList());
  });
}
  1. Your Action Arguments should now look as follows:

  2. Create the second Custom Action called getLists and paste the following code into it:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future<List<ListsRow>?> getLists(List<ListsRow>? results) async {
  return results;
}
  1. Your Action Arguments should now look as follows:

  2. On the HomePage page, you will create a placeholder Page State variable required for the next step.

    1. Click on State Management.

  3. Still on the HomePage page, select Actions and open "Action Flow Editor".

    1. Add the watchLists Custom Action.

    2. Click "Open" to edit the callback Argument for watchLists.

    3. Add the getLists Custom Action and set the results Action Argument to result and click "Confirm":

    4. Set the "Action Output Variable Name" to allLists and you should now see this:

    5. Add a second action to the chain, and set it to "Update Page State" and "Rebuild Current Page". This is to ensure the page gets redrawn when the database updates. Your callback action should now look like this:

    6. Click "Close" to exit the Action Flow Editor.

  4. In the UI Builder on the HomePage page, add a ListView component and add a ListTile inside the ListView.

  5. On the ListView component, click "Generate Dynamic Children". Enter a variable name of boundLists and set its value to allLists (no further changes). Click Save.

  6. On the ListTile component, set the Title field to "Set from Variable" and then get the name field from the boundLists variable:

  1. Do the same for the Subtitle field of the ListTile component, and set it to created_at.

  2. Hot reload your app and the screen will still be blank. This is because the lists table is empty in Supabase. Create a test row in the table by clicking on "Insert" -> "Insert Row" in your Supabase Table Editor.

    1. Leave id and created_at blank.

    2. Enter a name such as "Test from Supabase".

    3. Click "Select Record" for owner_id and select your test user.

Checkpoint: you should now see your single test row magically appear in your app

Creating Data

You will now update the app so that we can capture new list entries.

  1. Create a new Custom Action called createListItem and paste the following code:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!
import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future createListItem(String name) async {
  var supaUserId = await Supabase.instance.client.auth.currentUser?.id;

  final results = await db.execute('''
      INSERT INTO
        lists(id, created_at, name, owner_id)
        VALUES(uuid(), datetime(), ?, ?)
      ''', [name, supaUserId]);
}
  1. There should now be one argument for the Custom Action called name of type String and not nullable.

  2. In the Widget Tree view, select the HomePage page and navigate to State Management.

  3. Create a new State Field called fabClicked and set the type to boolean and toggle the "Initial Field Value" toggle twice to initialize the field to false.

  4. In the Widget Tree view, drop a Floating Action Button (FAB) onto the page.

  5. Click on the FAB and Open the Action Flow Editor.

  6. Add an action to Update Page State.

  7. On the Widget Palette again, add a Container child to the Column Widget.

    1. Now add a Column Widget to this Container.

    2. Add a TextField and a Button to this Column Widget.

  8. Set the Container and TextField widgets to have a width of 100%.

  9. Change the Button text to "Add".

  10. Open the Action Flow Editor for the Add button:

    1. Add a Custom Action call to createListItem.

    2. Set the "name" Argument to Widget State -> TextField 1.

    3. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField_1 field.

    4. Chain another Action to Update Page State and set fabClicked to false.

    5. Your Action Editor should now look like this:

Checkpoint: you should now be able hot reload your app, click on the FAB button and the TextField should appear. Enter a name and click Add. The new row should appear in the ListView and the TextField should be hidden again.

Deleting Data

In this section we will add the ability to swipe on a ListTile to delete it.

  1. Create a new Custom Action called deleteListItem and paste the below code:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future deleteListItem(ListsRow row) async {
  await db.execute('DELETE FROM lists WHERE id = ?', [row.id]);
}
  1. In the Widget Tree select the ListTile and enable Slidable.

Checkpoint: Stop and relaunch the app (Hot Reload won't work after adding the slidable package) and you should be able to swipe on items to delete them. Note that they are also magically deleted from Supabase!

Updating Data

In this section we will add the ability to update a list item. It entails:

  • A custom action to handle updating the data

  • Setting and using state fields to show/hide UI dynamically and reference the list item to edit

  • An button to edit a list item (set up similar to the Delete button in the previous section)

  • UI to enter and save the new item name (set up similar to the Create functionality we covered earlier)

  1. Create a new Custom Action called updateListItem and paste the below code:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!
import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

Future updateListItem(String name, ListsRow row) async {
  await db.execute('UPDATE lists SET name = ? WHERE id = ?', [name, row.id]);
}
  1. Hit Save and click "Yes" on the popup to set the Action Arguments for you:

  1. In the Widget Tree view, select the HomePage page and navigate to State Management.

  2. Create a new State Field called editClicked, set the type to Boolean and toggle the "Initial Field Value" toggle twice to initialize the field to false.

  3. In the Widget Tree select the ListTile.

  4. Under Slidable Properties click Add Action.

  5. Select the new SlidableActionWidget from the Widget Tree and set its properties to the following:

  6. Open the Action Flow Editor.

  7. Add an action to Update Page State.

  8. Add Field: Set the editClicked value to true.

  9. Add Field: Set the value of listItemIndex to the "Index in List" of the boundLists Item and click Close.

  1. On the Widget Palette again, add a Container child to the Column Widget.

    1. Now add a Column Widget to this Container.

    2. Add a TextField and a Button to this Column Widget.

    3. Your homepage layout should now look like this:

  2. Set the Container and TextField widgets to have a width of 100%.

  3. Change the Button text to "Save"

  4. Open the Action Flow Editor for the Save button.

    1. Add a Custom Action call to updateListItem.

    2. Set the "name" Argument to Widget State -> TextField 2.

    3. Set the "row" Argument:

      1. Select Action Outputs -> allLists.

      2. Under Available Options select "Item at Index".

      3. Under List Index Options select "Specific Index".

      4. Click Confirm

    4. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField_2 field.

    5. Chain another Action to Update Page State .

    6. Add Field: Set editClicked to false.

    7. Add Field: Set listItemIndex to Reset Value.

    8. Your Action Editor should now look like this:

  5. Close the Action Flow Editor.

Checkpoint: you should now be able hot reload your app, slide on an item to edit it. Enter the new item name into the text field that appears, and hit Save. The update should then reflect in Supabase.

Signing Out

  1. Create a new Custom Action called signOut without Arguments or Return Values and paste the below code:

// Automatic FlutterFlow imports
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/actions/index.dart'; // Imports other custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom action code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

// Set your action name, define your arguments and return parameter,
// and then add the boilerplate code using the green button on the right!
import 'package:powersync/powersync.dart' as powersync;
import '/custom_code/actions/initpowersync.dart';

Future signOut() async {
  listsSubscription?.cancel(); //close any open subscriptions from watch() queries
  await db.disconnectAndClear();
}
  1. Click Save Action.

  2. In the Widget Tree, drag a Button onto the right of your App Bar.

  3. Rename the button text to "Sign Out".

  4. Open Action Editor and click Open to launch the editor.

  5. Add a call to the signOutCustom Action.

  6. Chain another call to Auth -> Log Out:

  7. Click Close

Checkpoint: You should now be able to hot reload your app and sign out and in again.

Securing Your App

PowerSync's Sync Rules and Supabase's support for Row Level Security (RLS) can be used in conjunction. Here are some high level similarities and differences:

  • RLS should be used as the authoritative set of security rules applied to your users' CRUD operations that reach Postgres.

  • Sync Rules are only applied for data that is to be downloaded to clients — they do not apply to uploaded data.

    • Sync Rules can typically be considered to be complementary to RLS, and will generally mirror your RLS setup.

Enable RLS in Supabase

Run the below in your Supabase console to ensure that only list owners can perform actions on the lists table where owner_id matches their user id:

alter table public.lists
  enable row level security;

create policy "owned lists" on public.lists for ALL using (
  auth.uid() = owner_id
)

Update Sync Rules

Currently all lists are synced to all users, regardless of who the owner of the list is. You will now update this so that only a user's lists are synced to their device:

  1. Navigate to the PowerSync Dashboard and open your sync-rules.yaml file.

  2. Delete the existing content and paste the below contents:

bucket_definitions:
  user_lists:
    parameters: select token_parameters.user_id as user_id
    data:
      - select * from lists where owner_id = bucket.user_id
  1. Click on "Validate".

  2. Click on "Deploy sync rules".

  3. Wait for the deploy to complete.

Checkpoint: Your app should continue running seamlessly as before.

Known Issues, Limitations and Gotchas

Below is a list of known issues and limitations.

  1. It's not currently possible to use the FlutterFlow Web Editor to test your app due to limitations with FlutterFlow.

  2. When trying to compile any of the PowerSync Custom Actions, you will see errors — these can be safely ignored:

  1. Using watch() queries creates a StreamSubscription and it's important to regularly call .cancel() on these to avoid multiple subscriptions for the same query running.

Last updated