Skip to content

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.

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.

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.

  • 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.

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.