eutopic/docs/ARCHITECTURE.md

158 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-05-31 13:12:30 +00:00
# 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`.