Skip to main content

Events

:::info Source Sourced from services/delivery-service/EVENT_SCHEMAS.md in the documentation repo. :::

Companion: 04 Event-Driven Architecture · DOMAIN_MODEL · APPLICATION_LOGIC

All events use the platform EventEnvelope<T> defined in doc 04. This document specifies the payload: T for each delivery-service event.

1. NATS Stream Configuration

PropertyValue
Stream nameDELIVERY
Retention30 days hot (JetStream), 13 months cold (S3/R2)
Retention classoperational
Subject prefixdelivery.*
Partition keyplaySessionId (for session events), mountId (for offline events)

2. Published Events

2.1 delivery.play_session.started.v1

Subject: delivery.play_session.started.v1 Trigger: PlaySession transitions from init to active

interface PlaySessionStartedPayload {
sessionId: PlaySessionId;
tenantId: TenantId;
userId: UserId;
enrollmentId: EnrollmentId;
courseVersionId: CourseVersionId;
deviceId: DeviceId;
attemptNumber: number;
cursor: NavigationCursor;
isOffline: boolean;
offlineMountId?: string;
startedAt: ISODate;
}

Consumers: progress-service (open attempt), analytics-service, sync-service

2.2 delivery.play_session.navigated.v1

Subject: delivery.play_session.navigated.v1 Trigger: Cursor moves to a new position

interface PlaySessionNavigatedPayload {
sessionId: PlaySessionId;
tenantId: TenantId;
userId: UserId;
enrollmentId: EnrollmentId;
fromCursor: NavigationCursor;
toCursor: NavigationCursor;
navigationType: 'next' | 'prev' | 'jump' | 'branch';
durationOnPreviousMs: number; // time spent on previous position
navigatedAt: ISODate;
}

Consumers: progress-service (record navigation statement), analytics-service

2.3 delivery.play_session.completed.v1

Subject: delivery.play_session.completed.v1 Trigger: PlaySession transitions to completed

interface PlaySessionCompletedPayload {
sessionId: PlaySessionId;
tenantId: TenantId;
userId: UserId;
enrollmentId: EnrollmentId;
courseVersionId: CourseVersionId;
attemptNumber: number;
durationSeconds: number;
lessonsVisited: number;
completedAt: ISODate;
}

Consumers: progress-service (close attempt, compute completion), analytics-service, notification-service

2.4 delivery.play_session.abandoned.v1

Subject: delivery.play_session.abandoned.v1 Trigger: PlaySession transitions to abandoned

interface PlaySessionAbandonedPayload {
sessionId: PlaySessionId;
tenantId: TenantId;
userId: UserId;
enrollmentId: EnrollmentId;
courseVersionId: CourseVersionId;
attemptNumber: number;
reason: 'user_initiated' | 'inactivity_timeout' | 'enrollment_revoked' | 'tamper_detected';
durationSeconds: number;
lastCursor: NavigationCursor;
abandonedAt: ISODate;
}

Consumers: progress-service (close attempt as abandoned), analytics-service, notification-service (re-engagement)

2.5 delivery.play_session.tutor_turn.completed.v1

Subject: delivery.play_session.tutor_turn.completed.v1 Trigger: AI tutor completes a response

interface TutorTurnCompletedPayload {
sessionId: PlaySessionId;
tenantId: TenantId;
userId: UserId;
turnId: string;
lessonId: string;
promptTokens: number;
completionTokens: number;
aiProvenance: {
model: string;
local: boolean;
traceId: string;
cost?: { microUSD: number };
};
durationMs: number;
rating?: 'helpful' | 'unhelpful';
completedAt: ISODate;
}

Consumers: analytics-service (AI usage analytics), ai-gateway-service (cost tracking)

2.6 delivery.offline_mounted.v1

Subject: delivery.offline.mounted.v1 Trigger: OfflineMount created successfully

interface OfflineMountedPayload {
mountId: string;
tenantId: TenantId;
userId: UserId;
deviceId: DeviceId;
bundleId: BundleId;
courseVersionId: CourseVersionId;
expiresAt: ISODate;
mountedAt: ISODate;
}

Consumers: sync-service (register device for sync), analytics-service, content-service (track bundle distribution)

2.7 delivery.offline_unmounted.v1

Subject: delivery.offline.unmounted.v1 Trigger: OfflineMount terminated

interface OfflineUnmountedPayload {
mountId: string;
tenantId: TenantId;
userId: UserId;
deviceId: DeviceId;
bundleId: BundleId;
reason: 'user_initiated' | 'expired' | 'tamper_detected' | 'license_revoked';
unmountedAt: ISODate;
}

Consumers: sync-service (deregister device), analytics-service, content-service

3. Consumed Events

3.1 content.play_package.built.v1

Producer: content-service Consumer group: delivery-play-package-projector

interface PlayPackageBuiltPayload {
courseVersionId: CourseVersionId;
playPackageId: string;
manifestUrl: string;
manifestChecksum: SHA256;
builtAt: ISODate;
}

Handler: Cache manifest; invalidate stale versions.

3.2 enrollment.created.v1

Producer: enrollment-service Consumer group: delivery-enrollment-projector

interface EnrollmentCreatedPayload {
enrollmentId: EnrollmentId;
tenantId: TenantId;
userId: UserId;
courseVersionId: CourseVersionId;
createdAt: ISODate;
}

Handler: Store enrollment projection; pre-warm manifest cache.

3.3 content.play_package.bundle.published.v1

Producer: content-service Consumer group: delivery-bundle-projector

interface BundlePublishedPayload {
bundleId: BundleId;
courseVersionId: CourseVersionId;
checksum: SHA256;
sizeBytes: number;
publishedAt: ISODate;
}

Handler: Mark bundle available for offline mount.

3.4 content.bundle.tamper_detected.v1

Producer: content-service Consumer group: delivery-tamper-handler

interface BundleTamperDetectedPayload {
bundleId: BundleId;
deviceId: DeviceId;
tenantId: TenantId;
userId: UserId;
detectedAt: ISODate;
evidence: string;
}

Handler: Force-unmount affected mounts; pause active sessions; emit unmount events; log security alert.

4. Event Versioning Strategy

  • All events start at v1.
  • Additive changes (new optional fields) do NOT bump version.
  • Breaking changes (field removal, type change, semantic change) require a new v2 subject.
  • Dual-publish window: minimum 2 release cycles where both v1 and v2 are published.
  • Consumers must ignore unknown fields (forward-compatible).

5. Schema Registry

Event schemas are registered in the platform schema registry:

schemas://delivery/play_session/started/v1#sha256-XXXX
schemas://delivery/play_session/navigated/v1#sha256-XXXX
schemas://delivery/play_session/completed/v1#sha256-XXXX
schemas://delivery/play_session/abandoned/v1#sha256-XXXX
schemas://delivery/play_session/tutor_turn_completed/v1#sha256-XXXX
schemas://delivery/offline/mounted/v1#sha256-XXXX
schemas://delivery/offline/unmounted/v1#sha256-XXXX

6. Dead Letter Queue

  • DLQ subject: DELIVERY.dlq
  • Poison messages routed after 5 retry attempts with exponential backoff (1s, 2s, 4s, 8s, 16s).
  • Alert on non-empty DLQ (PagerDuty P3 after 10 messages, P2 after 100).
  • DLQ messages include original event, consumer group, error reason, and attempt count.