Migration Strategy

Context

The existing free Range Day app has ~60k users on a FlutterFlow + Supabase stack. Range Day Pro replaces this with a completely new stack (Rust/Axum server, Dioxus web, Swift/Kotlin native apps). All existing users need an upgrade path to the new platform with their data intact.

Legacy App Analysis

A full analysis of the existing FlutterFlow app’s data model, features, and architecture is documented in Legacy App Analysis. That document should be read before working on migration tasks.

Approach: Phased Migration

Phase 1: One-Time Data Migration (ETL)

A migration job reads all data from Supabase and writes it into the new Postgres database, transformed to fit the new schema:

Supabase (old)New Database
Users (Supabase Auth)User (status: active) + personal Org
DrillsDrill (visibility: private, owned by user’s personal org)
ScoresScore (with embedded drill metadata snapshots)
Groups / sharingOrg + OrgMembership
CoursesCourse + CourseDrill

After this runs, the new database has everything. Both databases are in sync at this point in time.

Phase 2: Deploy New Apps + One-Way Sync

  1. Publish the new native apps as an update to the existing app store listings (same iOS Bundle ID, same Android package name + signing key). From the app store’s perspective, it’s just a new version — the tech stack change is invisible.

  2. Users receive the update via auto-update or the standard app store “update available” prompt. T.REX ARMS announces the update across social media channels to reach the existing user base.

  3. New apps read/write to the new database only. Users who have updated are fully on the new platform.

  4. Old app instances (users who haven’t updated yet) continue to read/write to Supabase.

  5. One-way sync: Supabase → new DB. A listener on Supabase (database trigger or realtime subscription) transforms and forwards any old-app writes into the new database. This ensures data from users still on the old app flows into the new system.

What this means during coexistence:

  • New app users see everything — their data + old app writes flowing in via the sync
  • Old app users see a stale view — they miss writes from new app users. This is acceptable because they are being prompted to update, and it’s temporary.
  • Only one sync direction to maintain (old → new), not bidirectional

Phase 3: Cut Over

  1. Monitor old app activity — track Supabase read/write volume
  2. Once old app traffic drops to near zero, shut down the one-way sync
  3. Decommission Supabase — the new database is the sole source of truth
  4. Done

Why One-Way Sync (Not Bidirectional)

Bidirectional sync would require reverse-transforming the new data model (multi-tenant, visibility, append-only replacement chains) back into the old flat schema. Some concepts don’t map backward cleanly — e.g., a voided-and-replaced score chain has no representation in the old schema.

One-way (old → new) is a clean transform because the old schema is simpler. The new schema is a superset of the old one’s capabilities.

The tradeoff — old app users temporarily miss new app data — is acceptable because:

  • Auto-update handles most users automatically
  • Social media announcements reach the engaged user base
  • The coexistence period is weeks, not months
  • Old app users’ own writes are preserved (flowing into the new DB)
  • No data is lost — even if a user never updates, their data is already in the new database from the Phase 1 ETL

No Changes to the Old App

The existing FlutterFlow app is not modified. No “please update” banner, no version check, no final Flutter update. The app store’s native update mechanism and social media communication are sufficient.

App Store Continuity

The new native apps (Swift + SwiftUI for iOS, Kotlin + Jetpack Compose for Android) are published under the same app store listings:

  • iOS: same Bundle ID, same Apple Developer account
  • Android: same package name, same signing key (or Play App Signing)

The app store treats it as a version update. Users see “Range Day” (or “Range Day Pro” if the listing is renamed) in their updates. The fact that the underlying technology changed from Flutter to native is invisible to the user.

Data Mapping Notes

The ETL job will need to handle:

  • User identity — Supabase Auth users mapped to new User records. Auth credentials are re-created in the new auth module (users will need to reset passwords on first login to the new app — see Migrated Users).
  • ID conversion — Legacy uses integer IDs for drills/courses and UUID v4 strings for users. All must be mapped to UUID v7 in the new schema. The ETL maintains a mapping table for FK resolution.
  • Personal orgs — every migrated user gets a personal Org (is_personal: true)
  • Drill unification — Legacy has separate tables for curated drills (drills), custom drills (custom_drills), and dry fire drills (dry_fire_drills). All map to a single Drill table in the new schema with appropriate type/visibility.
  • Scoring model — Legacy uses USPSA-style scoring (alpha/charlie/delta/miss/no-shoot values). These map to the new scoring_params structured field.
  • Score snapshots — drill name and type embedded into each migrated score at migration time
  • JSON denormalization — Legacy stores drill steps, target scores, and course results as unstructured JSON columns. The ETL normalizes these into the new schema’s structured fields.
  • Deduplication — ensure no duplicate records if the ETL is run multiple times (idempotent)
  • Timestamp preservation — original created_at timestamps from Supabase are preserved, not overwritten with migration timestamps
  • Features not migrated — See Features Requiring Decision for features that exist in the legacy app but may not carry forward (challenges, friendships, achievements, etc.)

Timeline Estimate

PhaseDuration
Phase 1 (ETL development + testing)Before launch
Phase 2 (coexistence)~30-60 days after new app launch
Phase 3 (cut over)One-time, after old app traffic flatlines