Skip to content

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:

  1. Ephemeral by default. A moment has a duration (in hours) that stamps expires_at at creation. A scheduler soft-deletes once expired, then hard-deletes after a 7-day grace.
  2. Journaled is opt-in permanence. duration = 0is_journaled = true, no expires_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 = 0is_journaled = trueexpires_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