# DATA-MODEL.md — eutopic what gets stored and how. for interaction decisions see UIUX.md (pending). `?` = nullable. `//` = notes. `// constraint:` = multi-field constraint. --- ## user ``` id: uri; // activitypub, internal truth username: string; // unique within instance display_name: string; avatar_url: string?; bio: string?; email: string; // unique created_at: timestamp; is_admin: bool; // default false. survey creation only. federation_ready: bool; // always false until federation flag flips display_social_handle: none | mutuals | all; social_handles: [ // array, one entry per platform platform: signal | telegram | custom; handle: string; label: string?; // only for custom platform active: bool; // hides without deleting ] contact_prefs: string?; // free text e.g. "bad texter", "weekends only" ``` --- ## post ``` id: uri; author_id: user.id; body: string?; photos: string[]; // urls, max 10 created_at: timestamp; is_event: bool; // default false saveable: bool; // default false, toggled by poster at compose reaction_set: string[]?; // emoji, max 5. null = open picker only. event_date: timestamp?; // required if is_event event_ends_at: timestamp?; // required if is_event event_location: string?; poster_tag: string?; // one of: event | fundraiser | current affairs // constraint: poster_tag required if is_event = true archived_at: timestamp?; // set by background job 1 day after event_ends_at ``` --- ## story ``` id: uri; author_id: user.id; media_url: string; filter: string?; created_at: timestamp; expires_at: timestamp; // created_at + 24h ``` --- ## story_witness // private. one row per viewer per story. author sees flat witness list. no public count. ``` id: uri; story_id: story.id; viewer_id: user.id; witnessed_at: timestamp; expires_at: timestamp; // mirrors story.expires_at // constraint: unique(story_id, viewer_id) ``` --- ## reaction // no counters stored. counts derived at read time. ``` id: uri; post_id: post.id; user_id: user.id; emoji: string; created_at: timestamp; // constraint: unique(post_id, user_id) // one reaction per user per post ``` --- ## comment ``` id: uri; post_id: post.id; author_id: user.id; body: string; created_at: timestamp; deleted_at: timestamp?; // soft delete. author or post owner can delete. ``` --- ## follow // junction table: tracks directional relationships between users ``` id: uri; follower_id: user.id; // the one who initiates followee_id: user.id; // the one being followed created_at: timestamp; status: pending | accepted | blocked; // default accepted. no follow requests in v1, may change. // constraint: unique(follower_id, followee_id) // no duplicate follows // constraint: follower_id != followee_id // cannot follow yourself ``` --- ## invite ``` id: uri; code: string; // unique created_by: user.id; used_by: user.id[]; // multi-use: one link per social cluster max_uses: int?; expires_at: timestamp?; created_at: timestamp; ``` --- ## save ``` id: uri; post_id: post.id; // only valid if post.saveable = true user_id: user.id; created_at: timestamp; // constraint: unique(post_id, user_id) ``` --- ## survey // admin-created. mc only. ephemeral. dev use only — no results shown to users. ``` id: uri; created_by: user.id; // must be is_admin question: string; options: string[]; // multiple choice only expires_at: timestamp; // short-lived, exact duration tbd created_at: timestamp; ``` --- ## survey_response ``` id: uri; survey_id: survey.id; user_id: user.id; chosen_option: int; // index into survey.options created_at: timestamp; // constraint: unique(survey_id, user_id) ``` --- ## notes - all ids are uris internally for activitypub readiness. display layer strips domain until federation flips. - no counters stored anywhere. counts derived from queries. - event reactions surfaced to post author only. no public counts on bulletin board. - background job handles: story expiry, story_witness expiry, event post archiving. ```