Skip to content

Events

Purpose

EventService owns the event resource — creation, updates, participants, host transfer, and the embedded chat room that every event spawns. EventLifecycleService is its companion: it owns the time-driven state machine (upcoming → ongoing → completed → archived), orphan cleanup, and message retention.

Together they keep the events table consistent with both the wall clock and the social graph: an event with no remaining host or participants is removed; an event past its retention window has its messages purged.

Responsibilities

EventService

  • Create event + provision a paired ChatRoom (type group) and add the host as admin
  • Index event coordinates into Redis (AddEventLocation) for fast geospatial lookups
  • Join flow: access-code check, capacity / waitlist enforcement, attestation requirement, visibility (public / friends / invite_only), auto-accept toggle
  • Approve / remove participants (host-only)
  • TransferOwnership — swap host, demote old host to participant
  • Send + list messages on the event's chat room (the modern path lives on ChatService; this is kept for legacy POST /events/:id/messages)
  • Log every event-related action through ActivityLogService

EventLifecycleService

  • UpdateEventStatuses — walk every event and call determineStatus(now) to roll the state machine forward
  • ArchiveCompletedEvents — flip completed to archived once end_time + retention_days is past
  • PurgeArchivedEvents(olderThanDays) — hard-delete archived events past olderThanDays
  • CleanupOrphanedEvents — find events whose host no longer exists and which have no active participants, then drop them (purges messages + Redis + DB)
  • CheckAndCleanupEvent — same logic for a single event ID; called from LeaveEvent
  • CleanupUserEvents — fan-out cleanup when a user is hard-deleted

State machine

stateDiagram-v2
    [*] --> upcoming: CreateEvent
    upcoming --> ongoing: now > start_time
    ongoing --> completed: now > end_time
    completed --> archived: now > end_time + retention_days
    upcoming --> cancelled: CancelEvent
    ongoing --> cancelled: CancelEvent
    archived --> [*]: PurgeArchivedEvents

State transitions are evaluated by the status_update cron (every 5 minutes, see internal/scheduler/manager.go). The cancelled status is set explicitly via CancelEvent and also removes the event from the Redis geo index.

HTTP endpoints

Method Path Auth Description
POST /api/v1/events JWT Create event
GET /api/v1/events JWT List events (filtered + recommendation-ranked)
GET /api/v1/events/:id none Public event detail (shareable)
GET /api/v1/events/:id/public none Same, reduced payload
GET /api/v1/share/events/:id none HTML share page
PATCH /api/v1/events/:id JWT Update (also syncs chat room name on title change)
DELETE /api/v1/events/:id JWT Delete
POST /api/v1/events/:id/join JWT Join (access code + capacity + visibility checks)
GET /api/v1/events/:id/participants JWT List participants
POST /api/v1/events/:id/participants/:userId/approve JWT (host) Approve pending participant
DELETE /api/v1/events/:id/participants/:userId JWT (host) Remove participant
POST /api/v1/events/:id/start JWT (host) Force-start now
POST /api/v1/events/:id/cancel JWT (host) Cancel event
POST /api/v1/events/:id/transfer-ownership JWT (host) Transfer host role
POST /api/v1/events/:id/messages JWT Legacy: send chat message
GET /api/v1/events/:id/messages JWT Legacy: list chat messages
GET /api/v1/events/timezone JWT Resolve timezone from coords
GET /api/v1/users/:userId/events JWT Events a given user is in

Modern chat path

For interactive chat use the /api/v1/chat/* and /api/v1/ws/chat/:eventId endpoints, not the legacy /events/:id/messages ones.

Key types

type EventService interface {
    CreateEvent(ctx context.Context, event *models.Event) error
    GetEvents(ctx context.Context, filter repository.EventFilter) ([]models.Event, error)
    GetEventByID(ctx context.Context, id string) (*models.Event, error)
    UpdateEvent(ctx context.Context, eventID string, updates map[string]interface{}) error
    DeleteEvent(ctx context.Context, eventID string) error
    CancelEvent(ctx context.Context, eventID string) error
    TransferOwnership(ctx context.Context, eventID, newHostID string) error

    JoinEvent(ctx context.Context, eventID, userID, accessCode string) (status string, err error)
    LeaveEvent(ctx context.Context, eventID, userID string) error
    ApproveParticipant(ctx context.Context, eventID, userID, requestingUserID string) error
    RemoveParticipant(ctx context.Context, eventID, userID, requestingUserID string) error
    GetParticipants(ctx context.Context, eventID string) ([]models.EventParticipant, error)
    // ...
}

type EventLifecycleService interface {
    UpdateEventStatuses(ctx context.Context) error
    GetActiveEvents(ctx context.Context, filter repository.EventFilter) ([]models.Event, error)
    ArchiveCompletedEvents(ctx context.Context) error
    PurgeArchivedEvents(ctx context.Context, olderThanDays int) error
    CleanupOrphanedEvents(ctx context.Context) error
    CheckAndCleanupEvent(ctx context.Context, eventID string) (bool, error)
    CleanupUserEvents(ctx context.Context, userID uuid.UUID) error
}

Data model

  • events — host_id, title, description, start/end, status, visibility, access_code, max_capacity, waitlist_enabled, auto_accept, required_attestations, sponsorship_tier, chat_room_id, location_id, coordinates (PostGIS)
  • event_participants — composite of (event_id, user_id) with status (pending | approved | waitlist)
  • chat_rooms (type='group') — created alongside every event, joined via event.chat_room_id
  • chat_room_participants — host added as admin, joiners added as member on approve

Dependencies

Notable behavior

Join eligibility checks (in order)

  1. Access code matches (if set)
  2. Capacity not exceeded — overflow goes to waitlist only if waitlist_enabled
  3. Required attestations present on the user (waitlisted users also checked)
  4. Visibility rule — friends requires the joiner to be a friend of the host
  5. If auto_accept = true the joiner is set to approved and added to the chat room immediately; otherwise status is pending and the host must approve

Transfer of ownership

TransferOwnership removes the new host from event_participants (they're now the host, not a participant) and inserts the old host as an approved participant. The chat room participation flips accordingly. The action does not require the new host to consent in code — the handler is host-gated, so the host is unilaterally handing the room off.

Orphan detection on leave

LeaveEvent calls EventLifecycleService.CheckAndCleanupEvent, which deletes the event if the host is also gone and there are no other active participants. If participants remain but the host is gone, the event is moved to cancelled rather than deleted so the conversation history sticks around.

Message retention

Messages on an event chat room are purged by MessagePurgerService (the purge cron at 24h interval) once end_time + retention_days is past. Friend (1:1 / group) DM messages are never purged by that worker — only user deletion removes them.

Where to look