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(typegroup) and add the host asadmin - 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 legacyPOST /events/:id/messages) - Log every event-related action through
ActivityLogService
EventLifecycleService¶
UpdateEventStatuses— walk every event and calldetermineStatus(now)to roll the state machine forwardArchiveCompletedEvents— flipcompletedtoarchivedonceend_time + retention_daysis pastPurgeArchivedEvents(olderThanDays)— hard-delete archived events pastolderThanDaysCleanupOrphanedEvents— 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 fromLeaveEventCleanupUserEvents— 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) withstatus(pending|approved|waitlist)chat_rooms(type='group') — created alongside every event, joined viaevent.chat_room_idchat_room_participants— host added asadmin, joiners added asmemberon approve
Dependencies¶
repository.EventRepository,UserRepository,FriendRepositoryRedisService— Redis GEO index (AddEventLocation,RemoveEventLocation)RecommendationService— ranks theGetEventsresult set by category affinity + distanceEventLifecycleService— orphan cleanup onLeaveEventActivityLogService— async event creation / join logs
Notable behavior¶
Join eligibility checks (in order)
- Access code matches (if set)
- Capacity not exceeded — overflow goes to
waitlistonly ifwaitlist_enabled - Required attestations present on the user (waitlisted users also checked)
- Visibility rule —
friendsrequires the joiner to be a friend of the host - If
auto_accept = truethe joiner is set toapprovedand added to the chat room immediately; otherwise status ispendingand 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¶
backend/internal/services/event_service.gobackend/internal/services/event_lifecycle_service.gobackend/internal/handlers/event_handler.gobackend/internal/handlers/user_event_handler.gobackend/internal/repository/event_repository.gobackend/internal/models/event.gobackend/internal/scheduler/manager.go—status_updateandpurgecron registration