How I Migrated 1000+ Users from Firebase to Supabase

When beginning my journey as a Flutter developer, I thought Firebase would be the best backend solution for Flutter. Given that both are made by Google, I thought this would be the backend that would be the most stable and flexible. I was wrong.

The lack of full native Dart support, slow build times, sketchy workarounds, and no desktop support led me searching for another solution. After many hours of research, I was faced with the choice between two frameworks: Appwrite and Supabase. Both were great frameworks and both fit my use case perfectly. But, I decided to move forward with Supabase because of their philosophy of “ not reinventing the wheel”.

So now I was tasked with the problem of moving 1000+ active users from one platform to another. This was no easy feat, and I wanted to share my experience in case some one else wanted to perform a similar migration.

# Migration Criteria

I wanted a seamless migration. At most, the user would only need to reauthenticate into the app and they would see everything they’d expect to see. In addition, I wanted the migration to be backwards compatible with the previous app. Users aren’t going to update every app concurrently, so it’d make sense for the supabase version to be backwards compatible with the firebase version of the app.

In order to make the migration seamless and be backwards compatible, three components needed to be migrated:

Each critera had its own different set of problems. In the following sections, I’ll be walking through the problems I faced with each criteria, and the solution I eventually settled upon.

# Stripe migration

# Backend Stripe Migration

Stripe was the most difficult and finicky aspect of the migration. Within firebase, I used the stripe firebase extension to manage all the stripe components. It took care of a lot of the implementation work so I had no idea how to approach this when I first started. I bounced between different ideas like bi-directional sync of the subscription tier within firebase, but eventually I settled upon using Stripe as my single source of truth. Although it’d be more involved, I knew it’d make fully cutting Firebase out of Fleeting Notes easier down the road. Here are the migration steps I settled upon:

  1. User signs in and behind the scenes account migration occurs
  2. After account migration, a supabase database function adds a row to the stripe table with the supabase id filled in.
  3. Once the row is added, a database webhook calls a firebase function that grabs the relevant stripe_customer_id from firebase and populates it into supabase.
  4. Once the stripe_customer_id is populated into the table, another database webhook is triggered to pull the subscrition tier from stripe.
  5. Additionally, stripe webhooks are set up to update the subscription tier of customers whenever they are updated (e.g. trialing period ends).

# Payments Page

Another component I needed to upgrade was the payments page. For the current payments page, I closely modelled it to this sample payments page. The only problem with this payments page is that supabase isn’t notified of any new customers after the migration.

Meaning, if someone were to create a new subscription, only the firebase version of the app would know that they are a paying user and the supabase version would not. Hence, I needed to connect these in realtime and the way to do this was to utilize firebase trigger functions. Here’s how I did it:

  1. A user creates a subscription in the payments page
  2. The stripe firebase extension creates a new customer in the customer collection in firebase
  3. A firebase function is triggered to update the supabase tables with the correct stripe_customer_id
  4. If the stripe_customer_id is updated, this triggers a supabase function that updates the subscription tier

With all this functionality, we now have a table that’s constantly up to date with the subscription tier of the user. This table also stores the stripe_customer_id. Which can be used to create checkout sessions or redirect to the stripe customer portal. For our particular use case, we redirect the user to a checkout session if they are a free user (after sign in), otherwise redirect them to the stripe customer portal.

# Resources that helped me

A big shout out to the supabase team, supabase happy hour series, egghead stripe course and the happy-days repo for helping with this component of the migration. I definitely would’ve spent way more time figuring out what I needed to do without these helpful courses / videos.

# Notes migration

The idea of the two-way sync came from a github comment by the co-founder of supabase. Here’s how I made it work on the supabase end:

  1. A user saves a note with the supabase version of the app
  2. A database webhook sends the updated note data to a firebase function that saves the note to firebase.

A very similar thing happens on the firebase end:

  1. A user saves a note with the firebase version of the app
  2. A firebase trigger function updates supabase with the updated note

With this the notes are bi-directionally synced, but there was an issue where these functions keep triggering each other indefinitely. I avoided this was by using the modified_at field within the note. The update would not proceed if the note in the database was modified_at later than the note in question. Now the databases won’t be calling each other back and forth indefinitely.

After I got the sync set up, I still needed to migrate all existing notes from firebase to supabase. To do this, I ran a script to transfer all notes from firebase to supabase.

# Account Migration

Account migration is done within the supabase version on sign in. Here’s how it works:

  1. User clicks “sign in” for the first time on the supabase version of the app
  2. Using the credentials entered, two concurrent sign in attempts are made to the firebase and supabase.
  3. Only if supabase fails the login and firebase succeeds, we continue to the next steps.
    1. Otherwise we proceed the login (if supabase attempt successful) or fail the login (if supabase attempt unsuccessful).
  4. Attempt to register for supabase. On registration a series of backend functions are called to migrate the user data:
    1. On user creation, database function is called to insert rows to into other tables.
    2. Then, this triggers two database webhooks that get the stripe_customer_id and encryption key from firebase.
    3. If the stripe_customer_id exists, then another database webhook is triggered to get the subscription_tier
  5. After registration is complete, another supabase login attempt is made to login the user.

A single bad, but “acceptable” scenario

# Other considerations I had

# Was it worth it?


This entire migration process probably took 3 weeks of my time to plan, execute and fix (Yes, there were some bugs upon initial release). During this time, I could’ve been marketing or developing new features. It was a lot more work than anticipated and it probably would’ve been more if I saved it for later. But with this migration, I feel more at ease about the future of Fleeting Notes and I’m excited for what to come in the following months.

# All backend functions used for migration:

# Firebase functions

# Supabase functions

# Database Functions