ADR-006: Event-Driven Architecture¶
Status¶
Accepted
Date¶
2026-07-03
Context¶
Real-time collaboration platforms are inherently event-driven:
- Participants join and leave
- Tracks are published and unpublished
- Recordings start and stop
- Transcripts update in real-time
OmniMeet must expose these events in a provider-neutral way while supporting both:
- Server-side event handling — Webhooks for backend integration
- Client-side event handling — Real-time updates in agent participants
Decision¶
We will implement a unified event model with support for multiple delivery mechanisms.
Event Types¶
type EventType string
const (
// Meeting lifecycle
EventMeetingCreated EventType = "meeting.created"
EventMeetingStarted EventType = "meeting.started"
EventMeetingEnded EventType = "meeting.ended"
// Participant lifecycle
EventParticipantJoined EventType = "participant.joined"
EventParticipantLeft EventType = "participant.left"
EventParticipantUpdated EventType = "participant.updated"
// Track events
EventTrackPublished EventType = "track.published"
EventTrackUnpublished EventType = "track.unpublished"
EventTrackMuted EventType = "track.muted"
EventTrackUnmuted EventType = "track.unmuted"
EventTrackSubscribed EventType = "track.subscribed"
EventTrackUnsubscribed EventType = "track.unsubscribed"
// Recording events
EventRecordingStarted EventType = "recording.started"
EventRecordingStopped EventType = "recording.stopped"
EventRecordingFailed EventType = "recording.failed"
// Transcript events
EventTranscriptUpdated EventType = "transcript.updated"
EventTranscriptFinalized EventType = "transcript.finalized"
// Data events
EventDataMessageReceived EventType = "data_message.received"
// Connection events
EventConnectionQualityChanged EventType = "connection.quality_changed"
EventReconnecting EventType = "connection.reconnecting"
EventReconnected EventType = "connection.reconnected"
)
Event Structure¶
type Event struct {
ID string `json:"id"`
Type EventType `json:"type"`
MeetingID string `json:"meeting_id"`
Timestamp time.Time `json:"timestamp"`
Data any `json:"data"`
}
// Event-specific data types
type ParticipantJoinedData struct {
Participant Participant `json:"participant"`
}
type TrackPublishedData struct {
Participant Participant `json:"participant"`
Track Track `json:"track"`
}
type TranscriptUpdatedData struct {
ParticipantID string `json:"participant_id"`
Text string `json:"text"`
IsFinal bool `json:"is_final"`
Confidence float64 `json:"confidence"`
Timestamp time.Time `json:"timestamp"`
}
Event Delivery Mechanisms¶
1. Callback Registration (Provider Interface)¶
type EventHandler func(ctx context.Context, event Event) error
type MeetingProvider interface {
// Register event handler for server-side events
OnEvent(handler EventHandler)
// ... other methods
}
2. Channel-Based (Agent Participant)¶
type AgentParticipant interface {
// Real-time event channel
Events() <-chan Event
// Typed handlers for common events
OnParticipantJoined(handler func(Participant))
OnParticipantLeft(handler func(Participant))
OnTrackPublished(handler func(Track))
// ... other methods
}
3. Webhook Handler (Server Integration)¶
// WebhookHandler processes provider webhooks
type WebhookHandler interface {
// HandleWebhook processes incoming webhook and returns normalized events
HandleWebhook(r *http.Request) ([]Event, error)
// ValidateSignature verifies webhook authenticity
ValidateSignature(r *http.Request, secret string) error
}
// Provider-specific implementations
type LiveKitWebhookHandler struct{}
type DailyWebhookHandler struct{}
Provider Event Mapping¶
| OmniMeet Event | LiveKit | Daily |
|---|---|---|
participant.joined |
ParticipantJoined |
participant-joined |
participant.left |
ParticipantLeft |
participant-left |
track.published |
TrackPublished |
track-started |
track.unpublished |
TrackUnpublished |
track-stopped |
recording.started |
EgressStarted |
recording-started |
recording.stopped |
EgressEnded |
recording-stopped |
Usage Patterns¶
Server-Side (Webhooks)¶
// Configure webhook endpoint
meetingProvider := omnimeet.GetMeetingProvider("livekit")
meetingProvider.OnEvent(func(ctx context.Context, event Event) error {
switch event.Type {
case omnimeet.EventParticipantJoined:
data := event.Data.(ParticipantJoinedData)
log.Printf("Participant joined: %s", data.Participant.Name)
case omnimeet.EventMeetingEnded:
log.Printf("Meeting ended: %s", event.MeetingID)
}
return nil
})
// HTTP handler for provider webhooks
http.HandleFunc("/webhooks/livekit", func(w http.ResponseWriter, r *http.Request) {
events, err := meetingProvider.WebhookHandler().HandleWebhook(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for _, event := range events {
meetingProvider.EmitEvent(event)
}
w.WriteHeader(http.StatusOK)
})
Agent-Side (Real-Time)¶
agentParticipant.OnParticipantJoined(func(p Participant) {
log.Printf("Participant joined: %s", p.Name)
if p.Kind == ParticipantKindHuman {
// Greet the human
audioGreeting := omnivoice.Synthesize(ctx, "Hello! How can I help you?")
agentParticipant.PublishAudio(ctx, audioGreeting)
}
})
agentParticipant.OnTrackPublished(func(t Track) {
if t.Kind == TrackKindAudio {
// Start transcribing this participant's audio
go transcribeParticipant(ctx, t.ParticipantID)
}
})
Consequences¶
Positive¶
- Unified model — Same event types across all providers
- Multiple delivery mechanisms — Webhooks, callbacks, channels
- Type safety — Strongly typed event data
- Provider mapping — Clear mapping from provider events to OmniMeet events
Negative¶
- Event translation overhead — Must map provider-specific events
- Eventual consistency — Webhooks may arrive after real-time events
Mitigations¶
- Event mapping is one-time effort per provider
- Document expected event ordering and timing
- Provide idempotency support via event IDs
Alternatives Considered¶
Only Webhooks¶
Rejected because agent participants need real-time events, not HTTP callbacks.
Only Callbacks¶
Rejected because server-side integrations benefit from webhook-based event delivery.
Generic Event Bus¶
Considered using a generic pub/sub system, but rejected because:
- Adds external dependency
- Overkill for single-meeting context
- Provider-native event mechanisms are sufficient