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 viaBeforeCreate) - 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)inmoment_likes, returns newlikes_count CleanupExpired— soft-delete timed moments whoseexpires_athas passed. Called by themoment_cleanupcron every 5 minutesPurgeSoftDeleted— hard-delete moments soft-deleted more than 7 days ago. Called by themoment_purgecron 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.MomentRepository—Create,GetByID,ToggleLike,DeleteExpired,HardDeleteSoftDeletedstorage.FileStorage— S3-style uploadLocationService— invoked fromMomentHandler.CreateMomentto resolve raw coords into a persistedLocation(the moment is then linked vialocation_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¶
backend/internal/services/moment_service.gobackend/internal/handlers/moment_handler.gobackend/internal/repository/moment_repository.gobackend/internal/models/moment.gobackend/internal/scheduler/manager.go—moment_cleanup(5m),moment_purge(24h) cron registrationbackend/internal/scheduler/handlers.go— task handlersHandleMomentCleanup,HandleMomentPurge