Events
:::info Source
Sourced from services/tenant-service/EVENT_SCHEMAS.md in the documentation repo.
:::
Blueprint doc 5 of 17. Companion: 04 Event-Driven | APPLICATION_LOGIC
1. Envelope
Every event uses the platform EventEnvelope (Frozen at F01, M0 end):
interface EventEnvelope<T> {
eventId: ULID;
eventType: string; // e.g., 'tenant.org.provisioned'
eventVersion: number; // 1, 2, …
schemaUri: string; // 'schemas://tenant/org/provisioned/v1#sha256-...'
source: { service: 'tenant-service'; instance: string; commit: string };
occurredAt: ISODate;
ingestedAt: ISODate;
causationId?: ULID;
correlationId: ULID;
tenantId: TenantId;
actor: { type: 'user' | 'system' | 'api_key' | 'service_account'; id: string };
payload: T;
partitionKey: string; // usually tenantId
outbox?: { dbWriteTs: ISODate; outboxId: ULID };
retentionClass: 'operational' | 'regulated' | 'audit';
dataResidency: Region;
}
2. NATS Stream Configuration
| Stream name | Subjects | Retention (hot) | Retention (cold) | Partitions |
|---|---|---|---|---|
TENANT | tenant.> | 180 days | 7 years (regulated) | 16 (by tenantId hash) |
TENANT.DLQ | tenant.dlq.> | Until acknowledged | N/A | 1 |
All tenant events are regulated retention class because tenant provisioning, membership, and role changes are audited for compliance (SOX, ISO 27001, GDPR).
3. Events Published
3.1 tenant.org.provisioned.v1
Emitted when a new tenant is created.
interface TenantProvisionedPayload {
tenantId: TenantId;
type: 'org' | 'provider' | 'individual' | 'org+provider';
name: string;
slug: string;
homeRegion: Region;
plan: { id: string; addons: string[] };
status: 'trial' | 'active';
ownerEmail: Email;
ownerMembershipId: ULID;
rootOrgUnitId: OrgUnitId;
createdAt: ISODate;
}
Consumers: identity-service (link owner user), notification-service (welcome email), billing-service (plan activation), catalog-service (seed default taxonomies), sync-service (register replicable aggregates), analytics-service.
3.2 tenant.org.settings_updated.v1
interface TenantSettingsUpdatedPayload {
tenantId: TenantId;
changedFields: string[]; // e.g., ['mfaRequired', 'aiTutorEnabled']
previous: Partial<TenantSettings>;
current: Partial<TenantSettings>;
updatedAt: ISODate;
}
Consumers: identity-service (MFA enforcement), ai-gateway-service (aiEnabled), delivery-service (offlineEnabled), notification-service.
3.2a tenant.policy.mfa_changed.v1 (US-5)
Emitted when tenant MFA / passkey policy flags change (e.g. passkeyRequiredForAdmins).
interface TenantPolicyMfaChangedV1 {
tenantId: TenantId;
passkeyRequiredForAdmins: boolean;
changedBy: UserId;
at: ISODate;
}
Consumers: identity-service (login enforcement), analytics-service (admin passkey coverage), audit-service.
3.3 tenant.org.suspended.v1
interface TenantSuspendedPayload {
tenantId: TenantId;
reason: string;
suspendedAt: ISODate;
effectiveAt: ISODate;
}
Consumers: All services (halt service for tenant). API gateway uses this to reject requests.
3.4 tenant.org.closed.v1
interface TenantClosedPayload {
tenantId: TenantId;
closedAt: ISODate;
dataRetentionUntil: ISODate; // post-close retention per plan
}
Consumers: All services (initiate data archival/deletion per retention policy).
3.5 tenant.org.user_invited.v1
interface UserInvitedPayload {
membershipId: ULID;
tenantId: TenantId;
email: Email;
userId?: UserId; // populated if user already registered
roleIds: RoleId[];
orgUnitIds: OrgUnitId[];
invitedBy: UserId;
invitedAt: ISODate;
inviteToken: string; // short-lived signed token for accept link
expiresAt: ISODate;
}
Consumers: notification-service (invite email), identity-service (pre-register if userId null).
3.6 tenant.org.membership_activated.v1
interface MembershipActivatedPayload {
membershipId: ULID;
tenantId: TenantId;
userId: UserId;
roleIds: RoleId[];
orgUnitIds: OrgUnitId[];
activatedAt: ISODate;
trigger: 'invite_accepted' | 'auto_link_on_registration' | 'sso_jit_provisioning';
}
Consumers: notification-service (welcome email), enrollment-service (attach pending assignments), assignment-service (re-evaluate dynamic groups).
3.7 tenant.org.membership_suspended.v1
interface MembershipSuspendedPayload {
membershipId: ULID;
tenantId: TenantId;
userId: UserId;
reason: string;
suspendedBy: UserId;
suspendedAt: ISODate;
}
Consumers: identity-service (revoke sessions), delivery-service (block new play sessions), assignment-service.
3.8 tenant.role.created.v1
interface RoleCreatedPayload {
roleId: RoleId;
tenantId: TenantId;
name: string;
permissions: Permission[];
isSystem: false;
createdBy: UserId;
createdAt: ISODate;
}
3.9 tenant.role.updated.v1
interface RoleUpdatedPayload {
roleId: RoleId;
tenantId: TenantId;
changes: {
name?: { previous: string; current: string };
permissions?: { added: Permission[]; removed: Permission[] };
};
updatedBy: UserId;
updatedAt: ISODate;
}
Consumers: All services (invalidate authz cache for affected tenant).
3.10 tenant.role.deleted.v1
interface RoleDeletedPayload {
roleId: RoleId;
tenantId: TenantId;
deletedBy: UserId;
deletedAt: ISODate;
}
3.11 tenant.org_unit.created.v1
interface OrgUnitCreatedPayload {
orgUnitId: OrgUnitId;
tenantId: TenantId;
parentId?: OrgUnitId;
name: I18nString;
ltreePath: string;
createdAt: ISODate;
}
3.12 tenant.org_unit.moved.v1
interface OrgUnitMovedPayload {
orgUnitId: OrgUnitId;
tenantId: TenantId;
oldParentId?: OrgUnitId;
newParentId?: OrgUnitId;
oldLtreePath: string;
newLtreePath: string;
affectedSubtreeIds: OrgUnitId[]; // descendants whose paths changed
movedAt: ISODate;
}
Consumers: assignment-service (re-evaluate targeting), analytics-service.
3.13 tenant.org_unit.deleted.v1
interface OrgUnitDeletedPayload {
orgUnitId: OrgUnitId;
tenantId: TenantId;
deletedAt: ISODate;
}
3.14 tenant.dynamic_group.evaluated.v1
interface DynamicGroupEvaluatedPayload {
groupId: ULID;
tenantId: TenantId;
name: string;
memberCount: number;
memberIds: UserId[]; // may be paginated via followup events for >10k
addedMembers: UserId[]; // delta since last evaluation
removedMembers: UserId[];
evaluatedAt: ISODate;
queryHash: SHA256; // for dedup / cache invalidation
}
Consumers: assignment-service (re-target assignments), notification-service (segmented campaigns).
3.15 tenant.data_residency.changed.v1
Implementation note (2026-04): until the full data-plane migration ships, outbox payloads use a phase discriminator. Consumers must tolerate unknown phase values.
type DataResidencyChangedPayload =
| {
phase: 'scheduled';
migrationId: string;
tenantId: TenantId;
sourceRegion: Region;
targetRegion: Region;
status: 'scheduled';
scheduledAt: ISODate;
notifyOwner: boolean;
stepsCompleted?: number;
stepsTotal?: number;
}
| {
phase: 'step_completed';
dataPlane: 'pending';
migrationId: string;
tenantId: TenantId;
sourceRegion: Region;
targetRegion: Region;
stepsCompleted: number;
stepsTotal: number;
status: string;
}
| {
phase: 'orchestration_complete_stub';
dataPlane: 'pending';
migrationId: string;
tenantId: TenantId;
sourceRegion: Region;
targetRegion: Region;
stepsCompleted: number;
stepsTotal: number;
status: 'succeeded';
}
| {
/** Target end-state once cross-region copy + cutover are implemented */
tenantId: TenantId;
oldRegion: Region;
newRegion: Region;
migrationId: ULID;
startedAt: ISODate;
completedAt: ISODate;
dataClassesMigrated: string[];
};
Consumers: All services (update routing tables, invalidate regional caches). Stub phases (orchestration_complete_stub, dataPlane: 'pending') must not be treated as finished residency until homeRegion and regional stores are verified.
4. Events Consumed
4.1 identity.user.registered.v1
interface UserRegisteredPayload {
userId: UserId;
email: Email;
registeredAt: ISODate;
homeTenantId?: TenantId; // if registered via tenant signup flow
}
Handler: Query memberships with status='invited' matching email; transition to active and emit membership_activated events.
4.2 billing.subscription.changed.v1
interface SubscriptionChangedPayload {
tenantId: TenantId;
subscriptionId: string;
planId: string;
addons: string[];
status: 'active' | 'past_due' | 'canceled' | 'trialing';
previousStatus: 'active' | 'past_due' | 'canceled' | 'trialing';
effectiveAt: ISODate;
}
Handler: Update Tenant.plan; if status='canceled' and no grace period, suspend tenant; invalidate feature-flag cache; emit settings_updated.
4.3 gdpr.subject_request.received.v1
interface GDPRSubjectRequestPayload {
requestId: ULID;
userId: UserId;
requestType: 'export' | 'erasure' | 'rectification';
scope: 'all' | 'specific';
tenantIds?: TenantId[]; // scoped to specific tenants if set
deadline: ISODate;
}
Handler: For erasure, anonymize memberships (PII null); for export, compile tenant-service slice; emit gdpr.subject_request.slice_completed.v1 with service slice identifier.
5. Event Ordering & Partitioning
- Partition key:
tenantIdfor all tenant-* events. Guarantees FIFO per tenant. - Critical ordering:
tenant.org.provisioned.v1MUST precede any other event for a new tenant.tenant.org.user_invited.v1MUST precedemembership_activated.v1for same membership.tenant.role.created.v1MUST precede any role assignment events referencing it.
6. Schema Evolution Rules
Per platform event-driven spec (doc 04 §10):
- Additive changes only within a version (add optional fields).
- Breaking changes require new
vNand dual-publish window ≥ 1 milestone. - Never rename or remove fields in published versions.
- Enum narrowing forbidden. Enum widening (new values) allowed if consumers handle unknowns gracefully.
- Schema registered in
/event-schemas/tenant/Git repo with content-hash URI. - CI blocks PRs that modify frozen schemas without a version bump.
7. Dead Letter Queue
Events that fail schema validation or fail consumer processing after configured retries (default 5) land in tenant.dlq.{eventType}. Operators:
- Alert fires on first DLQ message.
- Runbook: FAILURE_MODES.md §DLQ Handling.
- Re-drive supported via admin endpoint
POST /admin/dlq/redrive.
8. Replay Semantics
All tenant events are replayable. Projections (e.g., authz cache, membership projections in other services) can be rebuilt from seq=0 by resetting the durable consumer cursor. Idempotency is enforced via the platform inbox pattern (unique eventId constraint).