Technical Reference
Subscriptions & Consent

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:

FieldTypeDescription
is_subscribedbooleanWhether the contact has opted in to marketing emails
opt_in_timestamptimestamptzWhen consent was given
opt_in_sourcetextSource of consent (form ID, "CSV Import", "Manual Consent", "eventbrite_auto_subscribe")
opt_in_ipinetIP address at time of consent
unsubscribed_attimestamptzWhen the contact unsubscribed (NULL if subscribed)

contact_suppressions

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidFK → workspaces.id — tenant isolation (ON DELETE CASCADE)
contact_iduuidFK → contacts.id (ON DELETE CASCADE)
reasontextSuppression type: bounce, complaint, unsubscribe, manual (reserved)

Note: The manual reason 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 Sourceopt_in_source Valueopt_in_ip Value
Form submissionForm ID or custom opt_in_source fieldSubmitter'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:

  1. 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
  2. User checks an attestation checkbox confirming legal consent
  3. System captures the admin's IP address
  4. 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)

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 exists

If 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 fails
  • complaint — Created automatically via Resend webhook when recipient marks as spam
  • unsubscribe — 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 Eventbrite
  • complaintCannot 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:

  1. is_subscribedfalse
  2. unsubscribed_at → current timestamp
  3. New contact_suppression record created with reason unsubscribe

Transactional emails omit the unsubscribe link entirely.

Eventbrite Auto-Subscribe

When enabled in Settings → Integrations → Eventbrite:

  1. Real-time webhook registrations trigger handleEventbriteRegistration()
  2. The contact is created or matched
  3. If the contact has a complaint suppression → skip auto-subscribe
  4. Otherwise, set is_subscribed = true with consent proof
  5. 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

TableOperationPolicy
contacts (consent fields)UPDATEWorkspace members can update consent fields
contact_suppressionsSELECTWorkspace members can view suppressions in their workspace
contact_suppressionsINSERTWorkspace members can create suppressions in their workspace
contact_suppressionsDELETEWorkspace members can clear suppressions (application enforces complaint permanence)