Events
ArrowLabs.Auth.Client hosts a RabbitMQ consumer that drains your per-app queue and dispatches typed payloads to handlers you register in DI. It uses RabbitMQ.Client with automatic connection recovery and attaches to arrowlabs.events.{your_client_id} (consume-only). See Platform events for the model.
1. Implement handlers
Section titled “1. Implement handlers”using ArrowLabs.Auth.Client;
public sealed class ProvisionOnRegister(IAccountService accounts) : IArrowLabsEventHandler<UserRegisteredPayload>{ public Task HandleAsync(UserRegisteredPayload payload, EventEnvelope envelope, CancellationToken ct) => accounts.ProvisionAsync(payload.UserId, payload.Email, ct);}
public sealed class DisableOnSuspend(IAccountService accounts) : IArrowLabsEventHandler<UserSuspendedPayload>{ public Task HandleAsync(UserSuspendedPayload payload, EventEnvelope envelope, CancellationToken ct) => accounts.DisableAsync(payload.UserId, ct);}Handlers are resolved per-message from a DI scope, so they can inject scoped services. Multiple handlers per event type are allowed.
2. Register the consumer and handlers
Section titled “2. Register the consumer and handlers”builder.Services.AddArrowLabsEventConsumer(options =>{ options.AmqpUrl = builder.Configuration["Amqp:Url"]!; // amqps://user:pass@host:5671 options.ClientId = "your-client-id"; // → queue arrowlabs.events.your-client-id options.PrefetchCount = 10; // max in-flight; default 10});
builder.Services.AddArrowLabsEventHandler<ProvisionOnRegister>();builder.Services.AddArrowLabsEventHandler<DisableOnSuspend>();The consumer runs as an IHostedService — it starts and stops with your app.
Delivery & failure
Section titled “Delivery & failure”- Ack on success. When every handler for a message resolves, the message is acknowledged.
- Failure → dead-letter, no requeue. If a handler throws (or the body can’t be decoded), the message is dead-lettered — no poison loops. Failed events are inspectable from the admin portal.
Idempotency
Section titled “Idempotency”Events are at-least-once. Make handlers idempotent; register an IArrowLabsEventDeduplicator to wire EventId to your own store:
public sealed class RedisDeduplicator(IConnectionMultiplexer redis) : IArrowLabsEventDeduplicator{ public async Task<bool> IsDuplicateAsync(string eventId, CancellationToken ct) => await redis.GetDatabase().KeyExistsAsync($"evt:{eventId}");
public Task MarkProcessedAsync(string eventId, CancellationToken ct) => redis.GetDatabase().StringSetAsync($"evt:{eventId}", "1", TimeSpan.FromDays(7));}
// builder.Services.AddSingleton<IArrowLabsEventDeduplicator, RedisDeduplicator>();IsDuplicateAsync is checked before handlers run (a duplicate is acked and skipped); MarkProcessedAsync runs after they all succeed, before the ack.
The catalogue is exposed as PlatformEventTypes constants, and every payload (UserRegisteredPayload, UserSuspendedPayload, …) is a strongly-typed record.