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_vectortsvector 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
deleteContactServer 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
activitiestable, and notes section - Deals -- data table of deals where
deals.contact_idmatches this contact - Activities -- activity feed filtered by
activities.contact_id
- Overview -- contact information card, recent activity timeline from
- 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_tagsjunction table - Lead Source -- select dropdown (Website, Referral, LinkedIn, Cold Call, Event, Other)
- Owner -- team member select from
profilestable - 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
.csvfiles - 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
createContactServer 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_vectortsvector 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
contactswherecompany_idmatches - Deals -- data table querying
dealswherecompany_idmatches - 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
- Deals & Pipeline -- Kanban pipeline board and deal management
- Tasks & Email -- task management, calendar, and email
- Server Actions -- complete reference for all mutations