Sansoif, the SXN Labs app for tracking your drinking with friends, is leaving Supabase for a Rails application. The server changes in one deploy, but the apps on people’s phones don’t follow: every published version keeps running as long as someone uses it, and all of them have to be served.

The iOS, Android and Apple Watch apps called 110 RPC functions and ten Edge Functions. Those RPC functions are nasty SQL, hard to read and hard to maintain. Frankly, they’re a pain, even though an agent, not me, is the one reading and maintaining them. Two of the Edge Functions had never been deployed to production, and the watch went two months without working standalone, with no error ever surfacing. I wanted a stack I understand. The business logic now lives in Rails, behind a REST API described by an OpenAPI file, and the apps display what the server computes.

Old builds stay on Supabase

The builds released before the migration talk to Supabase directly. People update whenever they like, and Apple and Google review every version before it ships. These builds can’t receive the HTTP 426 that tells outdated versions to update either, since they don’t talk to Rails. Shutting Supabase down would have stopped them with no explanation, so Supabase stays up and its data is copied to Rails.

A one-way sync

A Rails job copies Supabase to Rails every fifteen minutes, Apple and Google identities included, so a person finds their profile when they sign in to the new app. On their first session, Rails records the time in people.rails_signed_in_at and becomes the source of truth for them. Their data is no longer copied from Supabase, and neither are the friendships and pokes that involve them or the groups they own.

On the left, old builds write to Supabase. On the right, new builds write to Rails. A one-way arrow goes from Supabase to Rails every fifteen minutes. Under Rails, a person's first session sets rails_signed_in_at, and the sync no longer touches their personal data. At the bottom, the shutdown rule: seven days without a single Supabase write.
Both backends run side by side until shutdown, and each person switches at their first sign-in on the new app.

I ruled out a two-way sync, which would have taken several days and a conflict rule for every table. People still on an old build pay for it. They no longer see activity from friends who moved to Rails, and if a group’s owner has already switched, leaving the group from the old app doesn’t show up on the Rails side. An entry made on another device after the last copy can also go missing, even though the iOS client re-sends the journal kept on the phone.

The first run took 36 seconds for 48 people, 1,620 days and 8 avatars, with no errors. It also copied a bug from the old builds: 55,000 invites for 5 groups, because an old function created one on every call and those builds call it in a loop. I copy them as they are rather than fix the source mid-transition.

Measure before shutting down

A dashboard card shows how many people are still on Supabase and when the last write happened. I’ll shut Supabase down after seven days without a single write, and no later than thirty days after the Rails apps ship. Both the iOS and Android versions are out. Anyone who updates after the shutdown will find in Rails the data the sync carried over.

Rails builds will age too

The Rails apps shipping today will be tomorrow’s old builds, and the API will keep changing while they stay installed. The iOS client generated from the OpenAPI contract rejected an entire response as soon as an enum held an unknown value. Adding a plan or a badge would have broken decoding, in every version already released, for any response carrying the new value.

Response enums are now read as strings, with a fallback chosen screen by screen. An unknown plan shows up as a paid plan, with the limits the server returns and enforces, and an unknown badge is simply left out without stopping the rest of the board from rendering. For Rails versions that get too old, the 426 stays in place: unlike the Supabase builds, they talk to a server that can ask them to update.