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.
Configure Supabase and PowerSync prerequisites
Initialize your FlutterFlow project
Build a sign-in screen
Initialize PowerSync
Reading data
Creating data
Deleting data
Signing out
Securing your app
Enable RLS in Supabase
Update Sync Rules in PowerSync
Configure Supabase
Create a new project in Supabase.
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 a Postgres publication using the SQL Editor. This will enable data to be replicated from Supabase so that your FlutterFlow app can download it.
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
In the PowerSync dashboard Project tree, click on "Create new instance":
Give your instance a name, such as "Supabase Testing".
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).
Under the "DB Connections" tab, click on the + icon.
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.
Click "Test Connection" and fix any errors.
Under the "Client Auth" tab, enable "Use Supabase Auth".
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.
Replace the
sync-rules.yaml
file's contents with the below:
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
Create a new Blank app, give it a name, and disable Firebase.
Under "App Settings" -> "Integrations", enable Supabase. Enter your "API URL" and "Anon Key" and click "Get Schema".
Under "App Values" -> "Constants", click "Add App Constant".
For Constant Name, enter
PowerSyncUrl
.For Constant Value, copy and paste your instance URL from the PowerSync Dashboard:
You should now see this under App Constants:
Build A Sign-In Screen
Under Pages, click "Add Page, Component or Flow".
Select the Auth1 template and name the page "Login".
Under "App Settings" -> "App Settings" -> "Authentication":
Enable Authentication.
Set Supabase as the Authentication Type.
Set the Login page you just created as the Entry Page.
Set HomePage as the Logged In Page:
In your Supabase Dashboard, under "Authentication", click on "Add User" -> "Create new user" and create a user for yourself to test with:
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
Click on "Custom Code" -> "Add" -> "Action".
Name the Custom Action
initpowersync
.NOTE: use all lowercase for this Custom Action is important due to naming conversion that FF performs behind the scenes.
Copy and paste the custom action code from here: https://github.com/powersync-ja/powersync-flutterflow-template/blob/flutterflow/lib/custom_code/actions/initpowersync.dart
Import your schema:
Paste this into your Custom Action code on line 27 after the equals sign.
Due to a limitation in FF, you now need to prefix each instance of
Schema
,Column
andTable
withpowersync
.Your custom action schema definition should now look like this:
Under "Action Settings" on the right, add this dependency into "Pubspec Dependencies":
powersync: ^1.6.3
FlutterFlow imports and old version of sqflite by default and it's not possible to remove it, so you also need to add this dependency:
sqflite: ^2.3.3
Your dependencies should now look as follows:
Save your new custom action
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:
Stop any running simulator app sessions
Restart the app by clicking "Test", and sign in
Click on "Open Device Logs"
You should see this kind of log message:
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.
Create a new Custom Action and call it
watchLists
and paste the below code:
Your Action Arguments should now look as follows:
Create the second Custom Action called
getLists
and paste the following code into it:
Your Action Arguments should now look as follows:
On the HomePage page, you will create a placeholder Page State variable required for the next step.
Click on State Management.
Still on the HomePage page, select Actions and open "Action Flow Editor".
Add the
watchLists
Custom Action.Click "Open" to edit the
callback
Argument forwatchLists
.Add the
getLists
Custom Action and set theresults
Action Argument toresult
and click "Confirm":Set the "Action Output Variable Name" to
allLists
and you should now see this: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:
Click "Close" to exit the Action Flow Editor.
In the UI Builder on the HomePage page, add a ListView component and add a ListTile inside the ListView.
On the ListView component, click "Generate Dynamic Children". Enter a variable name of
boundLists
and set its value toallLists
(no further changes). Click Save.On the ListTile component, set the Title field to "Set from Variable" and then get the
name
field from theboundLists
variable:
Do the same for the Subtitle field of the ListTile component, and set it to
created_at
.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.Leave
id
andcreated_at
blank.Enter a name such as "Test from Supabase".
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.
Create a new Custom Action called
createListItem
and paste the following code:
There should now be one argument for the Custom Action called
name
of type String and not nullable.In the Widget Tree view, select the HomePage page and navigate to State Management.
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.In the Widget Tree view, drop a Floating Action Button (FAB) onto the page.
Click on the FAB and Open the Action Flow Editor.
Add an action to Update Page State.
On the Widget Palette again, add a Container child to the Column Widget.
Now add a Column Widget to this Container.
Add a TextField and a Button to this Column Widget.
Set the Container and TextField widgets to have a width of 100%.
Change the Button text to "Add".
Open the Action Flow Editor for the Add button:
Add a Custom Action call to
createListItem
.Set the "name" Argument to Widget State -> TextField 1.
Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField_1 field.
Chain another Action to Update Page State and set
fabClicked
to false.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.
Create a new Custom Action called
deleteListItem
and paste the below code:
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
A 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)
Create a new Custom Action called
updateListItem
and paste the below code:
Hit Save and click "Yes" on the popup to set the Action Arguments for you:
The Custom Action Arguments should now look as follows:
In the Widget Tree view, select the HomePage page and navigate to State Management.
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.In the Widget Tree select the ListTile.
Under Slidable Properties click Add Action.
Select the new SlidableActionWidget from the Widget Tree and set its properties to the following:
Open the Action Flow Editor.
Add an action to Update Page State.
Add Field: Set the
editClicked
value totrue
.Add Field: Set the value of
listItemIndex
to the "Index in List" of theboundLists
Item and click Close.
Chain another Action to "Set Form Field" -> TextField_2. This will initialize the text field to the current list item's name.
Set the variable to the boundLists Item
Under Available Options, select "Get Row Field"
Under Supabase Row Field, select "name"
Your action should look like this:
On the Widget Palette again, add a Container child to the Column Widget.
Now add a Column Widget to this Container.
Add a TextField and a Button to this Column Widget.
Your homepage layout should now look like this:
Set the Container and TextField widgets to have a width of 100%.
Change the Button text to "Save"
Open the Action Flow Editor for the Save button.
Add a Custom Action call to
updateListItem
.Set the "name" Argument to Widget State -> TextField 2.
Set the "row" Argument:
Select Action Outputs -> allLists.
Under Available Options select "Item at Index".
Under List Index Options select "Specific Index".
Click Confirm
Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField_2 field.
Chain another Action to "Update Page State".
Add Field: Set
editClicked
to false.Add Field: Set
listItemIndex
to Reset Value.Your Action Editor should now look like this:
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
Create a new Custom Action called
signOut
without Arguments or Return Values and paste the below code:
Click Save Action.
In the Widget Tree, drag a Button onto the right of your App Bar.
Rename the button text to "Sign Out".
Open Action Editor and click Open to launch the editor.
Add a call to the
signOut
Custom Action.Chain another call to Auth -> Log Out:
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:
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:
Navigate to the PowerSync Dashboard and open your
sync-rules.yaml
file.Delete the existing content and paste the below contents:
Click on "Validate".
Click on "Deploy sync rules".
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.
It's not currently possible to use the FlutterFlow Web Editor to test your app due to limitations with FlutterFlow.
When trying to compile any of the PowerSync Custom Actions, you will see errors — these can be safely ignored:
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.Deploying to the Apple App Store currently requires some workarounds due to limitations in FlutterFlow:
Download the code from FlutterFlow
Open the
Podfile
located in theios/
directoryThe following option in the
Podfile
needs to be updated fromuse_frameworks! :linkage => :static
touse_frameworks!
(remove everything after the exclamation sign)After removing that option, clean the build folder and build the project again.
You should now be able to submit to the App Store
Exporting the code from FlutterFlow using the "Download Code" action in FlutterFlow requires the same workaround listed in 4. above.
Other common issues and troubleshooting techniques are documented here: Troubleshooting
Last updated