eutopic/docs/ARCHITECTURE.md

157 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <App /> 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. 110 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`.