4.2 KiB
4.2 KiB
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?;
event_tags: string[]; // max 2, for bulletin board filters
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.