Files
member-console/design/integration/model.md
Christian Galo 195cd348a9 Update design to use schemas per module and revert to using industry
aligned terminology for discounts and coupons and promo codes.
2026-04-01 03:08:35 -05:00

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_events is 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 payload column: 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_outbox is a standalone operational table. aggregate_type and target_provider are text fields, not FK references. aggregate_id references 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 payload column: PII categorically prohibited.