Events (RabbitMQ)
Identity emits domain events onto a shared RabbitMQ topic exchange so tenant-services can subscribe instead of polling REST. F7 introduced the publisher infrastructure; this page documents the contract every consumer needs to follow.
The audit (cross-repo #43, #44, #57, #61) called out the previous "REST-poll only" status quo as the root cause of stale permission caches that took up to an hour to invalidate after a role change. The new pipeline reduces that to seconds (broker latency).
Exchange + envelope
| Property | Value |
|---|---|
| Exchange | vecton.events |
| Type | topic |
| Durable | true |
| Auto-delete | false |
| Delivery mode | persistent (2) |
Every message body is a JSON envelope:
{
"id": "identity.tenant.member_added:9f8a23ce4f1b8c12",
"type": "identity.tenant.member_added",
"service": "identity",
"occurred_at": "2026-05-08T10:23:14+00:00",
"payload": {
"tenant_id": "01h...",
"tenant_slug": "acme",
"user_id": 42,
"is_owner": false,
"actor_id": 7
}
}
The envelope ships with content_type: application/json and a
message_id matching id.
Routing keys
F7 wired four routing keys onto domain events:
| Routing key | Event class | Payload fields |
|---|---|---|
identity.tenant.member_added | App\Events\UserAddedToTenant | tenant_id, tenant_slug, user_id, is_owner, actor_id |
identity.tenant.member_removed | App\Events\UserRemovedFromTenant | tenant_id, tenant_slug, user_id, actor_id |
identity.user.scheduled_for_deletion | App\Events\UserScheduledForDeletion | user_id, scheduled_for_deletion |
identity.policy.updated | App\Events\PolicyUpdated | policy_id, policy_name, affected_user_ids[] |
Subscribing to identity.* gets every identity event; binding to
identity.tenant.* gets membership churn only.
Consumer pattern
A tenant-service consumer typically declares a queue and binds it to one or more routing keys:
$channel = $connection->channel();
$channel->exchange_declare('vecton.events', 'topic', false, true, false);
$channel->queue_declare('crm.identity-mirror', false, true, false, false);
$channel->queue_bind('crm.identity-mirror', 'vecton.events', 'identity.tenant.*');
$channel->queue_bind('crm.identity-mirror', 'vecton.events', 'identity.policy.updated');
$channel->basic_consume('crm.identity-mirror', '', false, false, false, false, function ($msg) {
$envelope = json_decode($msg->body, true);
handle($envelope);
$msg->ack();
});
Idempotency: each id is unique per emit. Consumers should dedupe on
id if they care about exactly-once semantics — the publisher only
guarantees at-least-once.
Failure model (current state)
EventPublisher::publish is best-effort:
- Connection errors → log + return false. The request path is not blocked.
- Broker outage → events emitted during the outage are lost unless the consumer reads from a durable queue that was bound before the outage.
This is a known F-future limitation — the audit identified an outbox pattern (DB row + dedicated worker) as the right fix. F7 chose the best-effort path because it's better than the previous "REST-poll with 1h cache" status quo and it doesn't add a writer-side dependency on RabbitMQ availability.
Event listener wiring
EventServiceProvider::$listen (F7 update):
PolicyUpdated::class => [
InvalidateUserTokensOnPolicyChange::class,
[PublishUserLifecycleToBroker::class, 'onPolicyUpdated'],
],
UserAddedToTenant::class => [
SendUserAddedToTenantNotification::class,
CancelUserDeletionOnTenantAssignment::class,
[PublishUserLifecycleToBroker::class, 'onUserAddedToTenant'],
],
UserRemovedFromTenant::class => [
SendUserRemovedFromTenantNotification::class,
[PublishUserLifecycleToBroker::class, 'onUserRemovedFromTenant'],
],
UserScheduledForDeletion::class => [
SendUserScheduledForDeletionNotification::class,
[PublishUserLifecycleToBroker::class, 'onUserScheduledForDeletion'],
],
The publisher listener is an additional subscriber alongside the email/cleanup listeners — broker availability does not affect those side effects.
Adding a new event type
- Define / reuse a domain event class under
App\Events. - Add a method on
App\Listeners\PublishUserLifecycleToBrokerthat maps the event to a routing key + payload. - Register the method as a listener in
EventServiceProvider::$listen. - Document the routing key + payload in the table above.
- Add a consumer-side handler in the relevant tenant-service.
Where to look in code
app/Services/EventPublisher.php— connection, envelope, publishapp/Listeners/PublishUserLifecycleToBroker.php— domain → routing keyapp/Providers/EventServiceProvider.php— listener wiringconfig/queue.php— RabbitMQ host config (the publisher reuses it)