Skip to content

Notifications

When a projection produces new state, Hapnd can push that change to your infrastructure. Notifications fire on projection state changes, not raw events — you receive the computed state, ready to use.

Three delivery mechanisms are available. Choose based on your use case, or combine them.

Outbound HTTP POST to your endpoint with HMAC-SHA256 signatures for verification. Best for server-to-server integration where you want Hapnd to push to you reliably.

Webhook configuration is provided when you upload a projection:

Terminal window
# Upload with notification config
curl -X POST https://hapnd-api.lightestnight.workers.dev/projections/upload \
-H "X-API-Key: your_key" \
-F "file=@projection.zip" \
-F 'notificationConfig={"webhook":{"url":"https://your-api.com/hooks/hapnd"}}'

Every webhook includes an HMAC-SHA256 signature in the headers. Verify it before processing:

var signature = request.Headers["X-Hapnd-Signature"];
var payload = await request.ReadAsStringAsync();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret));
var computed = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)));
if (signature != computed)
{
return Results.Unauthorized();
}

Hapnd generates webhook secrets automatically using the whsec_ prefix convention. Secrets are managed at two levels:

  • Tenant-level — a shared secret used by all projections by default
  • Per-projection — an isolated secret for projections that need independent rotation

Rotate secrets via the API:

Terminal window
# Rotate tenant-level secret (affects all projections using the shared secret)
curl -X POST https://hapnd-api.lightestnight.workers.dev/webhooks/rotate \
-H "X-API-Key: your_key"
# Rotate an isolated projection secret
curl -X POST https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/webhook/rotate \
-H "X-API-Key: your_key"

Real-time push via persistent WebSocket connections. Best for dashboards, live UIs, and reactive workflows where latency matters.

var subscription = hapnd.Subscriptions()
.OnStateChanged<OrderState>(async (update, ct) =>
{
Console.WriteLine($"Order {update.AggregateId} total: {update.State.Total}");
})
.OnError(async (error, ct) =>
{
error.Action.Reconnect();
})
.Subscribe();
// Later:
await subscription.DisposeAsync();
const subscription = hapnd.subscribe({
projections: [
{
id: "proj_orders",
onUpdate: async (update) => {
console.log(`Order ${update.aggregateId} total: ${update.state.total}`);
},
},
],
onError: async (error) => {
error.action.reconnect();
},
});
// Later:
await subscription.close();

Each projection gets its own WebSocket connection with automatic reconnection and sequence tracking. See the .NET SDK and TypeScript SDK docs for full details.

Cursor-based polling for environments where WebSockets aren’t practical or you prefer pull-based consumption.

Terminal window
# First request — get latest notifications
curl https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications \
-H "X-API-Key: your_key"
# Subsequent requests — pass the last sequence to get only new notifications
curl "https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications?afterSequence=42" \
-H "X-API-Key: your_key"

The response includes an array of notifications, each with a sequence number. Pass the highest sequence as afterSequence on your next request to get only new changes.

MechanismBest forTrade-offs
WebhooksServer-to-server, reliable deliveryHapnd manages retries and DLQ; you provide an endpoint
WebSocketReal-time dashboards, reactive workflowsRequires persistent connection; SDK handles reconnection
PollingSimple integrations, batch processingYou control the pace; higher latency than push

You can use multiple mechanisms for the same projection. For example, WebSocket for your dashboard and webhooks for your billing system.

Notification logs are retained for 7 days. After that, they’re archived to R2 storage and removed from the active log. The daily cleanup runs at 3am UTC.

If you need to reprocess notifications older than 7 days, the projection can be re-activated to replay from historical events.