Tags — Technical Reference
This page covers the data models, uniqueness constraints, and security policies behind the Tags feature.
Data Models
tags
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | FK → workspaces.id — tenant isolation (ON DELETE CASCADE) |
name | text | User-visible tag label |
color | text | Hex color code for visual identification |
created_by | uuid | FK → user_profiles.id — who created the tag |
created_at | timestamptz | When the tag was created |
Unique constraint: UNIQUE(workspace_id, LOWER(name)) — enforces case-insensitive
uniqueness per workspace.
contact_tags (Join Table)
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | FK → workspaces.id — tenant isolation (ON DELETE CASCADE) |
contact_id | uuid | FK → contacts.id (ON DELETE CASCADE) |
tag_id | uuid | FK → tags.id (ON DELETE CASCADE) |
created_at | timestamptz | When the tag was applied to the contact |
Unique constraint: UNIQUE(contact_id, tag_id) — prevents duplicate tag assignments.
Architecture
Case-Insensitive Deduplication
Tag name uniqueness is enforced at the database level with a functional unique index on
LOWER(name) scoped to workspace_id. This means:
- Creating a tag named
VIPwhenvipalready exists will match the existing tag - During CSV import,
seedTagsFromImport()usesILIKEmatching to find existing tags - The first-seen casing is preserved; subsequent imports with different casing reuse the existing record without renaming it
Automation Cascade Deletion
When a tag is deleted, all automation rules that reference it are cascade-deleted from the automations table. The tag deletion flow:
- Retrieve the count of rules in
automationswheretrigger_idoraction_idequals the tag ID (for the frontend warning dialog). - On confirmation, delete all matching rules from the
automationstable. - Delete the tag record from the
tagstable (cascading foreign keys oncontact_tagshandle removal from contacts).
Automation Dependency Tracking
The Tags list page queries automation rules to count how many reference each tag, powering the ⚡ N badge and the dependency drawer. The check is performed via the shared getComponentDependencies server action, which filters rules based on the trigger_id and action_id columns in the automations table.
Broadcast Audience Targeting
Broadcasts use tags for audience selection. The calculateAudienceCount() function filters
contacts by:
- Include tags — contacts must have at least one of the included tags
- Exclude tags — contacts must not have any of the excluded tags
- Subscription eligibility — contacts must be subscribed with no active suppressions
CSV Import Tag Handling
During CSV import (seedTagsFromImport()):
- Parse
tag1,tag2,tag3columns from each row - For each unique tag name, check for existing tags using
ILIKEmatching - Create new tags for any that don't exist, using the first-seen casing
- Apply the global tag (if set) to all contacts in the batch
- Insert
contact_tagsrecords, skipping duplicates withON CONFLICT DO NOTHING
Tag Color Picker UI Accessibility
The color picker UI in CreateTagDialog, EditTagDialog, and TagSelectDialog provides enhanced accessibility:
- Selected State Contrast: Shifts styling to bold, dark text and applies a dynamic border matching the active tag color.
- Background Color Tint: Adds a matching 7% opacity background tint behind the selected active option.
- Accessibility Checkmark: Renders a solid white checkmark inside the selected color circle to ensure clarity without relying solely on color hue.
Security
RLS Policies
| Operation | Policy |
|---|---|
| SELECT | Workspace members can view tags in their workspace |
| INSERT | Workspace members can create tags in their workspace |
| UPDATE | Workspace members can edit tags in their workspace |
| DELETE | Workspace members can delete tags in their workspace |
The same policies apply to the contact_tags join table.
Performance & Hydration Optimization
Server/Client Page Split & Lazy Loading
- Server Component (
pages/tags/page.tsx): Pre-fetches active workspace, tags list, and automation dependency counts on the server. - Client Component (
tags-client.tsx): Manages the tag grid rendering, contact counts, delete triggers, and local search. - Dynamic imports: Lazily loads
CreateTagDialogandEditTagDialogwithssr: falseto defer chunk loading until user interaction. - Sync Gates: Integrates
isFirstQueryRefmount guards andinitialWorkspaceIdRefstate sync gates.
Performance & Optimistic UI Updates
Optimistic Tag Additions
To provide an instant response when managing tags on a contact, the CRM implements optimistic UI updates:
- Instant Dialog Dismissal: The tag selection dialog (
TagSelectDialog) closes immediately when a user selects a tag, removing async blocking states. - Callbacks: Uses
onAddTagOptimisticandonRollbackTagcallbacks to update local UI states instantly while execution runs in the background. - Error Rollback: If the background API save fails, the system rolls back the client-side state to the previous snapshot and displays an error toast notification.
- Badge Order Preservation: Tag badge derivations are sorted alphabetically using
localeCompareto prevent badges from dynamically shifting or "jumping" when transitioning from the optimistic client state to the synced database state.