Technical Reference
Contacts

Contacts — Technical Reference

This page covers the data models, workspace isolation, and import architecture behind the Contacts feature.

Data Models

contacts

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidFK → workspaces.id — tenant isolation (ON DELETE CASCADE)
emailtextRequired. Contact's email address. Unique per workspace
first_nametextFirst name
last_nametextLast name
phonetextPhone number
sourcetextHow the contact was created: manual, form, import, eventbrite, stripe
address_line1textStreet address line 1
address_line2textStreet address line 2
citytextCity
statetextState or province
postal_codetextZIP or postal code
countrytextCountry
birth_monthintegerBirthday month (1–12). Must be paired with birth_day
birth_dayintegerBirthday day (1–31). Must be paired with birth_month
is_subscribedbooleanWhether the contact has opted in to marketing emails
opt_in_timestamptimestamptzWhen consent was given
opt_in_sourcetextWhere consent originated (form ID, "CSV Import", etc.)
opt_in_ipinetIP address at time of consent
unsubscribed_attimestamptzWhen the contact unsubscribed (NULL if subscribed)
assigned_user_iduuidFK → user_profiles.id — assigned workspace member
created_attimestamptzWhen the contact was created
updated_attimestamptzWhen the contact was last modified

Unique constraint: UNIQUE(workspace_id, email) — one contact per email per workspace.

Architecture

Workspace Isolation

Contacts are strictly isolated by workspace. Two workspaces can each have a contact with jane@example.com — they are completely independent records. This isolation is enforced at the database level using PostgreSQL Row-Level Security (RLS), meaning it cannot be bypassed by application bugs.

Contact Source Enum

The source field tracks how the contact entered the workspace:

ValueTrigger
manualCreated by a workspace member via the dashboard (default)
formCaptured via a Gordon CRM form submission
importUploaded via CSV import
eventbriteAuto-synced from an Eventbrite event registration
stripeAuto-created from a Stripe purchase

If no source is provided during creation, it defaults to manual.

CSV Import Pipeline

The CSV import runs as a single atomic database transaction. If any critical error occurs, everything rolls back — no partial data is committed.

Upsert strategy — keyed on workspace_id + email:

  1. Parse and validate each row (email required, birthday validation)
  2. For each row, check if a contact with that email already exists
  3. New contacts → INSERT with all provided fields
  4. Existing contacts → UPDATE only non-empty CSV columns (blank columns are skipped to prevent accidental data erasure)
  5. Process tags: create missing tags, insert contact_tags records with ON CONFLICT DO NOTHING
  6. Process notes: insert contact_notes records
  7. If "Mark as Subscribed" is enabled, record consent proof (preserving existing consent for already-subscribed contacts)

Birthday validation:

  • birth_month and birth_day must both be provided or both omitted
  • Valid ranges: month 1–12, day 1–31
  • Invalid values are skipped per-row with an error logged

Error handling:

  • Invalid rows are skipped, not failed
  • Error report includes row number and reason for each skip
  • All valid rows are processed regardless of individual row failures

Email Change Behavior

When a contact's email is updated via updateContact():

  • bounce suppressions are automatically deleted (inbox reputation resets with new address)
  • complaint and unsubscribe suppressions are explicitly preserved (human preferences)

See Subscriptions & Consent — Technical Reference for the full suppression lifecycle.

Related Entity Connections

A contact can be connected to many entities through foreign key relationships:

EntityTableFK ColumnCascade
Tagscontact_tagscontact_idON DELETE CASCADE
Notescontact_notescontact_idON DELETE CASCADE
Companiescompany_contactscontact_idON DELETE CASCADE
Dealsdealscontact_idON DELETE SET NULL
Taskstaskscontact_idON DELETE SET NULL
Appointmentsappointmentscontact_idON DELETE SET NULL
Campaign Enrollmentscampaign_enrollmentscontact_idON DELETE CASCADE
Event Registrationsevent_registrationscontact_idON DELETE CASCADE
Transactionstransactionscontact_idON DELETE SET NULL
Email Sendsemail_sendscontact_idON DELETE SET NULL
Suppressionscontact_suppressionscontact_idON DELETE CASCADE
Agreementsagreement_contactscontact_idON DELETE CASCADE

Security

RLS Policies

OperationPolicy
SELECTWorkspace members can view contacts in their workspace
INSERTWorkspace members can create contacts in their workspace
UPDATEWorkspace members can edit contacts in their workspace
DELETEWorkspace members can delete contacts in their workspace