# ARCHITECTURE.md — eutopic *to be fleshed out before v0 alpha.* covers: backend structure, API shape, federation mechanics, background job design, endpoint inventory. --- ## frontend — flutter folder structure ``` lib/ ├── main.dart // entry point. calls runApp(). almost nothing lives here — just bootstraps app.dart. ├── app.dart // root widget. sets up theme, router, and any top-level providers. think of it as the in react. │ ├── core/ // framework-level setup — not domain-specific. no business logic here. │ ├── theme/ │ │ ├── app_theme.dart // materialapp theme config — fonts, shape, component defaults │ │ └── app_colors.dart // all color constants in one place. nothing hardcodes hex values elsewhere. │ ├── router/ │ │ └── app_router.dart // go_router config. every route in the app is registered here. │ │ // also handles deep-link invite codes (hard section — fully written). │ ├── widgets/ │ │ └── bottom_nav_bar.dart // the five-tab nav bar. lives in core/ because it is framework plumbing — │ │ // it knows about routes, not domain data. every screen uses it. │ └── constants/ │ └── app_constants.dart // magic values that would otherwise be scattered: api base url, story duration, max photos, etc. │ ├── shared/ // domain code used by more than one feature. if only one feature uses it, it lives in that feature folder. │ ├── models/ │ │ ├── user.dart // the User type. used everywhere — feed, profile, follow suggestions, etc. │ │ ├── post.dart // a feed post. 1–10 photos, optional caption, reaction set, author. │ │ ├── poster_post.dart // a posters post. same base as post but adds tag + event fields. │ │ ├── story.dart // a story. 24h expiry, witness list, no replies. │ │ ├── reaction.dart // a single reaction: user + post + emoji + timestamp. │ │ ├── comment.dart // a comment on a post. flat list, chronological, no threading. │ │ ├── survey.dart // an active survey. multiple choice, 36h expiry. │ │ ├── invite_code.dart // an invite. carries the inviter's id for auto-follow suggestions. │ │ └── social_handle.dart // signal/telegram/custom handle + visibility setting (none/mutuals/all). │ ├── widgets/ │ │ ├── photo_viewer.dart // full-screen photo viewer used by feed, posters, and profile grid. │ │ ├── emoji_picker_sheet.dart // bottom sheet (slides up from below) for picking an emoji reaction. │ │ ├── loading_indicator.dart // shared spinner/skeleton — keeps loading states visually consistent. │ │ └── error_view.dart // shared error state widget for failed api calls. │ ├── services/ │ │ ├── api_client.dart // http client wrapper. all api calls go through here — one place to set auth headers, base url, error handling. │ │ ├── auth_service.dart // magic-link auth flow. stores session token, exposes current user. │ │ └── upload_service.dart // tus resumable upload. runs in a background isolate. hard section — fully written. │ └── utils/ │ ├── date_formatter.dart // relative time formatting ("3h ago", "yesterday") used across feed, posters, stories. │ └── validators.dart // input validation for username, email, invite code fields. │ ├── features/ // one folder per screen group. each folder is self-contained: screen + widgets + state. │ │ // rule: a widget in features/feed/ should never be imported by features/profile/. │ │ // if two features need the same widget, it moves to shared/widgets/. │ │ │ ├── onboarding/ │ │ ├── onboarding_screen.dart // shell screen that hosts the multi-step onboarding flow. │ │ ├── widgets/ │ │ │ ├── invite_gate.dart // step 1: invite code entry. prefilled from deep link if available. │ │ │ ├── email_username_form.dart // step 2: email + username. │ │ │ ├── magic_link_confirm.dart // step 3: "check your email" holding screen. │ │ │ ├── social_handle_opt_in.dart // step 4: signal/telegram/custom. skippable. │ │ │ └── follow_suggestions.dart // step 5: friends-of-inviter. skippable. │ │ └── providers/ │ │ └── onboarding_provider.dart // holds onboarding state across steps: invite code, entered email, etc. │ │ │ ├── feed/ │ │ ├── feed_screen.dart // home tab. fetches post list, renders it, handles pull-to-refresh. │ │ ├── widgets/ │ │ │ ├── post_card.dart // a single post. renders author, photos, reactions. does not fetch — receives a Post object. │ │ │ ├── post_photo_swipe.dart // swipeable photo carousel within a post card. up to 10 photos. │ │ │ ├── reaction_bar.dart // emoji reactions row below a post. shows the post's reaction set. │ │ │ ├── comment_sheet.dart // bottom sheet (slides up) showing flat comment thread for a post. │ │ │ └── survey_card.dart // injected at slot 1 when an active survey exists. not a real post. │ │ └── providers/ │ │ └── feed_provider.dart // manages post list state: loading, pagination, optimistic reaction updates. │ │ │ ├── posters/ │ │ ├── posters_screen.dart // posters tab. 2-column grid, paginated, filterable by tag. │ │ ├── widgets/ │ │ │ ├── poster_grid.dart // the 2-column grid layout. handles infinite scroll pagination. │ │ │ ├── poster_card.dart // a single poster card. shows tag, title, event date if applicable. │ │ │ ├── poster_filter_bar.dart // tab strip or dropdown for filtering by event/fundraiser/current affairs. TBD. │ │ │ └── rsvp_button.dart // placeholder — rsvp interaction model not yet designed. see OPEN-QUESTIONS.md. │ │ └── providers/ │ │ └── posters_provider.dart │ │ │ ├── compose/ │ │ ├── compose_screen.dart // center nav tab. opens as modal or full screen (TBD). orchestrates the compose flow. │ │ ├── widgets/ │ │ │ ├── photo_picker_tile.dart // photo selection grid. up to 10 photos. │ │ │ ├── caption_field.dart // optional caption text input. │ │ │ ├── destination_toggle.dart // feed vs posters toggle. │ │ │ ├── tag_picker.dart // event / fundraiser / current affairs. required if posting to posters. │ │ │ ├── event_fields.dart // date/time fields shown only when tag = event. │ │ │ ├── reaction_set_picker.dart // choose up to 5 emoji for this post's reaction set. │ │ │ ├── savable_toggle.dart // saveable on/off toggle. default off. │ │ │ └── upload_progress_animation.dart // hand-drawn lottie animation shown while tus upload runs in background. │ │ └── providers/ │ │ └── compose_provider.dart // compose form state + triggers upload_service on confirm. │ │ │ ├── stories/ │ │ ├── stories_screen.dart // stories tab. shows list of active stories as thumbnails. │ │ ├── story_viewer_screen.dart // full-screen story playback. hard section — timer, gestures, progress bar fully written. │ │ ├── story_create_screen.dart // camera or photo picker → post. no drafts. │ │ ├── widgets/ │ │ │ ├── story_thumbnail.dart // thumbnail shown in the stories tab list. │ │ │ ├── story_progress_bar.dart // segmented progress bar at top of viewer. one segment per story. │ │ │ ├── story_filter_overlay.dart // swipe-gesture filter layer. applied at view time (TBD). │ │ │ └── witness_list_sheet.dart // bottom sheet showing who has seen your story. visible to author only. │ │ └── providers/ │ │ └── stories_provider.dart │ │ │ ├── profile/ │ │ ├── profile_screen.dart // own profile and others' profiles share this screen. own = shows settings. other = shows follow button. │ │ ├── widgets/ │ │ │ ├── profile_header.dart // avatar, display name, username, bio. │ │ │ ├── social_handle_display.dart // signal/telegram/custom handle, shown per display_social_handle setting. │ │ │ ├── contact_prefs_display.dart // free text contact prefs ("bad texter", "weekends only"). │ │ │ ├── profile_post_grid.dart // grid of user's own posts. │ │ │ ├── follow_button.dart // follow/unfollow. shown on others' profiles only. │ │ │ └── settings_button.dart // link to settings. shown on own profile only. │ │ └── providers/ │ │ └── profile_provider.dart │ │ │ ├── settings/ │ │ ├── settings_screen.dart │ │ ├── widgets/ │ │ │ ├── social_handle_settings.dart // edit signal/telegram/custom handles. │ │ │ └── display_preferences.dart // display_social_handle: none / mutuals / all. │ │ └── providers/ │ │ └── settings_provider.dart │ │ │ └── feedback/ │ ├── feedback_screen.dart // always accessible. active survey at top if one exists, free text below always. │ ├── widgets/ │ │ ├── active_survey_card.dart // renders the current multiple-choice survey if one is active. │ │ ├── survey_question_tile.dart // a single survey question with answer options. │ │ └── free_text_field.dart // permanent free-text input. always present, survey or not. │ └── providers/ │ └── feedback_provider.dart ``` ### structural decisions **`core/` vs `shared/`** — `core/` is framework plumbing (theme, router, constants). `shared/` is domain code (models, services, reusable widgets). the distinction matters when onboarding a new contributor: core is "don't touch unless you know what you're doing," shared is "add things here when two features need the same thing." **feature-first, strict imports** — nothing in `features/feed/` imports from `features/profile/`. cross-feature dependencies always flow through `shared/`. this keeps features independently readable and prevents tangled refactors later. **one `*_screen.dart` per feature, more where flows are genuinely distinct** — screens own data fetching and pass data down to widgets. widgets are display-only and receive everything they need as constructor parameters. this is the flutter equivalent of container/presentational component separation in react. stories is the exception: it has three screen files (`stories_screen`, `story_viewer_screen`, `story_create_screen`) because the viewer (timer + gesture layer) and creator (camera + picker) are full-screen flows with complex independent lifecycles, not components. the rule bends for stories; it does not apply everywhere. **`providers/` per feature** — state management sits next to the screen it serves, not in a global `state/` folder. a provider that only `feed_screen.dart` uses has no reason to be visible from `profile/`. **no notification files anywhere** — hard constraint. no FCM, no APNs, no device tokens, no unread badge logic. if a future contributor adds one, it's a deliberate reversal of a design decision, not an accident. **`comment_sheet` and `witness_list_sheet` are sheets, not screens** — they slide up over the current screen without replacing it. naming them `*_sheet.dart` signals this — they are never registered as routes in `app_router.dart`.