EmailEvent

EmailEvent is an append-only event log for the outbound email lifecycle. One row is written per observed state transition โ€” both internal (MailService called the provider) and provider-side (delivery and engagement webhooks from SendGrid). Multiple rows per logical send are correlated via message_id, a UUID generated at send time and echoed back by the provider in every webhook. The table is provider-agnostic: no column names SendGrid directly, so future provider swaps require no schema change. The entity is associated to a Workspace via a nullable SET NULL foreign key so bounce/suppression history outlives workspace deletion.

NamingValue
ObjectEmailEvent
Resource type (JSON:API type)email_event
Collection / records rootโ€” (not a records root)
REST base/v1/email-events
Entity classMigration20260507094935_email_events, Migration20260507120000_email_events_workspace_set_null, Migration20260507121000_email_events_drop_created_at

Internal object. Not currently exposed on the public REST API. The operations below describe the intended contract.

API operations

OperationMethod & pathStatus
ListGET /v1/email-events๐ŸŸก Planned
RetrieveGET /v1/email-events/{id}๐ŸŸก Planned
CreatePOST /v1/email-events๐ŸŸก Planned
UpdatePATCH /v1/email-events/{id}๐ŸŸก Planned
DeleteDELETE /v1/email-events/{id}๐ŸŸก Planned

Data model

Attributes

FieldTypeRequiredConstraintsAllowed valuesDescription
email_event_iduuid (๐Ÿ”’ system)โœ… Yesuniqueโ€”Public UUID for this event row. Generated at insert by gen_random_uuid(); never set by callers.
message_iduuidโœ… Yesโ€”โ€”Logical send correlator. Assigned by MailService at send time and embedded in provider customArgs so every provider webhook event for the same logical send echoes the same UUID. Ties the internal 'sent' row to all downstream provider lifecycle rows.
email_typetextโšช Noโ€”welcome, sync_complete, weekly_digest, document_forward, invitation, member_welcomeEmail category from MailService. Stored as TEXT (no DB CHECK) so adding a new template requires only a new entry in EMAIL_TYPES, no migration. NULL for provider webhook events that arrive before the corresponding 'sent' row is correlated.
recipient_emailtextโœ… Yeslength <= 254 (CHECK email_events_recipient_length)โ€”Email address of the recipient. Denormalised on every row so suppression queries (bounce list, re-invite guard) keep working even when workspace is NULL after workspace deletion.
statustextโœ… YesCHECK email_events_status_values: one of the 13 allowed valuessent, failed, processed, dropped, deferred, delivered, bounced, opened โ€ฆ +5 moreLifecycle status of the email at the moment this event row was written. Two categories: internal (sent, failed โ€” written by MailService) and provider (all others โ€” written from provider webhooks). A DB CHECK constraint enforces the vocabulary to prevent silent typo inserts.
occurred_attimestamptzโœ… Yesโ€”โ€”When the event happened. Provider timestamp for webhook events; wall clock for internal events. Paired with received_at to compute webhook delivery latency.
received_attimestamptz (๐Ÿ”’ system)โœ… Yesdefault now()โ€”When this row was written to the database (wall clock, set by onCreate hook). Paired with occurred_at; the difference is webhook delivery latency. Defaults to now() at insert.
providertextโšช Noโ€”sendgridActive email provider for this event. NULL for internal-only events that never reached a provider (status=failed before provider call). The only current value is 'sendgrid'; the vocabulary lives in EMAIL_PROVIDERS.
provider_message_idtextโšช Noโ€”โ€”Provider's per-message identifier. Useful for support tickets and cross-referencing against the provider's own dashboards. NULL for internal events.
provider_event_idtextโšช Nopartial unique index idx_email_events_provider_event_id_unique on (provider, provider_event_id) WHERE provider_event_id IS NOT NULLโ€”Provider's per-event idempotency key. A partial unique index on (provider, provider_event_id) WHERE provider_event_id IS NOT NULL prevents duplicate webhook inserts. Internal events have this as NULL and are exempt from the constraint.

Relationships

NameTypeRequiredDescription
workspaceto-one (Workspace)NoThe workspace that owns this email event. Nullable: SET NULL on workspace deletion so bounce/spam/unsubscribe history outlives the workspace and prevents re-emailing a previously hard-bounced recipient after re-invitation. Rows with workspace=NULL are naturally hidden from workspace-scoped Hasura RLS queries.

System-computed

  • email_event_id โ€” gen_random_uuid() default at insert, never caller-supplied
  • received_at โ€” set by MikroORM onCreate hook to new Date(); represents the DB write timestamp
  • workspace FK on delete SET NULL โ€” workspace deletion orphans rows rather than cascading deletes, preserving suppression history
  • Partial unique index on (provider, provider_event_id) WHERE provider_event_id IS NOT NULL โ€” idempotency guard for provider webhook retries; index maintained by Postgres, not application code
  • No created_at / updated_at / deleted_at โ€” this entity is append-only (no soft-delete, no update semantics); created_at was dropped in Migration20260507121000 as redundant with received_at

Example

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "type": "email_event",
    "attributes": {
      "email_event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "email_type": "invitation",
      "recipient_email": "alice@example.com",
      "status": "delivered",
      "occurred_at": "2026-05-07T14:32:10.000Z",
      "received_at": "2026-05-07T14:32:11.243Z",
      "provider": "sendgrid",
      "provider_message_id": "sg-msg-00aabbcc",
      "provider_event_id": "sg-evt-112233445566"
    },
    "relationships": {
      "workspace": {
        "data": { "id": "9f3e4a71-1234-5678-abcd-000000000001", "type": "workspace" }
      }
    }
  }
}
Source: apps/api/src/database/entities/EmailEvent.ts โ€” migrations: apps/api/src/database/migrations/Migration20260507094935_email_events.ts, Migration20260507120000_email_events_workspace_set_null.ts, Migration20260507121000_email_events_drop_created_at.ts ยท domain: platform ยท tier: Activity