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 (Blocked Email Delivery) |
Note: The
manualreason 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 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) |
| 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:
- 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.
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 whereis_subscribed = falseorunsubscribed_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:
is_subscribed = trueunsubscribed_atisNULL- No active
contact_suppressionsrecord 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:
- No Hard Suppression exists (no
contact_suppressionsrecord with reasonbounce,complaint, ormanual).
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 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 a form, Eventbrite, Stripe purchase, or a public subscribe link.complaint— Cannot 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:
is_subscribed→falseunsubscribed_at→ current timestamp- New
contact_suppressionrecord created with reasonunsubscribe
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 (
bounceorcomplaint) are blocked from re-subscribing. The API rejects these requests and redirects the user to the/subscribe/errorpage with contextual error messages. - Successful Subscription: For eligible contacts, the API:
- Sets
is_subscribed = true. - Clears
unsubscribed_at(sets toNULL). - Deletes all soft suppression records (
unsubscribe,manualreasons) associated with the contact. - Records the opt-in IP address and timestamp.
- Redirects the user to the
/subscribe/confirmedpage.
- Sets
- Split-Brain Resolution: If a contact is already marked
is_subscribed = truebut has staleunsubscribed_atvalues 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 (meaningis_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_enabledtoggle and uses the customsubscribe_footer_messageandsubscribe_footer_link_textconfigured in settings. - Link Generation: The footer includes a link to
https://[app-url]/api/subscribe/[contactId]using theGET /api/subscribe/[contactId]endpoint. - Integration: The footer injection is built into the core email dispatch utilities (
sendEmailfor campaigns andsendBatchEmailsfor broadcasts) insrc/lib/resend/emails.ts.
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) |
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