Gå til innholdet
Digilist Dokumentasjon
Book demo

ARKITEKTUR · EVENT-BUS

Hendelse-buss

Outbox-mekanikk, 110+ topic-katalog, abonnent-kart, retries og webhook-routing. Slik snakker komponenter på tvers uten å lese hverandres tabeller.

Hendelse-bussen er outbox-basert: produsenter skriver hendelser inn i en outboxEvents-tabell som del av samme transaksjon som forretningsskriven. En cron-jobb plukker dem opp og leverer til abonnenter. Garanterer at hendelser aldri går tapt — selv hvis dispatch-jobben er nede.

1. Mekanikk

PRODUSENT OUTBOX ABONNENT
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ bookings │ │ outboxEvents │ │ notifications│
│ .create │ ─── emit ──> │ topic, payload │ ─── relay ─> │ .send │
│ │ (atomic) │ state=pending │ (5s cron) │ │
└──────────────┘ └──────────────────┘ └──────────────┘
┌──────────────────┐
│ outboxDeliveries │
│ per-subscriber │
│ retry + state │
└──────────────────┘

Garantier

GarantiHvordan
At-least-onceCron-relay leverer til alle abonnenter; abonnent må være idempotent.
Atomicitetemit(ctx, ...) skriver i samme Convex-mutasjon som forretningsskriven.
Ordring per topicIndex by_topic_createdAt. Abonnent prosesserer i ts-rekkefølge.
Retry med backoff1 min → 5 min → 30 min → 6 t → 1 d. Etter 5 forsøk → dead-letter.
Observabilitet/platform/event-bus viser pending, failing, dead-lettered per topic.

2. Topic-katalog (utdrag)

110+ topics er katalogisert i eventTopics. Tabellen under viser de mest aktive (>1 000 dispatches/dag i mai 2026):

TopicProdusentAbonnenterBeskrivelse
booking.createdbookingsnotifications, audit, intelligenceNy booking opprettet (alle states inkl. pending_approval)
booking.approvedbookingsnotifications, payments, calendarSaksbehandler har godkjent
booking.rejectedbookingsnotifications, auditSaksbehandler har avvist
booking.cancelledbookingsnotifications, payments, ledgerAvbestilt av innbygger eller saksbehandler
payment.intent.createdpaymentsbookings, auditStripe/Vipps intent opprettet
payment.capturedpaymentsbookings, ledger, notifications, accountingBetaling fullført, klar til payout
payment.failedpaymentsbookings, notifications, auditBetaling feilet (utløpt kort, avslag)
payment.refundedpaymentsbookings, ledger, notificationsRefusjon fullført
payout.completedledgeraccounting, notificationsUtbetaling til tenant fullført
user.signed_upidentitynotifications, audit, intelligenceNy bruker registrert (ID-porten|BankID|magic-link)
user.signed_inidentityaudit, intelligenceInnlogging — feed for risiko-deteksjon
mfa.requiredidentitynotificationsStep-up auth påkrevd for sensitiv handling
resource.publishedresourcesnotifications, intelligence, auditLokale eller event publisert til markedsplassen
message.postedmessagingnotifications, auditNy melding i tråd innbygger ↔ saksbehandler
audit.gdpr.export_requestedauditaudit (selv), notificationsDSAR-eksport startet
audit.gdpr.deletion_scheduledauditaudit, notifications, identitySletting planlagt etter grace-periode
season.allocation.computedseasonsnotifications, bookingsSesongleie-fordeling kjørt, lag/foreninger varslet
subscription.upgradedsubscriptionstenants, accounting, notificationsTenant byttet til høyere plan
cron.daily.06_00(system)intelligence, reporting, accountingDaglig kl. 06:00 UTC — content-agent, audits, EHF-batching
webhook.stripe.receivedhttppaymentsInnkommende Stripe-webhook (deduplisering på eventId)
webhook.vipps.receivedhttppaymentsInnkommende Vipps-callback
webhook.signicat.receivedhttpidentityID-porten / BankID-callback

3. Webhook-routing

Eksterne webhooks går via http.ts → topic på outbox-bussen → abonnent. Dette dekobler eksterne tider fra interne mutasjoner og lar oss replay’e hele webhook-strømmen ved feil.

Stripe / Vipps / Signicat
▼ POST /webhooks/<provider>
┌──────────────────────────┐
│ http.ts │ Verifiser signatur → emit topic
│ - verify HMAC │
│ - dedupe på eventId │
│ - emit "webhook.<x>.received"
└──────────────────────────┘
▼ (5s relay cron)
┌──────────────────────────┐
│ payments.handleWebhook │ Idempotent abonnent
│ - resolve intent → booking
│ - emit "payment.captured"
└──────────────────────────┘

4. Abonnement og idempotens

Abonnenter MÅ være idempotente — samme hendelse kan leveres flere ganger.

// God praksis: deduplisering på naturlig nøkkel
export const onPaymentCaptured = internalMutation({
args: { paymentId: v.id("payments"), intentId: v.string() },
handler: async (ctx, { paymentId, intentId }) => {
// Idempotency-sjekk: har vi allerede prosessert denne intent?
const existing = await ctx.db
.query("ledgerEntries")
.withIndex("by_intent", (q) => q.eq("intentId", intentId))
.first();
if (existing) return;
await ctx.db.insert("ledgerEntries", { /* … */ intentId });
},
});

5. Dead-letter og operasjon

Hendelser som feiler 5 ganger flytter til dead-letter-tilstand. Operatør kan se og replay’e fra /platform/event-bus/dead-letters.

Terminal window
# Replay alle dead-letters for et topic siste 24t
curl -X POST https://api.digilist.no/platform/event-bus/replay \
-H "Authorization: Bearer $TOKEN" \
-d '{"topic":"payment.captured","since":"24h"}'

Beslektet