Contacts & Companies

Last updated on 2026-09-02

The contacts and companies modules form the core of the CRM's data model. Unlike the frontend-only kit where all data comes from data/seed.ts, this full-stack kit reads from and writes to Supabase tables with full-text search, Server Action mutations, and automatic activity logging.

Contacts List

Route: /contacts

The primary contacts view with a feature-rich data table. All data is fetched server-side from Supabase.

  • Data table -- columns for avatar, full name (linked to detail), email, phone, company (linked to company detail), total deal value, last contacted date, tags (as colored badges), and a row actions dropdown
  • Full-text search -- uses the search_vector tsvector column on contacts for fast, typo-tolerant search across name, email, and phone
  • Filters -- dropdown filters for company and tag, querying Supabase with .eq() and tag joins
  • View toggle -- switch between table view and card grid view
  • Pagination -- server-side pagination with configurable page size (10, 25, 50)
  • Row actions -- View, Edit, and Delete; delete calls deleteContact Server Action with confirmation dialog

Search Implementation

// Full-text search query (simplified)
const { data } = await supabase
  .from("contacts")
  .select("*, companies(name, logo_url), contact_tags(tags(*))")
  .textSearch("search_vector", query, { type: "websearch" })
  .range(offset, offset + limit - 1)

Data Sources

Data Source Query
Contacts contacts table With company and tag joins
Companies (filter) companies table Select id, name
Tags (filter) tags table All tags

Contact Detail

Route: /contacts/[id]

A detailed view with hero section, tabbed content, and right sidebar. Data is fetched server-side with joins.

  • Hero section -- avatar, full name, title, company link, email (mailto), phone (tel), and social links
  • Tabbed content:
    • Overview -- contact information card, recent activity timeline from activities table, and notes section
    • Deals -- data table of deals where deals.contact_id matches this contact
    • Activities -- activity feed filtered by activities.contact_id
  • Right sidebar -- deal summary (aggregated from deals), assigned owner, tags, lead source, quick action buttons (Log Call, Send Email, Add Note -- each calls a Server Action)

Quick Actions

The quick action buttons create activities via the createActivity Server Action:

// Log a call
await createActivity({
  type: "call",
  description: "Called about proposal follow-up",
  contact_id: contactId,
})
// This also triggers the contact.last_contacted update

Contact Create / Edit

Route: /contacts/new and /contacts/[id]/edit

A Zod-validated form that calls the createContact or updateContact Server Action.

  • Personal Info -- first name, last name, email (validated), phone, job title
  • Company -- company select dropdown (searchable, queried from Supabase)
  • Address -- street, city, state, zip, country
  • Social -- LinkedIn URL, Twitter handle, website URL
  • Tags -- multi-select tag picker; writes to contact_tags junction table
  • Lead Source -- select dropdown (Website, Referral, LinkedIn, Cold Call, Event, Other)
  • Owner -- team member select from profiles table
  • Notes -- freeform textarea

Validation Schema

const contactSchema = z.object({
  firstName: z.string().min(1, "First name is required"),
  lastName: z.string().min(1, "Last name is required"),
  email: z.string().email("Invalid email address"),
  phone: z.string().optional(),
  jobTitle: z.string().optional(),
  companyId: z.string().optional(),
  address: addressSchema.optional(),
  social: socialSchema.optional(),
  tags: z.array(z.string()),
  leadSource: z.enum(["website", "referral", "linkedin", "cold_call", "event", "other"]),
  ownerId: z.string().min(1, "Owner is required"),
  notes: z.string().optional(),
})

Contact Import

Route: /contacts/import

A 3-step wizard for importing contacts from a CSV file. Each step validates before allowing progression.

Step 1: Upload CSV

  • Drag-and-drop file upload accepting .csv files
  • File preview with filename, size, and row count
  • Template download link

Step 2: Column Mapping

  • Side-by-side mapping interface pairing CSV columns to CRM fields
  • Auto-mapping for columns with matching header names
  • Required fields (firstName, lastName, email) must be mapped

Step 3: Preview & Confirm

  • Data preview table showing all rows as they will be imported
  • Duplicate detection based on email matches against existing contacts in Supabase
  • Import triggers the createContact Server Action for each valid row

Companies List

Route: /companies

A data table of all companies with data from Supabase.

  • Data table -- logo (or initials fallback), company name, industry badge, employee count, annual revenue, deal count (aggregated), location, and row actions
  • Full-text search -- uses the search_vector tsvector column on companies
  • Filter -- dropdown filter by industry
  • Sorting -- column header sorting via Supabase .order()
  • Row actions -- View, Edit, Delete (admin only)

Search Implementation

// Company full-text search
const { data } = await supabase
  .from("companies")
  .select("*, company_tags(tags(*))")
  .textSearch("search_vector", query, { type: "websearch" })

Company Detail

Route: /companies/[id]

Detailed company view mirroring the contact detail layout.

  • Hero section -- logo, name, industry badge, website, location, employee count, annual revenue
  • Tabbed content:
    • Overview -- description, key metrics (total deal value, active deals, contacts -- aggregated from Supabase), recent activity
    • Contacts -- data table querying contacts where company_id matches
    • Deals -- data table querying deals where company_id matches
    • Activities -- activity feed filtered by activities.company_id
  • Sidebar -- industry, founded year, size, website, social links, account owner, tags

Company Create / Edit

Route: /companies/new and /companies/[id]/edit

A Zod-validated form calling the createCompany or updateCompany Server Action.

  • Company Info -- name, industry select, website, phone, description
  • Location -- street, city, state, zip, country
  • Details -- employee count, annual revenue, founded year
  • Social -- LinkedIn company URL, Twitter handle
  • Account Owner -- team member select from profiles
  • Tags -- multi-select tag picker; writes to company_tags

Activity Auto-Logging

When activities are created with a contact_id, the database trigger automatically updates the contact's last_contacted timestamp. This keeps the contacts list showing accurate "last contacted" dates without any extra application code.

Next Steps