Moments — Domain Spec¶
A moment is an image + caption + place + lifetime. This page is the rules for ephemeral vs. journaled, expiry, visibility, and how moments attach to locations.
Mental model¶
Two shapes:
- Ephemeral by default. A moment has a
duration(in hours) that stampsexpires_atat creation. A scheduler soft-deletes once expired, then hard-deletes after a 7-day grace. - Journaled is opt-in permanence.
duration = 0→is_journaled = true, noexpires_at, no expiry path. Journaled moments live on the user's profile as a diary.
Moment lifecycle¶
| Phase | Entered when | Exit |
|---|---|---|
active |
CreateMoment (sets expires_at = now + duration unless journaled) |
expires_at < now → soft-deleted |
soft-deleted (deleted_at set) |
moment_cleanup scheduler (5-min cadence) |
7 days later → hard-deleted |
| (hard-deleted) | moment_purge scheduler (24h cadence) |
Row + media gone |
journaled |
duration = 0 at create |
Never expires; only user-driven delete or account hard-delete |
Visibility¶
| Mode | Who can see |
|---|---|
public |
Anyone (auth or public read path) |
friends_only |
Accepted friends of the poster only |
Default in the UI is friends_only. There is no private
(self-only) tier today — for self-only content, use a journaled
moment with friends_only visibility (only friends can see, but
the user retains it indefinitely).
Invariants¶
| Invariant | Why |
|---|---|
duration = 0 ⟺ is_journaled = true ⟺ expires_at IS NULL |
Three flags are equivalent; the cleanup workers filter on expires_at IS NULL |
| Two-phase deletion (soft 5 min, hard 7 d) | Recovery window for bad releases; keeps moment likes joinable for 7 d |
| Media is removed from object storage before row is deleted | On user hard-delete, media_url is listed, S3 objects removed, then CASCADE drops the rows |
Moment with location_id only shows as a badge on the location pin |
Avoid double-pinning attached moments on the map |
Moment without location_id is a standalone pin |
Map-only object at raw coordinates |
One like per (moment_id, user_id) |
Composite uniqueness in moment_likes |
| Media upload ≤ 15 MB, jpeg/png/gif/webp/heic/heif only | Object-storage budget + format support |
Lifecycle / state machine (prose)¶
Moments transition active → soft-deleted → (hard-deleted) on the
ephemeral path. The moment_cleanup scheduler ticks every five
minutes, finds rows with expires_at < now AND deleted_at IS NULL
AND is_journaled = false, and sets deleted_at = now. Seven days
later the moment_purge scheduler (24h cadence) finds rows with
deleted_at < now - 7d and hard-deletes them along with their
media. Journaled moments skip the entire path — expires_at IS NULL
filters them out of both workers.
On the like path, a moment has a denormalised likes_count
column that ToggleLike maintains. The composite uniqueness on
moment_likes(moment_id, user_id) makes the toggle idempotent.
Cross-domain interactions¶
| Other domain | Interaction |
|---|---|
| Locations | Optional location_id foreign key; on create, the LocationService resolves raw coordinates into a persisted location, then the moment is linked to it |
| Discovery | Moments serve as map markers (standalone) or badge counts (location-attached); subject to visibility |
| Friends | friends_only visibility checks accepted friendship |
| Identity | On user hard delete, userService.deleteMomentMedia removes S3 objects before FK CASCADE drops rows |
| Chat | (none today) — moments are not shared into chat as first-class objects yet |
Privacy / visibility¶
| Field on moment detail | public |
friends_only |
|---|---|---|
| Media URL, caption, location | Anyone | Friends of poster |
| Likes count | Anyone | Friends of poster |
| Liker list | Self only (today) | Self only |
| Poster identity | Anyone | Friends of poster |
See also¶
- Narrative: Bible → Moments
- Backend: Backend → Moments service
- How moments surface: Domains → Discovery
- Underlying place: Domains → Locations
- UX:
../ux/moments.md(coming soon)