Subscriptions & Consent — Technical Reference
This page covers the data models, eligibility logic, and consent architecture behind the Subscriptions & Consent feature.
Data Models
Contact Consent Fields
Consent is tracked directly on the contacts table:
| Field | Type | Description |
|---|---|---|
is_subscribed | boolean | Whether the contact has opted in to marketing emails |
opt_in_timestamp | timestamptz | When consent was given |
opt_in_source | text | Source of consent (form ID, "CSV Import", "Manual Consent", "eventbrite_auto_subscribe") |
opt_in_ip | inet | IP address at time of consent |
unsubscribed_at | timestamptz | When the contact unsubscribed (NULL if subscribed) |
contact_suppressions
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | FK → workspaces.id — tenant isolation (ON DELETE CASCADE) |
contact_id | uuid | FK → contacts.id (ON DELETE CASCADE) |
reason | text | Suppression type: bounce, complaint, unsubscribe, manual (reserved) |
Note: The
manualreason is defined in the schema CHECK constraint but is not currently wired up in the UI. No server action or UI control exists to create a manual suppression. It is included as a reserved value for future use. |created_at|timestamptz| When the suppression was created |
Unique constraint: One active suppression per reason per contact.
Architecture
Consent Proof Recording
When a contact subscribes, the system records a complete audit trail:
| Subscribe Source | opt_in_source Value | opt_in_ip Value |
|---|---|---|
| Form submission | Form ID or custom opt_in_source field | Submitter's IP |
| CSV Import | "CSV Import" | Uploader's IP |
| Manual re-subscribe | "Admin Manual Entry: [legal basis]" | Admin's IP |
| Eventbrite auto-subscribe | "eventbrite_auto_subscribe" | null (webhook IP is Eventbrite's server) |
Manual Subscribe Flow
The manual consent flow is a guarded action for subscribing any non-subscribed contact (whether they've never been subscribed or previously unsubscribed). It requires:
- User selects a legal basis from a dropdown:
"Verbal Consent"— spoken permission (e.g., call or meeting)"Written Consent"— written permission (e.g., signed form or email)"Existing Relationship"— pre-existing business relationship
- User checks an attestation checkbox confirming legal consent
- System captures the admin's IP address
- On confirm, the system:
- Sets
is_subscribed = true - Records
opt_in_timestamp,opt_in_source,opt_in_ip - Clears
unsubscribed_at - Deletes all non-complaint suppressions (
bounce,unsubscribe)
- Sets
The simple unsubscribe action (toggling off) does not require the modal — only subscribing a non-subscribed contact triggers this flow.
Email Eligibility Logic
A contact is eligible for a marketing email when all conditions are met:
1. is_subscribed = true
2. unsubscribed_at IS NULL
3. No active contact_suppression record existsIf any condition fails, the email is skipped and logged.
Transactional emails bypass conditions 1 and 2. Only active suppressions (bounce, complaint) prevent a transactional email from being sent.
Suppression Lifecycle
Creation:
bounce— Created automatically via Resend webhook when delivery failscomplaint— Created automatically via Resend webhook when recipient marks as spamunsubscribe— Created when the contact clicks the unsubscribe link
Clearance:
bounce,unsubscribe— Can be cleared from the contact detail page, or automatically cleared when the contact subscribes via form or Eventbritecomplaint— Cannot be cleared. This is enforced at the application level. The Eventbrite auto-subscribe flow also respects this: contacts with a complaint suppression are never auto-subscribed.
Email change behavior (updateContact()):
When a contact's email address is updated, bounce suppressions are automatically deleted
(inbox reputation resets with a new address). Complaint and unsubscribe suppressions are
explicitly preserved — the code comment reads: "Preserve complaint, unsubscribe, and manual
suppressions (human preferences)."
Unsubscribe Link Architecture
Marketing emails include an unsubscribe link using the workspace's tracking subdomain:
https://links.yourdomain.com/unsubscribe/[contactId]For workspaces using the fallback sender, the link uses FALLBACK_TRACKING_DOMAIN or
the platform URL. When clicked:
is_subscribed→falseunsubscribed_at→ current timestamp- New
contact_suppressionrecord created with reasonunsubscribe
Transactional emails omit the unsubscribe link entirely.
Eventbrite Auto-Subscribe
When enabled in Settings → Integrations → Eventbrite:
- Real-time webhook registrations trigger
handleEventbriteRegistration() - The contact is created or matched
- If the contact has a
complaintsuppression → skip auto-subscribe - Otherwise, set
is_subscribed = truewith consent proof - Clear non-complaint suppressions (
bounce,unsubscribe)
Auto-subscribe only applies to real-time webhooks, not historical imports or manual re-syncs.
Notification Integration
When a bounce or complaint suppression is created, the system dispatches a notification
to the Notification Center. Each suppressed contact generates its own notification entry:
- Bounce: Includes a "View Contact" action link
- Complaint: Includes a "View Contact" action link, with the permanent suppression warning
See Notifications for the full notification schema.
Security
RLS Policies
| Table | Operation | Policy |
|---|---|---|
contacts (consent fields) | UPDATE | Workspace members can update consent fields |
contact_suppressions | SELECT | Workspace members can view suppressions in their workspace |
contact_suppressions | INSERT | Workspace members can create suppressions in their workspace |
contact_suppressions | DELETE | Workspace members can clear suppressions (application enforces complaint permanence) |