5.0 KiB
Integration -- Model Reference
Module: Integration (Cross-cutting)
Tables: 2
Schema: integration
These tables support integration with external services (payment processors, infrastructure provisioning, tax computation, notification delivery). They live in the integration schema (Decision 113) because they are shared across all providers -- unlike provider-specific mapping tables, which live in per-provider schemas designed at implementation time (Decision 86).
webhook_events
A webhook_event records an inbound event received from an external provider. This table provides idempotency (deduplication by provider event ID), auditability, and queryable webhook history for debugging and replay. Processing of webhook events is handled by the application layer (e.g., Temporal workflows).
The table is partitioned by monthly RANGE on received_at, matching the audit log partitioning pattern.
| Field | Type | Purpose |
|---|---|---|
event_id |
UUID | Composite PK with received_at. |
received_at |
TIMESTAMPTZ | When the event was received. Partition key. Composite PK with event_id. |
provider |
VARCHAR(50) | Which provider sent this event: stripe, polar, nextcloud, etc. |
provider_event_id |
VARCHAR(500) | The provider's unique identifier for this event. |
event_type |
VARCHAR(100) | Event type as classified by the provider (e.g., invoice.finalized, subscription.updated). |
payload |
JSONB | Full raw webhook payload. Must not contain PII per JSONB governance policy. |
status |
VARCHAR(20) | Processing state: received, processing, completed, failed, skipped. |
processing_started_at |
TIMESTAMPTZ | When processing began. |
completed_at |
TIMESTAMPTZ | When processing completed. |
error_message |
TEXT | Error detail if processing failed. |
retry_count |
INTEGER | Number of processing attempts. |
Constraints:
UNIQUE(provider, provider_event_id)-- enforces idempotency. Duplicate webhook deliveries are detected at insert time.
Status lifecycle: received -> processing -> completed | failed | skipped
Relationships:
webhook_eventsis a standalone operational table. Provider and event type are text fields, not FK references. No foreign key references to any other table exist in either direction.
Cross-cutting policy notes:
- JSONB governance policy applies to the
payloadcolumn: PII categorically prohibited. Provider webhook payloads containing PII must be scrubbed before storage.
integration_outbox
An integration_outbox entry represents a domain state change that must trigger an action in an external system (e.g., provisioning a Nextcloud instance when an entitlement activates, syncing a product to Stripe when it's created). The outbox INSERT is performed in the same transaction as the domain state change, guaranteeing that no integration trigger is lost even if the application crashes after commit.
A background process (e.g., a Temporal workflow) polls the outbox and executes the external action. This is the same transactional outbox pattern used by audit_outbox in the audit module for audit event delivery.
| Field | Type | Purpose |
|---|---|---|
outbox_id |
UUID | Primary key |
aggregate_type |
VARCHAR(50) | The type of core entity that changed (e.g., subscription, entitlement, invoice). |
aggregate_id |
UUID | ID of the core entity that changed. |
event_type |
VARCHAR(100) | What happened (e.g., entitlement.activated, invoice.finalized, product.created). |
target_provider |
VARCHAR(50) | Which provider should handle this event: stripe, nextcloud, notify, etc. |
payload |
JSONB | Event payload for the handler. Must not contain PII per JSONB governance policy. |
status |
VARCHAR(20) | pending, processing, completed, failed, dead_letter. |
attempts |
INTEGER | Number of delivery attempts. |
max_attempts |
INTEGER | Maximum delivery attempts before moving to dead letter. |
next_attempt_at |
TIMESTAMPTZ | When the next attempt should be made. Supports exponential backoff. |
last_error |
TEXT | Last delivery error, if any. |
created_at |
TIMESTAMPTZ | |
completed_at |
TIMESTAMPTZ | When successfully processed. |
Constraints:
- Index on
(status, next_attempt_at)for the polling query pattern:WHERE status IN ('pending', 'failed') AND next_attempt_at <= NOW() ORDER BY next_attempt_at LIMIT $batch_size.
Status lifecycle: pending -> processing -> completed | failed -> (retry) | dead_letter
Relationships:
integration_outboxis a standalone operational table.aggregate_typeandtarget_providerare text fields, not FK references.aggregate_idreferences a core entity by UUID but is not constrained by a foreign key -- the aggregate type is determined by the text discriminator, following the same bare polymorphic pattern documented in the audit module for entity references.
Cross-cutting policy notes:
- JSONB governance policy applies to the
payloadcolumn: PII categorically prohibited.