Customer records, shared contact profiles, notes, and CRM extension points.
The customer CRM is a tenant-scoped domain built on a shared contact-profile identity layer. This page documents the data model and the deliberate extension points for customer portal access, inbox communication, and workflows.
| Table | Ownership | Purpose |
|---|---|---|
public.contact_profiles | Global | Canonical contact identity. Optionally links to auth.users via user_id. Survives before signup and after account deletion. Owns name, avatar, and display name. |
public.contact_methods | Follows contact_profiles | Multi-channel reachability: email, phone, WhatsApp. Used by internal users, contacts linked to customers, and future portal users. |
public.customers | Tenant (organization_id) | Company or individual record. Owns business facts: KVK, BTW, billing, primary address, and status. |
public.customer_contacts | Tenant | Relationship between a customer and a contact_profile. Owns role, primary/billing flags, portal-contact state, and lifecycle. |
public.comment_threads | Tenant | One discussion thread per customer (subject_type = 'customer'). |
public.comments | Tenant | Rich-text timeline on that thread. content_text for search and content_doc for structured blocks and mentions. |
public.file_drives | Tenant | Includes one customers drive per organization. Customer files live here, not in a separate attachment table. |
public.file_nodes | Tenant | customer_id marks the system-managed root folder for that customer in the customers drive. |
The profiles table intentionally stays thin: it stores account-level preferences for signed-in users and does not own mutable contact methods.
When a contact linked to a customer later has their own portal account, ownership is defined by field group rather than locking the entire row:
contact_profiles and contact_methods: name, avatar, personal reachability, and preferred contact channel.customer_contacts: role, primary/billing flag, portal-contact state, lifecycle status, and relationship notes.customers: company facts, billing entity, and primary address. User edits to their personal address do not overwrite this.Historical documents such as invoices and deals should snapshot addresses and tax details at creation time rather than re-read customers on every render.
Permissions introduced with this slice:
organization.customers.vieworganization.customers.createorganization.customers.updateorganization.customers.deleteThey live in packages/identity/src/rbac/permissions.ts and are seeded through supabase/schemas/055_functions_rbac.sql. Customer tables enforce tenant access through public.has_org_permission(...); per-user rows on contact_profiles and contact_methods additionally allow access through the linked auth user.
The customer Notes tab is a multi-author comments thread. Staff with
organization.customers.update can add notes, edit their own, and delete any
note. Each row stores authorship, timestamps, and the same collaboration
document used on task comments (content_doc plus a content_text search
shadow). Mentions resolve @ users/teams and # records.
comments.content_doc stores a versioned block document. Mentions live
as typed reference inlines inside those blocks, not as a separate mentions
array. The Notes tab UI matches the task-detail comments section (author
avatar, chronological oldest-first list, inline edit, overflow delete).
Organization members and signed-in contacts linked through contact_profiles.user_id → customer_contacts can open a portal under /{orgSlug}/portal.
/{orgSlug}/portal/* is what enters the portal zone.invoice_payments record is collectible. Only safe payment status is exposed to the portal (never PaymentIntent client secrets in list/detail RPCs — secrets are retrieved through authorized server actions)./{orgSlug} as a customer-only user redirects to /{orgSlug}/portal.Invitations, automatic email/profile linking, quote acceptance, and broader portal mutations remain future work.
contact_methods.customers with workflow-specific columns.listCustomers service input without changing route structure.packages/crm/src/customers, packages/portal/src/customer-portal.packages/supabase-crm/src/customers/service.ts, packages/supabase-portal/src/customer-portal/service.ts.supabase/schemas/058_functions_customer_portal.sql.packages/core/src/services.ts, packages/core/src/request-services/customer-portal.ts.apps/saas/app/[locale]/(app)/[orgSlug]/customers/* (contacts under /customers/contacts and /customers/[customerId]/contacts; files at /customers/[customerId]/files and Files hub ?scope=customers).apps/saas/app/[locale]/(app)/[orgSlug]/portal/*.