Skip to main content

Event Schema Documentation

Event Schema Documentation

Project: Drop{{PROJECT_NAME}} Version: 0.1.0{{VERSION}} Date: 2026-02-23{{DATE}} Author: Platform Architect (AI){{AUTHOR}} Status: Draft | In Review | Approved Reviewers: Alem Bašić (CEO){{REVIEWERS}}

Document History

Version Date Author Changes
0.1 2026-02-23{{DATE}} Platform Architect (AI){{AUTHOR}} Initial draft — event-driven architecture not yet implemented

1. Event-Driven Architecture Overview

graph LR
    subgraph "Publishers"
        UserService["user-service"]
        OrderService["order-service"]
        PaymentService["payment-service"]
    end

    subgraph "Message Broker"
        Broker["{{Kafka / RabbitMQ / AWS SQS + SNS}}"]
    end

    subgraph "Consumers"
        NotifService["notification-service"]
        AnalyticsService["analytics-service"]
        SearchService["search-service"]
        AuditService["audit-service"]
    end

    UserService -->|user.* events| Broker
    OrderService -->|order.* events| Broker
    PaymentService -->|payment.* events| Broker

    Broker -->|filtered| NotifService
    Broker -->|all events| AnalyticsService
    Broker -->|entity events| SearchService
    Broker -->|all events| AuditService

Drop'sEvent-driven currentuse architecturecases isin athis synchronoussystem:

monolith
  • Decoupled there is no internal event bus or message queuenotifications (nouser.created BullMQ, SQS,send orwelcome RabbitMQ). All operations are handled synchronously within Next.js API route handlers.

    This document covers:

    1. External webhook events received from Sumsub (KYC status updates) — the only production external event sourceemail)
    2. InternalSearch index updates (entity.updated → reindex)
    3. Audit trail (all mutations → audit log events — database-level event tracking for compliancelog)
    4. PlannedCross-service eventdata schemasync (order.created for futureupdate async processing when an event bus is addedinventory)

2. ExternalMessage WebhookBroker EventsConfiguration

Broker: {{Apache Kafka | RabbitMQ | AWS SQS/SNS | NATS | Upstash Kafka}} Version: {{3.x}} Hosting: {{Confluent Cloud / self-hosted / AWS MSK}}

2.1Topic Sumsub/ Queue Naming Convention

{{DOMAIN}}.{{ENTITY}}.{{ACTION}}

Examples:
  user.user.created
  order.order.status_changed
  payment.invoice.generated
  notification.email.sent

Pattern rules:

  • All lowercase, dot-separated
  • Domain prefix = service name (without -service)
  • Entity = singular noun
  • Action = past tense verb (created, updated, deleted, completed)

Topic Configuration

TopicPartitionsReplicationRetentionCompaction
user.user.*637 daysNo
order.order.*12330 daysNo
payment.invoice.*6390 daysNo
*.*.deleted6330 daysLog compaction

3. Event Naming Conventions

ComponentRuleExamples
Full event type{domain}.{entity}.{action}user.user.created
DomainLowercase, matches service prefixuser, order, payment
EntitySingular noun, lowercase with underscoresuser, order_item, invoice
ActionPast-tense verb, lowercase with underscorescreated, updated, status_changed, payment_failed

Do NOT use:

  • Present tense (user.user.createKYCwrong)
  • Status
  • Generic Webhook

    Source: src/lib/services/mock-sumsub.ts, production Sumsub docs: https://docs.sumsub.com/reference/applicant-review

    Sumsub sends webhook events when a KYC applicant's review status changes.

    Endpoint: POST /api/kyc/webhooknames (TBDuser.user.changedpendingtoo Openvague)

  • Banking
  • Abbreviations provider(usr.usr.crtd integration)

    Webhookunreadable)

  • signature
verification:
HMAC-SHA256

4. usingEvent SUMSUB_SECRET_KEY.Envelope RejectFormat any(CloudEvents webhook1.0)

with invalid signature.

Event: applicantReviewed

Sent when Sumsub completes review of a KYC applicant.

{
  "specversion": "1.0",
  "type": "applicantReviewed"{{DOMAIN}}.{{ENTITY}}.{{ACTION}}",
  "reviewStatus"source": "completed"{{SERVICE_NAME}}",
  "applicantId"id": "sumsub_applicant_id"evt_01HX7M2K5N3P4Q5R6S7T8V9W0",
  "externalUserId"time": "usr_a1b2c3d4"2024-01-15T10:30:00.000Z",
  "reviewResult"datacontenttype": "application/json",
  "subject": "{{optional: entity ID}}",
  "data": {
    "reviewAnswer"{{field}}": "GREEN",
    {{value}}"rejectLabels": [],
    "reviewRejectType": null
  },
  "createdAt": "2026-02-23T12:00:00Z"
}

Envelope fields:

reasons(if
Field Type ValuesRequired Description
specversionstringYesAlways "1.0"
type string applicantReviewedYes Event type (see naming convention)
reviewAnswersource string GREEN, RED, RETRYYes VerificationEmitting outcomeservice name
externalUserIdid string Drop user IDYes LinksUnique toevent DropID (ULID format)
timestringYesISO 8601 timestamp (UTC)
datacontenttypestringYesAlways users.id"application/json"
rejectLabelssubject arraystring e.g.,No Primary entity ID (for routing)
["DOCUMENT_UNREADABLE"]data Rejectionobject Yes Domain-specific RED/RETRY)event payload

DropTypeScript action on receipt:interface:

switchinterface (reviewAnswer)CloudEvent<T = Record<string, unknown>> {
  casespecversion: 'GREEN'1.0';
  type: string;
  source: string;
  id: string;
  time: string;
  datacontenttype: 'application/json';
  subject?: //string;
  Updatedata: users SET kyc_status = 'approved' WHERE id = externalUserId
    break;
  case 'RED':
    // Update users SET kyc_status = 'rejected' WHERE id = externalUserId
    break;
  case 'RETRY':
    // Update users SET kyc_status = 'pending' WHERE id = externalUserId
    // User must resubmit documents
    break;T;
}

5. Per-Event Documentation


5.1 User Service Events

Event: applicantPendinguser.user.created

SentPublished when applicanta documentsnew areuser submittedaccount andis awaitingcreated.

review.
PropertyValue
Publisheruser-service
Consumersnotification-service, analytics-service, audit-service
Topicuser.user.created
Ordering guaranteePer user ID (partitioned by subject)
Idempotency keyid (event ID) — consumers must deduplicate
Retry behaviorConsumer retries up to 5x before DLQ

Payload schema (JSON Schema):

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["userId", "email", "name", "role", "createdAt"],
  "properties": {
    "userId": { "type": "string", "format": "uuid" },
    "email": { "type": "string", "format": "email" },
    "name": { "type": "string", "minLength": 1 },
    "role": { "type": "string", "enum": ["admin", "user", "viewer"] },
    "createdAt": { "type": "string", "format": "date-time" }
  }
}

Example event:

{
  "specversion": "1.0",
  "type": "user.user.created",
  "source": "user-service",
  "id": "evt_01HX7M2K5N3P4Q5R6S7T8V9W0",
  "time": "2024-01-15T10:30:00.000Z",
  "datacontenttype": "application/json",
  "subject": "usr_01HX7...",
  "data": {
    "userId": "usr_01HX7...",
    "email": "[email protected]",
    "name": "Jane Doe",
    "role": "user",
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
}

user.user.updated

Published when user profile data changes.

PropertyValue
Publisheruser-service
Consumerssearch-service, notification-service, analytics-service
Topicuser.user.updated
Ordering guaranteePer user ID
Idempotency keyid (event ID)

Payload schema:

{
  "type": "applicantPending"object",
  "applicantId"required": ["userId", "updatedFields", "updatedAt"],
  "properties": {
    "userId": { "type": "..."string" },
    "externalUserId"updatedFields": {
      "type": "usr_..."array",
      "createdAt"items": { "type": "2026-02-23T12:00:00Z"string" },
      "description": "List of field names that changed"
    },
    "before": { "type": "object", "description": "Previous values (only changed fields)" },
    "after": { "type": "object", "description": "New values (only changed fields)" },
    "updatedAt": { "type": "string", "format": "date-time" }
  }
}

DropExample action:event: Update users SET kyc_status = 'pending'.


2.2 Open Banking Provider — Payment Webhooks (Planned)

When an Open Banking provider is selected, the following webhook events will be received for PISP payment status updates.

Endpoint: POST /api/payments/webhook (future)

Event: payment.settled

{
  "event"specversion": "payment.settled"1.0",
  "paymentId"type": "provider_payment_id"user.user.updated",
  "dropTransactionId"source": "tx_rem_abc123"user-service",
  "status"id": "completed"evt_01HX8...",
  "settledAt"time": "2026-02-23T14:30:00Z"2024-01-16T08:00:00.000Z",
  "amount": 2000,
  "currency"datacontenttype": "NOK"application/json",
  "subject": "usr_01HX7...",
  "data": {
    "userId": "usr_01HX7...",
    "updatedFields": ["name"],
    "before": { "name": "Jane Doe" },
    "after": { "name": "Jane Smith" },
    "updatedAt": "2024-01-16T08:00:00.000Z"
  }
}

Drop action: UPDATE transactions SET status = 'completed', completed_at = NOW() WHERE id = dropTransactionId


Event: payment.faileduser.user.deleted

{

Published "event":when "payment.failed",a "paymentId": "provider_payment_id", "dropTransactionId": "tx_rem_abc123", "status": "failed", "failureReason": "InsufficientFunds", "failedAt": "2026-02-23T14:30:00Z" }

Drop action: UPDATE transactions SET status = 'failed' WHERE id = dropTransactionId, refund bankuser account balance.


2.3 BankID — No Webhook Events

BankID OIDC is a synchronous request-response flow. No webhooks are received from BankID. The callback URL (GET /api/auth/bankid/callback) handles the code exchange synchronously.soft-deleted.


3. Internal Audit Log Events

Drop maintains an audit_log table for all significant application events. This functions as an internal event log for compliance (AML, GDPR) and security investigations.

3.1 Audit Log Schema

CREATE TABLE audit_log (
  id TEXT PRIMARY KEY,
  action TEXT NOT NULL,          -- Event type (see below)
  user_id TEXT,                  -- Acting user (null for system events)
  resource_type TEXT,            -- Entity type (user, transaction, secret, etc.)
  resource_id TEXT,              -- Entity ID
  details TEXT,                  -- JSON blob with event-specific data
  ip_address TEXT,               -- Client IP at time of action
  timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);

3.2 Audit Event Types

ActionProperty TriggerResource TypeDetailsValue
user_registeredNew user created via BankIDPublisher useruser-service
Consumersorder-service, notification-service, analytics-service
Payload { kyc_method,userId, auth_providerdeletedAt, reason }
kyc_approvedOrdering guarantee KYCPer statususer → approveduser{ sumsub_applicant_id, review_answer }
kyc_rejectedKYC status → rejecteduser{ reject_labels, review_answer }
transaction_createdRemittance or QR payment initiatedtransaction{ type, amount, fee, currency }
transaction_completedPayment confirmed by providertransaction{ settled_at, provider_ref }
transaction_failedPayment rejectedtransaction{ failure_reason }
session_createdUser logged insession{ auth_method }
sessions_revokedUser logged outsession{ revoked_count }
account_deletedGDPR erasure requestuser{ deleted_at, retention_note }
data_exportedGDPR data exportuser{ export_timestamp }
consent_grantedGDPR consent givenconsent{ consent_type, ip_address }
consent_withdrawnGDPR consent revokedconsent{ consent_type, ip_address }
complaint_submittedUser complaint filedcomplaint{ category, subject }
aml_alert_createdAML monitoring flagged activityaml_alert{ alert_type, severity, transaction_id }
str_filedSTR filed with Finanstilsynetstr_report{ filed_at, case_number }
secret_rotatedSecret key rotatedsecret{ provider, key_name, rotated_at }
merchant_registeredNew merchant accountmerchant{ org_number, business_name }ID

3.3

Example Audit Log Entry

event:

{
  "id"specversion": "aud_1a2b3c4d5e6f7890"1.0",
  "action": "transaction_created",
  "user_id": "usr_abc123",
  "resource_type": "transaction",
  "resource_id": "tx_rem_xyz789",
  "details": {
  "type": "remittance"user.user.deleted",
  "amount": 2000,
    "fee": 10,
    "currency"source": "NOK"user-service",
  "recipient_country"id": "RS"evt_01HX9...",
  "exchange_rate"time": 11.7
  }"2024-01-17T12:00:00.000Z",
  "ip_address"datacontenttype": "85.20.12.45"application/json",
  "timestamp"subject": "2026-02-23T14:30:usr_01HX7...",
  "data": {
    "userId": "usr_01HX7...",
    "deletedAt": "2024-01-17T12:00:00.000Z",
    "reason": "user_requested"
  }
}

3.45.2 QueryingOrder AuditService LogsEvents

-- Recent user activity
SELECT * FROM audit_log
WHERE user_id = 'usr_abc123'
ORDER BY timestamp DESC
LIMIT 50;

-- Security: all secret rotations
SELECT * FROM audit_log
WHERE action = 'secret_rotated'
ORDER BY timestamp DESC;

-- AML: suspicious transactions
SELECT al.* FROM audit_log al
JOIN transactions t ON al.resource_id = t.id
WHERE al.action = 'transaction_created'
  AND t.send_amount > 50000  -- High-value transactions
  AND al.timestamp > NOW() - INTERVAL '24 hours'
ORDER BY al.timestamp DESC;

Retention: Audit logs retained for 5 years per hvitvaskingsloven (Norwegian AML law).


4. Planned Event-Driven Architecture (Future)

When Drop scales beyond MVP, an event bus will decouple synchronous operations:

4.1 Proposed Event Bus

Technology options:

  • AWS SQS + SNS (native AWS, fits existing infrastructure)
  • BullMQ + Redis (simpler, Node.js native)

Recommendation: SQS for production reliability.

4.2 Planned Event Topics

order.order.created

order
TopicProperty PublisherSubscribersPurposeValue
drop.transaction.initiatedPublisher Payment order-servicePISP provider, Audit, NotificationTrigger payment + notify user
drop.transaction.settledConsumers Openpayment-service, Bankingnotification-service, webhook handlerAudit, Notification, AMLUpdate transaction statusinventory-service
drop.kyc.status_changedTopic Sumsub webhook handlerAudit, User serviceUpdate user KYC statusorder.order.created
drop.user.registeredOrdering guarantee AuthPer service KYC, NotificationTrigger KYC flow
drop.aml.alertAML monitoringCompliance, NotificationFlag suspicious activityID

4.3

Payload Planned Event Envelope

schema:

{
  "eventId"type": "evt_unique_id"object",
  "eventType"required": ["drop.transaction.initiated"orderId", "version": "1.0"userId", "timestamp": "2026-02-23T14:30:00Z"items", "source": "drop-payment-service"total", "correlationId": "req_abc123"currency", "data"createdAt"],
  "properties": {
    "transactionId"orderId": { "type": "tx_rem_xyz"string" },
    "userId": { "usr_abc"type": "string" },
    "amount"items": 2000,{
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "productId": { "type": "string" },
          "quantity": { "type": "integer" },
          "unitPrice": { "type": "number" }
        }
      }
    },
    "total": { "type": "number" },
    "currency": { "NOK"type": "string", "pattern": "^[A-Z]{3}$" },
    "createdAt": { "type": "string", "format": "date-time" }
  }
}

5.3 Slack{{DOMAIN}} AlertService Events

(Operational)

Slack operational alerts from src/lib/alerts.ts function as a simple event notification system:

{{domain}}.{{entity}}.{{action}}

SeverityProperty EventTriggerValue
infoPublisher App startupApplication boots{{service-name}}
infoConsumers App{{consumer-a, shutdownGraceful shutdownconsumer-b}}
criticalTopic Error spike> 5 errors in 60 seconds{{domain.entity.action}}
criticalOrdering guarantee Unhandled`{{Per exceptionentity ID
Idempotency key Process error handler{{id}}

ThesePayload areschema: fire-and-forgetTODO: HTTPDefine POSTJSON callsSchema

Example noevent: acknowledgementTODO: orAdd retry logic.example


Related6. DocumentsDead Letter Queue Handling

DLQ naming: {{topic}}.dlq — e.g., user.user.created.dlq

DLQ workflow:

Event Published
    ↓
Consumer processes
    ↓ Fails
Retry (exp. backoff: 1s, 2s, 4s, 8s, 16s — max 5 retries)
    ↓ All retries exhausted
Move to DLQ
    ↓
Alert fires: PagerDuty P3
    ↓
On-call investigates
    ↓
Option A: Fix consumer bug → replay from DLQ
Option B: Skip message (data was invalid) → log + discard

DLQ message format:

{
  "originalEvent": { /* original CloudEvent */ },
  "failureReason": "Consumer threw: Cannot read property 'id' of undefined",
  "attemptCount": 5,
  "firstFailedAt": "2024-01-15T10:30:00Z",
  "lastFailedAt": "2024-01-15T10:32:00Z",
  "consumerGroup": "notification-service-consumer"
}

DLQ retention: 14 days. DLQ alert threshold: > 10 messages in DLQ within 5 minutes.


7. Event Versioning Strategy

Strategy: Backward-compatible field addition + major version in event type.

Rules:

  1. Adding optional fields: allowed without version bump
  2. Removing fields: NOT allowed (use deprecation first, remove after all consumers updated)
  3. Changing field types: NOT allowed (breaking change)
  4. Adding required fields: requires version bump
  5. Major breaking change: new event type user.user.created.v2

Deprecation process:

1. Mark field as deprecated in schema docs
2. Notify all consumer teams
3. Wait 2 sprint cycles (4 weeks minimum)
4. Remove field from schema
5. Update documentation

Schema registry: {{Confluent Schema Registry | AWS Glue Schema Registry | Manual docs}} Validation: Consumer validates incoming events against pinned schema version.


8. Event Replay Capability

Replay supported: {{Yes — Kafka log retention | No — events are ephemeral}}

Replay scenarios:

Replay procedure:

  1. Identify topic and time range to replay
  2. Coordinate with all consumer teams (replay may cause duplicate side effects)
  3. Ensure consumers are idempotent before replay
  4. Set consumer offset to target timestamp: kafka-consumer-groups --reset-offsets --to-datetime
  5. Restart consumer with temporary consumer group to avoid affecting production offset
  6. Verify replayed state is correct
  7. Switch production consumer to new state

Retention periods by topic: See topic configuration table in Section 2.


9. Monitoring & Observability

MetricAlert ThresholdSeverityChannel
Consumer lag (per topic)> 10,000 messagesP2Slack #alerts
DLQ depth> 10 messages / 5minP3Slack #alerts
Producer error rate> 1% / 5minP1PagerDuty
Consumer error rate> 5% / 5minP2PagerDuty
Event processing latency P99> 5 secondsP3Slack #alerts

Dashboard: {{https://monitoring.domain.com/dashboards/events}} Distributed tracing: All events carry traceparent header (OpenTelemetry W3C Trace Context).


10. Testing Event-Driven Flows

Unit Tests

// Test producer: verify event shape
it('should publish user.created event with correct schema', async () => {
  await userService.create(createUserDto);

  expect(eventBus.publish).toHaveBeenCalledWith(
    expect.objectContaining({
      type: 'user.user.created',
      source: 'user-service',
      data: expect.objectContaining({
        userId: expect.any(String),
        email: createUserDto.email,
      }),
    })
  );
});

// Test consumer: verify handler idempotency
it('should not send welcome email twice for duplicate event', async () => {
  const event = buildUserCreatedEvent();
  await handler.handle(event);
  await handler.handle(event); // duplicate
  expect(emailService.send).toHaveBeenCalledTimes(1);
});

Integration Tests

// Use real broker in integration tests (testcontainers)
const kafka = await new KafkaContainer('confluentinc/cp-kafka:7.5.0').start();

E2E Tests

Test full event chain: API action → event published → consumer processes → side effect visible.

POST /users → poll for welcome email (SendGrid sandbox) → assert received within 5s

Approval

Bašić
Role Name Date Signature
Author Platform Architect (AI) 2026-02-23
ReviewerBackend Lead
ApproverPlatform / Infrastructure Lead Alem
Architect