Legacy App Analysis
Analysis of the existing Range Day app (FlutterFlow + Supabase). Source code is in tmp/range_day/ (not committed to the repo). This document is the reference for migration planning and feature parity decisions.
Architecture
- Frontend: FlutterFlow (generates Dart/Flutter code)
- Backend: Supabase (Postgres + Auth + Realtime + Storage)
- Auth: Supabase Auth (email/password)
- Push notifications: Firebase Cloud Messaging (FCM)
- Analytics: Firebase Analytics + Mixpanel (user identification)
- API: One Supabase Edge Function (
search-users-by-name) - State management: FlutterSecureStorage for persisted state, in-memory for session state
No backend logic layer — the app talks directly to Supabase. All business logic is in Dart on the client.
Database Schema (27 tables)
Users & Auth
users
| Column | Type | Notes |
|---|---|---|
| id | String (UUID v4) | PK, from Supabase Auth |
| String | ||
| first_name, last_name, name | String | Redundant name fields |
| user_tag | String | Unique display handle |
| photo_url | String | Profile photo |
| phone_number | String | |
| is_admin | bool | |
| is_influencer | bool | Content creator flag |
| opt_in_emails | bool | |
| social_notifications | bool | |
| trex_notifications | bool | |
| new_item_notifications | bool | |
| sales_notifications | bool | |
| fcm_token | List<String> | Push notification tokens |
| favorited_drills | List<int> | FK to drills |
| favorited_assessments | List<int> | |
| favorited_quals | List<int> | |
| favorited_courses | List<int> | FK to courses |
| deleted_custom_drills | int | Counter |
| dry_fire_drill_count | int | Counter |
| created_at | DateTime |
admin_users — Simple table with just id (String). Separate from users.is_admin.
Drills
Three separate drill tables exist in the legacy app. The new schema unifies these into a single Drill entity with type/visibility fields.
drills (T.REX-curated)
| Column | Type | Notes |
|---|---|---|
| id | int | PK (integer, not UUID) |
| drill_name | String | |
| drill_description | String | |
| alpha_value, charlie_value, delta_value, mike_value | int | USPSA target zone point values |
| no_shoot_value | int | Penalty for hitting no-shoot target |
| drill_targets | List<String> | Target identifiers |
| total_rounds | int | Round count |
| drill_steps | dynamic (JSON) | Structured steps |
| possible_points | int | Max possible score |
| image_url | String | Drill diagram |
| stage_image_url | String | Stage layout image |
| hits_needed_image_url | String | Required hits diagram |
| video_url, video_url_youtube | String | Instructional video |
| category | String | Drill category |
| loadout | dynamic (JSON) | Required equipment |
| is_active | bool | Publish flag |
| created_at, last_update_time | DateTime |
custom_drills (user-created)
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| drill_name, drill_description | String | |
| target_count | int | |
| drill_steps | List<dynamic> | JSON |
| par_time | double | |
| alphaValue, charlieValue, deltaValue, missValue, noShootValue | int | Scoring values |
| distance_metric | String | |
| stage_image | String | |
| custom_stage | List<dynamic> | JSON — custom stage layout |
| is_drill | bool | Distinguishes drills from assessments |
| created_at | DateTime |
dry_fire_drills
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| drill_name, drill_description | String | |
| recommended_par_time | double | |
| video_url, image_url, video_url_youtube | String | |
| index | int | Display ordering |
| created_at, last_update_time | DateTime |
Courses
courses (from curated drills)
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| course_title | String | |
| stage_count | int | |
| user_id | String | FK to users (creator) |
| courses | List<dynamic> | JSON — ordered drill references |
| drill_ids | List<int> | FK to drills |
| created_at | DateTime |
custom_courses (from custom drills)
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| course_title | String | |
| stage_count | int | |
| user_id | String | FK to users |
| courses | List<dynamic> | JSON — ordered drill references |
| created_at | DateTime |
Completed Drills (Scores)
completed_drills
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| drill_name | String | Embedded snapshot (same as our score portability) |
| drill_description | String | Embedded snapshot |
| total_time | double | |
| hit_factor | double | Points / time |
| total_points | int | |
| notes | String | User notes |
| target_scores | List<dynamic> | JSON — per-target breakdown |
| drill_steps | List<dynamic> | JSON — step results |
| alphaValue, charlieValue, deltaValue, missValue, noShootValue | int | Scoring params snapshot |
| distance_metric | String | |
| category | String | |
| is_custom_drill | bool | Indicates source table |
| is_community_drill | bool | |
| user_uploaded_photo | String | User media |
| user_uploaded_video | String | User media |
| created_at | DateTime |
completed_multi_shooter_drills
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users (organizer) |
| drill_name, drill_description | String | |
| shooter_results | List<dynamic> | JSON — per-shooter scores |
| par_time | double | |
| shooter_count | int | |
| unique_id | String | Dedup key |
| is_custom_drill | bool | |
| alphaValue, charlieValue, deltaValue, missValue, noShootValue | int | |
| distance_metric | String | |
| created_at | DateTime |
completed_custom_courses
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| course_title | String | |
| stage_count, shooter_count | int | |
| course_results | List<dynamic> | JSON — per-drill results |
| is_msd | bool | Multi-shooter flag |
| is_community_course | bool | |
| created_at | DateTime |
Social & Friends
friendships
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| friend_id | String | FK to users |
| status | int | 0=pending, 1=accepted |
| is_notification_open | bool | |
| created_at | DateTime |
friend_requests
| Column | Type | Notes |
|---|---|---|
| friendship_id | int | FK to friendships |
| friend_id | String | FK to users (recipient) |
| requester_id | String | FK to users (sender) |
| requester_tag | String | Display name |
| requester_photo | String | |
| request_time | DateTime |
my_friends (denormalized view of accepted friendships)
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| friend_id | String | FK to users |
| user_tag | String | |
| friend_photo | String | |
| status | int | |
| frienship_start_time | DateTime | Note: typo in column name |
influencer_followers
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| influencer_id | String | FK to users |
| follower_id | String | FK to users |
| created_at | DateTime |
friends_leaderboard
| Column | Type | Notes |
|---|---|---|
| user_id | String | FK to users |
| friend_id | String | FK to users |
| friend_user_tag | String | |
| drill_name | String | |
| hit_factor | double | |
| completed_at | DateTime | |
| friend_photo_url | String |
Challenges
challenges
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| created_by | String | FK to users |
| drill_id | int | FK to drills |
| drill_name | String | |
| expires_at | DateTime | |
| participant_count | int | |
| participants | List<String> | FK to users |
| is_custom_drill | bool | |
| is_completed | bool | |
| created_at | DateTime |
user_challenges
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| challenge_id | int | FK to challenges |
| user_id | String | FK to users |
| status | String | ’pending’, ‘accepted’, ‘completed’ |
| accepted_at, completed_at | DateTime | |
| total_time | double | |
| total_points | int | |
| hit_factor | double | |
| notes | String | |
| target_scores, drill_steps | List<dynamic> | JSON |
| user_tag | String | |
| alphaValue, charlieValue, deltaValue, missValue, noShootValue | int | |
| user_uploaded_photo, user_uploaded_video | String | |
| is_notification_open | bool | |
| created_at | DateTime |
Community Content
community_drills
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| added_by | String | FK to users (who shared it) |
| drill_owned_by | String | FK to users (original creator) |
| drill_owner_name | String | |
| drill_id | int | FK to drills |
| course_id | int | FK to courses |
| qual_id | int | |
| itemName | String | |
| is_favorited | bool | |
| created_at | DateTime |
Achievements
achievements
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| title, description | String | |
| category | String | |
| photo_url | String | Badge image |
user_achievements
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| achievement_id | int | FK to achievements |
| created_at | DateTime |
Notifications & Messaging
notifications
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| sender_id, recipient_id | String | FK to users |
| title, body | String | |
| initial_page_name | String | Deep link target |
| parameter_data | String | Deep link params |
| created_at | DateTime |
in_app_messages
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| title, body | String | |
| is_challenge | bool | |
| challenge_id | int | FK to challenges |
| is_read | bool | |
| created_at | DateTime |
push_press (push notification content)
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| title, body | String | Content |
| notification_title, notification_body | String | Push display text |
| image_url, video_url | String | Rich media |
| link | String | |
| drill_id | int | FK to drills |
| dry_fire_drill_id | int | FK to dry_fire_drills |
| social, trex_messages, new_drills, sales | bool | Category flags |
| created_at | DateTime |
read_push_press — tracks which users have read which push notifications.
Performance & Analytics
hit_factors
| Column | Type | Notes |
|---|---|---|
| id | int | PK |
| user_id | String | FK to users |
| name | String | Drill name |
| time | double | |
| hit_factor | double | Performance metric |
| alpha, charlie, delta, miss, no_shoot | int | Shot breakdown |
| created_at | DateTime |
global_hitfactor_averages — aggregate view: drill_name, average_hit_factor, total_completions.
Analytics views (admin, read-only):
new_users_per_day/new_users_per_weeknew_completed_drills_per_day/new_completed_drills_per_weeknew_custom_drills_per_day/new_custom_drills_per_week
Features (40+ screens)
Drill Execution
- Preconfigured drills — T.REX-curated drills with descriptions, videos, diagrams
- Custom drills — user-created drills with custom scoring, targets, steps
- Dry fire drills — timer-based practice drills, no live fire
- Designated target drills — specific target engagement patterns (Bill Drill, etc.)
- Multi-shooter drills — multiple shooters scored simultaneously on one drill
Courses
- Custom courses — bundle multiple drills into a sequence
- Multi-shooter courses — courses with multiple shooters
- Course drill selection — pick which drills to include
Scoring
- USPSA-style scoring — alpha (A-zone), charlie (C-zone), delta (D-zone), miss, no-shoot
- Hit factor — total points divided by time (key performance metric)
- Per-target breakdown — individual target scores within a drill
- Step-based scoring — drills broken into timed steps
- User media — photos and videos uploaded per completed drill
Past Results
- Drill history — all completed drills with full score details
- Multi-shooter history — per-shooter breakdowns
- Course history — per-drill breakdowns within courses
- Designated target results — specialized result view
Social
- User search — find users by name (Supabase Edge Function)
- Friend requests — send, accept, decline
- Friends list — view accepted friends
- Friends leaderboard — compare hit factors on drills among friends
- Influencer following — follow content creators
Challenges
- Create challenge — pick a drill, invite friends, set expiration
- Accept/decline — respond to incoming challenges
- Complete challenge — run the drill, submit score
- Challenge results — compare scores across participants
Community
- Community drills — browse drills shared by other users
- Favorites — bookmark drills, assessments, quals, courses
Other
- Range bag — track equipment/gear, suggested items per drill
- Achievements — earn badges for milestones
- Push notifications — categorized (social, T.REX, new drills, sales)
- Notification preferences — per-category opt-in/out
Scoring Model: USPSA-Style
The legacy app uses USPSA (United States Practical Shooting Association) scoring terminology throughout. This is important for the migration and for the new scoring_params field:
| Zone | Meaning | Typical Points |
|---|---|---|
| Alpha (A) | Center of target | 5 points |
| Charlie (C) | Mid ring | 3 points |
| Delta (D) | Outer ring | 1 point |
| Miss (M) | Missed target entirely | 0 points + penalty |
| No-Shoot (NS) | Hit a non-target | Penalty (configurable) |
Hit Factor = Total Points / Total Time. This is the primary performance metric tracked across the app — used in leaderboards, personal history, and global averages.
Each drill defines its own point values per zone (alpha_value, charlie_value, etc.), allowing non-standard scoring for custom drills.
In the new system, USPSA scoring maps to a built-in ScoringProfile with calculation method hit_factor. The per-drill point value overrides in the legacy app map to the zone_overrides field on the new Drill entity. See ScoringProfile.
Data Model Observations
Patterns Worth Noting
- Score portability already exists —
completed_drillsembeds drill name, description, and scoring params. The legacy app independently arrived at the same design we documented in Score Portability. - JSON columns everywhere — drill_steps, target_scores, course_results, shooter_results are all unstructured JSON. Works but prevents database-level queries and validation.
- Denormalized tables —
my_friendsduplicatesfriendships,friends_leaderboardduplicatescompleted_drills+friendships. Performance optimization for Supabase’s direct-query model. - Test tables —
drills_test_tableanddry_fire_drills_test_tableare copies of production tables used for testing. Not migrated. - No events concept — the legacy app has no formal “event” entity. Multi-shooter drills are ad-hoc, not organized under an event. This is the core capability Range Day Pro adds.
Migration Complexity by Table
| Complexity | Tables |
|---|---|
| Straightforward | users, drills, custom_drills, dry_fire_drills, courses, custom_courses |
| Moderate (JSON normalization) | completed_drills, completed_multi_shooter_drills, completed_custom_courses |
| Decision needed | challenges, user_challenges, friendships, friend_requests, achievements, user_achievements, community_drills |
| Skip | test tables, analytics views (regenerated from new data), denormalized views (my_friends, friends_leaderboard) |
Features Requiring Decision
These features exist in the legacy app but are not currently in the Range Day Pro roadmap. Each needs a keep/defer/drop decision before migration planning is complete:
| Feature | Legacy Implementation | Recommendation | Notes |
|---|---|---|---|
| Challenges | Full system — create, invite, accept, complete, results | Discuss | Popular feature (10+ screens). Could map to lightweight events. |
| Friends / social graph | Friend requests, friends list, leaderboard | Discuss | Org membership may replace this. Or friends could exist alongside orgs. |
| Leaderboards | Per-drill hit factor rankings among friends | Discuss | Could be per-org or per-event in the new model. |
| Dry fire drills | Separate table and category | Fold into Drill type | No structural reason for a separate table. Add a type enum value. |
| Range bag | Equipment tracking, suggested items per drill | Defer | Nice-to-have, not core to MVP. |
| Achievements | Badges earned for milestones | Defer | Gamification, not core to MVP. |
| Favorites | Bookmarked drills, courses | Keep (MVP) | Simple feature, high usability value. |
| User media | Photos/videos per completed drill | Discuss | Requires file storage (S3/DO Spaces). Changes data model and infra. |
| Push notifications | FCM, categorized | Keep (MVP) | Needed for event reminders, score updates. Missing from current docs. |
| Hit factor | USPSA performance metric | Keep (MVP) | Core scoring concept. Fold into scoring_params. |
| Influencer system | Follow content creators | Defer/Drop | Not relevant for MVP customer segment (church security teams). |
| Community drill sharing | Browse/copy shared drills | Keep | Already covered by drill visibility (public/published) in the new model. |
| Admin analytics views | User/drill growth dashboards | Defer | Observability stack covers operational metrics. Admin reporting is post-MVP. |
External Services
| Service | Usage | Migration Impact |
|---|---|---|
| Supabase Auth | Email/password authentication | Users migrated with null password_hash. See Auth Architecture. |
| Supabase Postgres | All application data | ETL migration to new Postgres. |
| Supabase Storage | User-uploaded photos/videos | If user media is kept, files must be migrated to new storage (DO Spaces or similar). |
| Firebase Cloud Messaging | Push notifications | Replace with new push notification system if push is kept for MVP. |
| Firebase Analytics | Event tracking | Replace with observability stack. See Observability. |
| Mixpanel | User identification | Drop — not needed with new observability. |