Skip to main content

Event Schema Documentation

Event Schema Documentation

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

Document History

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

1. Event-Driven Architecture Overview

Drop's

graphcurrent LRarchitecture subgraphis "Publishers"a UserService["user-service"]synchronous OrderService["order-service"]monolith PaymentService["payment-service"] endthere subgraphis "Messageno Broker"internal Broker["{{Kafkaevent /bus RabbitMQor /message AWSqueue SQS(no +BullMQ, SNS}}"]SQS, endor subgraphRabbitMQ). "Consumers"All NotifService["notification-service"]operations AnalyticsService["analytics-service"]are SearchService["search-service"]handled AuditService["audit-service"]synchronously endwithin UserServiceNext.js -->|user.*API events|route Broker
    OrderService -->|order.* events| Broker
    PaymentService -->|payment.* events| Broker

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

Event-drivenThis usedocument cases in this system:covers:

    1. DecoupledExternal notificationswebhook events received from Sumsub (user.createdKYC status sendupdates) welcome email)the only production external event source
    2. SearchInternal indexaudit updateslog (entity.updatedevents reindex)database-level event tracking for compliance
    3. AuditPlanned trailevent (allschema mutations for auditfuture log)async processing when an event bus is added
    4. Cross-service data sync (order.created → update inventory)

2. MessageExternal BrokerWebhook ConfigurationEvents

2.1 Sumsub — KYC Status Webhook

Broker:Source: {{Apachesrc/lib/services/mock-sumsub.ts, Kafkaproduction |Sumsub RabbitMQdocs: |https://docs.sumsub.com/reference/applicant-review

AWS

Sumsub SQS/SNSsends |webhook NATSevents |when Upstasha Kafka}}KYC applicant's review status changes.

Version:Endpoint: {{3.x}}POST /api/kyc/webhook Hosting:(TBD {{Confluent Cloudpending /Open self-hostedBanking /provider AWS MSK}}integration)

Topic / Queue Naming Convention

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

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

PatternWebhook rules:signature verification:

    HMAC-SHA256
  • All lowercase, dot-separated
  • Domain prefix = service name (withoutusing -serviceSUMSUB_SECRET_KEY)
  • .
  • EntityReject =any 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

invalid
ComponentRuleExamples
Full event type{domain}.{entity}.{action}user.user.created
DomainLowercase, matches service prefixuser, order, payment
EntitySingular noun, lowercasewebhook with underscores user,signature.

Event: order_itemapplicantReviewed, invoice

ActionPast-tense verb, lowercase with underscorescreated, updated, status_changed, payment_failed

DoSent NOTwhen use:Sumsub completes review of a KYC applicant.

  • Present tense (user.user.create — wrong)
  • Generic names (user.user.changed — too vague)
  • Abbreviations (usr.usr.crtd — unreadable)

4. Event Envelope Format (CloudEvents 1.0)

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

Envelope fields:

e.g., Rejection
Field Type RequiredValues Description
specversiontype string YesapplicantReviewed AlwaysEvent type
reviewAnswerstringGREEN, "1.0"RED, RETRYVerification outcome
externalUserIdstringDrop user IDLinks to Drop users.id
typerejectLabels stringarray Yes Event type (see naming convention)
source["DOCUMENT_UNREADABLE"] string YesEmitting service name
idstringYesUnique event IDreasons (ULIDif format)
timestringYesISO 8601 timestamp (UTC)
datacontenttypestringYesAlways "application/json"
subjectstringNoPrimary entity ID (for routing)
dataobjectYesDomain-specific event payloadRED/RETRY)

TypeScriptDrop interface:action on receipt:

interfaceswitch CloudEvent<T(reviewAnswer) {
  case 'GREEN':
    // Update users SET kyc_status = Record<string,'approved' unknown>>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;
}

Event: applicantPending

Sent when applicant documents are submitted and awaiting review.

{
  "type": "applicantPending",
  "applicantId": "...",
  "externalUserId": "usr_...",
  "createdAt": "2026-02-23T12:00:00Z"
}

Drop action: 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": "payment.settled",
  "paymentId": "provider_payment_id",
  "dropTransactionId": "tx_rem_abc123",
  "status": "completed",
  "settledAt": "2026-02-23T14:30:00Z",
  "amount": 2000,
  "currency": "NOK"
}

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

Event: payment.failed

{
  "event": "payment.failed",
  "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 bank 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.


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

ActionTriggerResource TypeDetails
user_registeredNew user created via BankIDuser{ kyc_method, auth_provider }
kyc_approvedKYC status → 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 }

3.3 Example Audit Log Entry

{
  "id": "aud_1a2b3c4d5e6f7890",
  "action": "transaction_created",
  "user_id": "usr_abc123",
  "resource_type": "transaction",
  "resource_id": "tx_rem_xyz789",
  "details": {
    specversion:"type": "remittance",
    "amount": 2000,
    "fee": 10,
    "currency": "NOK",
    "recipient_country": "RS",
    "exchange_rate": 11.7
  },
  "ip_address": "85.20.12.45",
  "timestamp": "2026-02-23T14:30:00.000Z"
}

3.4 Querying Audit Logs

-- Recent user activity
SELECT * FROM audit_log
WHERE user_id = '1.0';usr_abc123'
type:ORDER string;BY source:timestamp string;DESC
id:LIMIT string;50;

time:-- string;Security: datacontenttype:all secret rotations
SELECT * FROM audit_log
WHERE action = 'application/json';secret_rotated'
subject?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

TopicPublisherSubscribersPurpose
drop.transaction.initiatedPayment servicePISP provider, Audit, NotificationTrigger payment + notify user
drop.transaction.settledOpen Banking webhook handlerAudit, Notification, AMLUpdate transaction status
drop.kyc.status_changedSumsub webhook handlerAudit, User serviceUpdate user KYC status
drop.user.registeredAuth serviceKYC, NotificationTrigger KYC flow
drop.aml.alertAML monitoringCompliance, NotificationFlag suspicious activity

4.3 Planned Event Envelope

{
  "eventId": string;"evt_unique_id",
  data:"eventType": T;"drop.transaction.initiated",
  "version": "1.0",
  "timestamp": "2026-02-23T14:30:00Z",
  "source": "drop-payment-service",
  "correlationId": "req_abc123",
  "data": {
    "transactionId": "tx_rem_xyz",
    "userId": "usr_abc",
    "amount": 2000,
    "currency": "NOK"
  }
}

5. Per-EventSlack DocumentationAlert Events (Operational)

Slack


operational

5.1alerts Userfrom Servicesrc/lib/alerts.ts Events

function

user.user.created

Published whenas a newsimple userevent accountnotification is created.system:

audit-service
PropertySeverity ValueEventTrigger
Publisherinfo user-serviceApp startupApplication boots
Consumersinfo notification-service,App analytics-service,shutdown Graceful shutdown
Topiccritical user.user.createdError spike> 5 errors in 60 seconds
Ordering guaranteecritical PerUnhandled user ID (partitioned by subject)
Idempotency keyexception idProcess (eventerror ID) — consumers must deduplicate
Retry behaviorConsumer retries up to 5x before DLQhandler

PayloadThese schemaare (JSONfire-and-forget Schema):

HTTP
{POST "$schema":calls "http://json-schema.org/draft-07/schema#", "type":no "object",acknowledgement "required":or ["userId",retry "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": "object",
  "required": ["userId", "updatedFields", "updatedAt"],
  "properties": {
    "userId": { "type": "string" },
    "updatedFields": {
      "type": "array",
      "items": { "type": "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" }
  }
}

Example event:

{
  "specversion": "1.0",
  "type": "user.user.updated",
  "source": "user-service",
  "id": "evt_01HX8...",
  "time": "2024-01-16T08:00:00.000Z",
  "datacontenttype": "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"
  }
}

user.user.deleted

Published when a user account is soft-deleted.

PropertyValue
Publisheruser-service
Consumersorder-service, notification-service, analytics-service
Payload{ userId, deletedAt, reason }
Ordering guaranteePer user ID

Example event:

{
  "specversion": "1.0",
  "type": "user.user.deleted",
  "source": "user-service",
  "id": "evt_01HX9...",
  "time": "2024-01-17T12:00:00.000Z",
  "datacontenttype": "application/json",
  "subject": "usr_01HX7...",
  "data": {
    "userId": "usr_01HX7...",
    "deletedAt": "2024-01-17T12:00:00.000Z",
    "reason": "user_requested"
  }
}

5.2 Order Service Events

order.order.created

PropertyValue
Publisherorder-service
Consumerspayment-service, notification-service, inventory-service
Topicorder.order.created
Ordering guaranteePer order ID

Payload schema:

{
  "type": "object",
  "required": ["orderId", "userId", "items", "total", "currency", "createdAt"],
  "properties": {
    "orderId": { "type": "string" },
    "userId": { "type": "string" },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "productId": { "type": "string" },
          "quantity": { "type": "integer" },
          "unitPrice": { "type": "number" }
        }
      }
    },
    "total": { "type": "number" },
    "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
    "createdAt": { "type": "string", "format": "date-time" }
  }
}

5.3 {{DOMAIN}} Service Events

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

PropertyValue
Publisher{{service-name}}
Consumers{{consumer-a, consumer-b}}
Topic{{domain.entity.action}}
Ordering guarantee`{{Per entity ID
Idempotency key{{id}}

Payload schema: TODO: Define JSON Schema

Example event: TODO: Add examplelogic.


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 replayDesign
  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

Alem
Role Name Date Signature
Author Platform Architect (AI) 2026-02-23
Backend LeadReviewer
Platform / Infrastructure LeadApprover
ArchitectBašić