eutopic/docs/ARCHITECTURE.md

12 KiB
Raw Permalink Blame History

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.