9.5 KiB
Contacts
User-managed address book — vCard-compatible contacts, groups, import/export, and flexible meta jsonb.
Architecture Overview
┌──────────────────────────────────────┐
│ Frontend │
│ ContactsManager.tsx │
│ (MUI DataGrid, batch bar, dialogs) │
│ │
│ client-contacts.ts │
│ (fetch wrappers, bearer token) │
└──────────────┬───────────────────────┘
│ /api/contacts/*
▼
┌──────────────────────────────────────┐
│ Server – ContactsProduct │
│ products/contacts/index.ts │
│ products/contacts/routes.ts │
└──────────────┬───────────────────────┘
│ Supabase
▼
┌──────────────────────────────────────┐
│ Tables │
│ contacts │
│ contact_groups │
│ contact_group_members │
└──────────────────────────────────────┘
Database
contacts
| Column | Type | Notes |
|---|---|---|
id |
uuid | PK, auto-generated |
owner_id |
uuid | FK → auth.users, not null |
name |
text | Full display name |
first_name |
text | — |
last_name |
text | — |
emails |
jsonb | Array of { email, label?, primary? } objects |
phone |
text | Primary phone |
organization |
text | Company / org name |
title |
text | Job title |
address |
jsonb | Array of { street, city, state, postal_code, country, label? } |
source |
text | Origin of contact (cscart, import, manual, …) |
language |
text | Preferred language tag (en, de, …) |
status |
text | active / unsubscribed / bounced / blocked |
notes |
text | Free-form notes |
tags |
text[] | Searchable tags |
log |
jsonb | Audit / event log array [{ at, event, data }] |
meta |
jsonb | Arbitrary extra fields (vCard extensions, etc.) |
created_at |
timestamptz | — |
updated_at |
timestamptz | Auto-updated via trigger |
Indexes: owner_id, status, source, language, tags (GIN), emails (GIN)
contact_groups
| Column | Type | Notes |
|---|---|---|
id |
uuid | PK |
owner_id |
uuid | FK → auth.users |
name |
text | not null |
description |
text | — |
meta |
jsonb | e.g. color, icon |
created_at |
timestamptz | — |
updated_at |
timestamptz | — |
contact_group_members
| Column | Type | Notes |
|---|---|---|
group_id |
uuid | FK → contact_groups |
contact_id |
uuid | FK → contacts |
added_at |
timestamptz | — |
| PK | composite | (group_id, contact_id) |
RLS
- Owners: full CRUD on their own rows (
owner_id = auth.uid()) - Admins (
user_roles.role = 'admin'): full access to all rows
Server API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/contacts |
Auth | List contacts. Query: ?group=<id>&q=<search>&status=<status>&limit=&offset= |
POST |
/api/contacts |
Auth | Create contact |
GET |
/api/contacts/:id |
Auth | Get single contact |
PATCH |
/api/contacts/:id |
Auth | Update contact (partial) |
DELETE |
/api/contacts/:id |
Auth | Delete contact |
POST |
/api/contacts/import |
Auth | Bulk import JSON array or vCard text (?format=json|vcard) |
GET |
/api/contacts/export |
Auth | Export all contacts. Query: ?format=json|vcard&group=<id> |
GET |
/api/contact-groups |
Auth | List groups |
POST |
/api/contact-groups |
Auth | Create group |
PATCH |
/api/contact-groups/:id |
Auth | Update group |
DELETE |
/api/contact-groups/:id |
Auth | Delete group |
GET |
/api/contact-groups/members |
Auth | List all group memberships for the user's contacts → { contact_id, group_id }[] |
POST |
/api/contact-groups/:id/members |
Auth | Add contacts { contact_ids: string[] } → { added: number } |
DELETE |
/api/contact-groups/:id/members/:contactId |
Auth | Remove contact from group |
Route priority: static sub-paths (
/import,/export,/members) are registered before parameterised:idroutes to avoid conflicts.
Import / Export Format
JSON (default)
[
{
"email": "jane@example.com",
"name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+1 555 0100",
"organization": "Acme",
"title": "Engineer",
"address": { "city": "Berlin", "country": "DE" },
"tags": ["customer", "newsletter"],
"meta": { "source": "cscart" }
}
]
vCard (format=vcard)
Standard vCard 3.0 — one BEGIN:VCARD … END:VCARD block per contact.
Fields mapped: FN, N, EMAIL, TEL, ORG, TITLE, ADR, NOTE, CATEGORIES.
Extended fields stored in meta as X-PM-* (X-PM-LANGUAGE, X-PM-SOURCE, X-PM-STATUS).
Frontend Client
src/modules/contacts/client-contacts.ts — all functions inject the Supabase bearer token automatically via authHeaders(). Requests are routed through a shared apiFetch helper that resolves VITE_SERVER_IMAGE_API_URL.
// Contacts CRUD
fetchContacts(options?) → Contact[] // options: { group?, q?, status?, limit?, offset? }
getContact(id) → Contact
createContact(data) → Contact
updateContact(id, data) → Contact
deleteContact(id) → void
// Import / Export
importContacts(body, format?) → { imported: number; skipped: number }
exportContacts(options?) → string | Contact[] // options: { format?, group? }
// Groups CRUD
fetchContactGroups() → ContactGroup[]
createContactGroup(data) → ContactGroup
updateContactGroup(id, data) → ContactGroup
deleteContactGroup(id) → void
fetchGroupMembers() → { contact_id: string; group_id: string }[]
addGroupMembers(groupId, contactIds) → { added: number }
removeGroupMember(groupId, contactId) → void
Key Types
interface ContactEmail { email: string; label?: string; primary?: boolean }
interface ContactAddress { street?; city?; state?; postal_code?; country?; label? }
interface Contact { id; owner_id; name?; first_name?; last_name?; emails: ContactEmail[];
phone?; address: ContactAddress[]; source?; language?;
status?: 'active'|'unsubscribed'|'bounced'|'blocked';
organization?; title?; notes?; tags?; log?; meta?;
created_at?; updated_at? }
interface ContactGroup { id; owner_id; name; description?; meta?; created_at?; updated_at? }
Frontend UI — ContactsManager
Full-featured management interface built with MUI DataGrid inside a shadcn/ui shell.
Features
| Feature | Detail |
|---|---|
| DataGrid | Sortable, filterable columns (name, email, status, groups, tags, actions). Checkbox selection. |
| URL state sync | Filter, sort, column visibility and pagination models are persisted in URL search params via gridUtils. |
| Toolbar filters | Search (q), group dropdown, status dropdown — all reflected in URL and sent to the server. |
| Contact dialog | Create / edit form with email chips, tag chips, group toggles, and status select. |
| Batch bar | When rows are selected: set group, remove from all groups, set status, or delete. Uses addGroupMembers, removeGroupMember, updateContact, deleteContact. |
| Import | File picker accepts .json / .vcf, auto-detects format. |
| Export | Dropdown for JSON or vCard, respects active group filter. Downloads as file. |
| Group management | Dialog to create / delete groups. Inline in the toolbar. |
URL Parameters
| Param | Source |
|---|---|
q |
Search input |
group |
Group filter dropdown |
status |
Status filter dropdown |
filter_* |
DataGrid column filters (via gridUtils) |
sort |
DataGrid sort model |
hidden |
DataGrid column visibility |
page / pageSize |
DataGrid pagination (defaults: 0 / 50) |
Environment Variables
Inherits same Supabase env as the rest of the server — no additional variables required.
Source Files
| File | Description |
|---|---|
| contacts.md | This document |
| migration | DB schema, RLS, indexes |
| routes.ts | Zod-OpenAPI route definitions |
| index.ts | ContactsProduct handlers |
| client-contacts.ts | Frontend fetch wrappers |
| ContactsManager.tsx | Main UI component (DataGrid, batch ops, dialogs) |