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 (Blocked Email Delivery)

Note: The manual reason is created when an administrator manually blocks email delivery for a contact. It acts as a hard suppression gate across all outbound communications, RSVPs, and form submissions. | 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)
Stripe auto-subscribe"stripe_auto_subscribe"null (webhook IP is Stripe's Connect account 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.

Tiered Suppression Engine

The CRM enforces a two-tiered suppression engine to evaluate contact email eligibility. Suppressions are categorized as either Hard or Soft:

  • Hard Suppressions (bounce, complaint, manual): These block all outbound communications (both Marketing and Transactional emails, event RSVPs, and form registrations) to protect domain reputation and enforce manual bans.
  • Soft Suppressions (unsubscribe, or any case where is_subscribed = false or unsubscribed_at IS NOT NULL): These block marketing emails only. Transactional communications bypass soft suppressions, ensuring recipients receive critical alerts, purchase receipts, or notifications.

Email Eligibility Logic

A contact's eligibility for an outbound email is evaluated by category:

Marketing Emails

A contact is eligible for a marketing email when all of the following conditions are met:

  1. is_subscribed = true
  2. unsubscribed_at is NULL
  3. No active contact_suppressions record of any type (bounce, complaint, unsubscribe, manual) exists.

If any check fails, the email is skipped and logged.

Transactional Emails

A contact is eligible for a transactional email when:

  1. No Hard Suppression exists (no contact_suppressions record with reason bounce, complaint, or manual).

Soft suppressions (unsubscribe, manual) and general consent flags (is_subscribed = false or unsubscribed_at IS NOT NULL) are bypassed for transactional emails. However, if a contact is transactional-eligible but has marketing-blocking conditions, the system may inject a transactional subscribe footer (see below).

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 a form, Eventbrite, Stripe purchase, or a public subscribe link.
  • complaintCannot be cleared. This is enforced at the application level. The Eventbrite and Stripe auto-subscribe flows also respect 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. The campaign and broadcast sweeper engines explicitly bypass injecting unsubscribe headers, footers, or resolving the {{unsubscribe_url}} merge tag when processing transactional categories.

Subscribe Link & API Architecture

Unsubscribed contacts can opt back in to marketing communications via public subscribe links.

The Subscribe API Endpoint

The public endpoint GET /api/subscribe/[contactId] processes subscribe requests:

  • Hard Suppression Guard ("Old Link Trap"): To prevent abuse and protect deliverability, contacts with active hard suppressions (bounce or complaint) are blocked from re-subscribing. The API rejects these requests and redirects the user to the /subscribe/error page with contextual error messages.
  • Successful Subscription: For eligible contacts, the API:
    • Sets is_subscribed = true.
    • Clears unsubscribed_at (sets to NULL).
    • Deletes all soft suppression records (unsubscribe, manual reasons) associated with the contact.
    • Records the opt-in IP address and timestamp.
    • Redirects the user to the /subscribe/confirmed page.
  • Split-Brain Resolution: If a contact is already marked is_subscribed = true but has stale unsubscribed_at values or residual soft suppression records, the API cleanses these fields to resolve the inconsistent state.

Subscribe Footer Injection

When sending a transactional email to a contact who is currently opted-out of marketing emails, the system provides a path to re-subscribe:

  • Renderer (appendSubscribeFooter()): If a recipient is eligible for a transactional email but blocked from marketing (meaning is_subscribed = false, unsubscribed_at IS NOT NULL, or a soft suppression exists), the system appends a subscribe footer.
  • Workspace Settings: The injection respects the workspace's subscribe_footer_enabled toggle and uses the custom subscribe_footer_message and subscribe_footer_link_text configured in settings.
  • Link Generation: The footer includes a link to https://[app-url]/api/subscribe/[contactId] using the GET /api/subscribe/[contactId] endpoint.
  • Integration: The footer injection is built into the core email dispatch utilities (sendEmail for campaigns and sendBatchEmails for broadcasts) in src/lib/resend/emails.ts.

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)

Middleware Public Routes

To allow unauthenticated recipients to opt out or opt in, the following routes bypass authentication checks in src/lib/supabase/middleware.ts:

  • /unsubscribe/[contactId]
  • /subscribe/[contactId]
  • /api/subscribe/[contactId]
  • /subscribe/confirmed
  • /subscribe/error