The model separates global contact identity from audience/client subscription state. This matters because the same email can belong to DataTalksClub and AI Shipping Labs with different permissions and preferences.
Represents the top-level owner.
Examples:
datatalksclubai-shipping-labs
Important fields:
idnameslugcreated_at
Represents a list or brand-level audience.
Examples:
datatalks-clubai-shipping-labs
Important fields:
idorganization_idnameslugcreated_at
Represents an application or product using Datamailer.
Examples:
dtc-newsletterdtc-coursesai-shipping-labs
Important fields:
idorganization_idnameslugis_activecreated_at
Represents named Bearer credentials for client integrations.
Important fields:
idclient_idnamekey_hashpublic_idnoteslast_used_atrevoked_atcreated_atupdated_at
Only hashes are stored. public_id is safe to display as dm_<public_id> in the operator UI, API docs, and audit metadata. revoked_at IS NULL means active, and active key names are unique per client.
Global email identity.
Important fields:
idemailnormalized_emailverified_atglobal_unsubscribed_athard_bounced_atcomplained_atcreated_atupdated_at
Constraints and indexes:
- Unique
normalized_email. - Index
verified_at. - Index
global_unsubscribed_at.
Subscription state scoped to an audience and optionally a client.
Important fields:
idcontact_idaudience_idclient_idstatus:pending,subscribed,unsubscribedverified_atunsubscribed_atunsubscribe_reasoncreated_atupdated_at
Constraints and indexes:
- Unique
(contact_id, audience_id, client_id). - Index
(audience_id, client_id, status). - Index
(contact_id, updated_at).
Reusable labels within an audience.
Important fields:
idaudience_idnameslug
Constraints and indexes:
- Unique
(audience_id, slug).
Many-to-many membership between contacts and tags.
Important fields:
contact_idtag_idcreated_at
Constraints and indexes:
- Unique
(contact_id, tag_id). - Index
(tag_id, contact_id).
Campaign definition and aggregate stats.
Important fields:
idclient_idaudience_idsubjectpreview_texthtml_bodytext_bodystatus:draft,queued,snapshotting,sending,sent,cancelled,failedscheduled_atsent_atinclude_tagsexclude_tagsrecipient_countsent_countskipped_countdelivered_countunique_open_countopen_countunique_click_countclick_countunsubscribe_countbounce_countcomplaint_countcreated_atupdated_at
Derived rates:
- Open rate:
unique_open_count / sent_count. - Click rate:
unique_click_count / sent_count. - Click-to-open rate:
unique_click_count / unique_open_count. - Bounce rate:
bounce_count / sent_count. - Unsubscribe rate:
unsubscribe_count / sent_count.
One row per intended campaign recipient.
For a 120k-recipient campaign, this table gets 120k rows. This is intentional. It provides auditability and supports send/not-send status.
Important fields:
idcampaign_idcontact_idemailstatus:pending,sent,skipped,failed,bounced,complained,unsubscribedskip_reason:unverified,global_unsubscribe,client_unsubscribe,audience_unsubscribe,hard_bounce,complaint,duplicate,suppressedtracking_token_hashunsubscribe_token_hashses_message_idsent_atdelivered_atfirst_opened_atfirst_clicked_atopen_countclick_countlast_errorcreated_atupdated_at
Constraints and indexes:
- Unique
(campaign_id, contact_id). - Unique
tracking_token_hash. - Unique
unsubscribe_token_hash. - Index
(campaign_id, status). - Index
(contact_id, sent_at). - Index
ses_message_id. - Index
first_opened_at. - Index
first_clicked_at.
Append-only event timeline.
Important fields:
idcampaign_idcampaign_recipient_idtransactional_message_idcontact_idclient_idaudience_idevent_type:queued,skipped,sent,delivered,open,click,unsubscribe,bounce,complaint,failedurlmetadatacreated_at
Indexes:
(contact_id, created_at).(campaign_id, event_type, created_at).(campaign_recipient_id, event_type).(client_id, created_at).
Growth plan:
- Keep this append-only.
- Partition monthly when volume grows enough to make maintenance/reporting painful.
- Archive old raw events to S3 if needed.
Reusable transactional and campaign templates.
Important fields:
idclient_idkeynamesubjecthtml_bodytext_bodyis_transactionalcreated_atupdated_at
One row per transactional send request.
Important fields:
idclient_idcontact_idemailtemplate_idtemplate_keystatus:queued,sent,failed,skipped,bounced,complainedidempotency_keyses_message_idsent_atdelivered_atfirst_opened_atfirst_clicked_atopen_countclick_countmetadatalast_errorcreated_atupdated_at
Constraints and indexes:
- Unique
(client_id, idempotency_key)when idempotency key is present. - Index
(contact_id, created_at). - Index
(client_id, status, created_at). - Index
ses_message_id.
Contact history is assembled from:
- Contact creation and verification fields.
- Subscription changes.
- Campaign recipient rows.
- Transactional message rows.
- Email events.
The UI should show this as a chronological timeline per contact.