Skip to content

Moments

Purpose

MomentService owns the ephemeral photo-drop format: short-lived images pinned to a location or to raw coordinates, with a configurable lifetime in hours. Moments can be journaled (is_journaled = true, no expiry, lives in the user's diary forever) or timed (default 24 hours, hard-deleted after expiry). The service also handles media upload and the like toggle.

Responsibilities

  • Persist a moment record via MomentRepository (coordinates are PostGIS, set via BeforeCreate)
  • Upload media to storage.FileStorage (max 15 MB; jpeg, png, gif, webp, heic, heif)
  • Toggle like on a moment — uses a uniqueness index on (moment_id, user_id) in moment_likes, returns new likes_count
  • CleanupExpired — soft-delete timed moments whose expires_at has passed. Called by the moment_cleanup cron every 5 minutes
  • PurgeSoftDeleted — hard-delete moments soft-deleted more than 7 days ago. Called by the moment_purge cron every 24 hours

HTTP endpoints

Method Path Auth Description
POST /api/v1/moments JWT Create moment via multipart form (media + metadata)
POST /api/v1/moments/:id/like JWT Toggle like — returns {is_liked, likes_count}

Read paths live on the DiscoveryService: GET /discovery/moment/:id for detail, GET /discovery/map for spatial listing, GET /discovery/location/:id for location-attached highlights.

Key types

type MomentService interface {
    CreateMoment(ctx context.Context, moment *models.Moment) error
    GetMomentByID(ctx context.Context, id uuid.UUID) (*models.Moment, error)
    ToggleLike(ctx context.Context, userID, momentID uuid.UUID) (liked bool, newCount int, err error)
    UploadMedia(ctx context.Context, file *multipart.FileHeader, userID uuid.UUID) (url string, err error)
    CleanupExpired(ctx context.Context) (int64, error)
    PurgeSoftDeleted(ctx context.Context, olderThanDays int) (int64, error)
}

// models.Moment fields
type Moment struct {
    ID          uuid.UUID
    UserID      uuid.UUID
    LocationID  *uuid.UUID  // nullable for standalone coordinates
    MediaURL    string
    Caption     string
    Category    string
    Visibility  string      // "public" | "friends_only"
    Duration    int         // hours; 0 == journaled
    IsJournaled bool
    ExpiresAt   *time.Time  // nil when journaled
    LikesCount  int
    Country     string
    CountryCode string
    // ... PostGIS coordinates
}

type MomentLike struct {
    MomentID uuid.UUID
    UserID   uuid.UUID
}

Data model

Table Notes
moments coordinates (PostGIS), expires_at (indexed), is_journaled, visibility, likes_count, deleted_at (soft delete), location_id (nullable). BeforeCreate stamps expires_at = now + duration unless is_journaled is true.
moment_likes Composite uniqueness on (moment_id, user_id). FK to moments with ON DELETE CASCADE.

Dependencies

  • repository.MomentRepositoryCreate, GetByID, ToggleLike, DeleteExpired, HardDeleteSoftDeleted
  • storage.FileStorage — S3-style upload
  • LocationService — invoked from MomentHandler.CreateMoment to resolve raw coords into a persisted Location (the moment is then linked via location_id)

Notable behavior

Two-phase deletion

Timed moments aren't blown away the moment they expire. They are first soft-deleted (every 5 minutes via the moment_cleanup cron → MomentRepository.DeleteExpired), then hard-deleted seven days after that (moment_purge cron daily → MomentRepository.HardDeleteSoftDeleted). This gives the cleanup workers a window to recover from bad releases and keeps moment-likes joinable for that grace period.

Journaled moments never expire

Setting duration = 0 on the upload form makes the moment journaled — BeforeCreate skips the ExpiresAt stamp, and both cleanup jobs ignore rows with expires_at IS NULL. Use friends_only visibility for diary-style content.

Standalone vs. attached moments

Moments with a location_id show up only as a count badge on the location marker in DiscoveryService.GetDiscoveryData — they are not returned as standalone pins, to avoid double-pinning. Moments without a location_id are returned as their own standalone_moment marker.

Media cleanup on user delete

When a user is hard-deleted (see Users), userService.deleteMomentMedia first lists every media_url for that user and removes the S3 objects, then lets the FK CASCADE drop the rows. The order matters — once the rows are gone, the URLs are gone, and the orphan media would never be reclaimed.

Where to look