Contacts — Technical Reference
This page covers the data models, workspace isolation, and import architecture behind the Contacts feature.
Data Models
contacts
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | FK → workspaces.id — tenant isolation (ON DELETE CASCADE) |
email | text | Required. Contact's email address. Unique per workspace |
first_name | text | First name |
last_name | text | Last name |
phone | text | Phone number |
source | text | How the contact was created: manual, form, import, eventbrite, stripe |
address_line1 | text | Street address line 1 |
address_line2 | text | Street address line 2 |
city | text | City |
state | text | State or province |
postal_code | text | ZIP or postal code |
country | text | Country |
birth_month | integer | Birthday month (1–12). Must be paired with birth_day |
birth_day | integer | Birthday day (1–31). Must be paired with birth_month |
is_subscribed | boolean | Whether the contact has opted in to marketing emails |
opt_in_timestamp | timestamptz | When consent was given |
opt_in_source | text | Where consent originated (form ID, "CSV Import", etc.) |
opt_in_ip | inet | IP address at time of consent |
unsubscribed_at | timestamptz | When the contact unsubscribed (NULL if subscribed) |
assigned_user_id | uuid | FK → user_profiles.id — assigned workspace member |
created_at | timestamptz | When the contact was created |
updated_at | timestamptz | When 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:
| Value | Trigger |
|---|---|
manual | Created by a workspace member via the dashboard (default) |
form | Captured via a Gordon CRM form submission |
import | Uploaded via CSV import |
eventbrite | Auto-synced from an Eventbrite event registration |
stripe | Auto-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:
- Parse and validate each row (email required, birthday validation)
- For each row, check if a contact with that email already exists
- New contacts → INSERT with all provided fields
- Existing contacts → UPDATE only non-empty CSV columns (blank columns are skipped to prevent accidental data erasure)
- Process tags: create missing tags, insert
contact_tagsrecords withON CONFLICT DO NOTHING - Process notes: insert
contact_notesrecords - If "Mark as Subscribed" is enabled, record consent proof (preserving existing consent for already-subscribed contacts)
Birthday validation:
birth_monthandbirth_daymust 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():
bouncesuppressions are automatically deleted (inbox reputation resets with new address)complaintandunsubscribesuppressions 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:
| Entity | Table | FK Column | Cascade |
|---|---|---|---|
| Tags | contact_tags | contact_id | ON DELETE CASCADE |
| Notes | contact_notes | contact_id | ON DELETE CASCADE |
| Companies | company_contacts | contact_id | ON DELETE CASCADE |
| Deals | deals | contact_id | ON DELETE SET NULL |
| Tasks | tasks | contact_id | ON DELETE SET NULL |
| Appointments | appointments | contact_id | ON DELETE SET NULL |
| Campaign Enrollments | campaign_enrollments | contact_id | ON DELETE CASCADE |
| Event Registrations | event_registrations | contact_id | ON DELETE CASCADE |
| Transactions | transactions | contact_id | ON DELETE SET NULL |
| Email Sends | email_sends | contact_id | ON DELETE SET NULL |
| Suppressions | contact_suppressions | contact_id | ON DELETE CASCADE |
| Agreements | agreement_contacts | contact_id | ON DELETE CASCADE |
Security
RLS Policies
| Operation | Policy |
|---|---|
| SELECT | Workspace members can view contacts in their workspace |
| INSERT | Workspace members can create contacts in their workspace |
| UPDATE | Workspace members can edit contacts in their workspace |
| DELETE | Workspace members can delete contacts in their workspace |