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 through
the automation_rules table. The tag deletion flow:
- Query
automation_ruleswheretrigger_configoraction_configreferences the tag ID - Count matching rules for the confirmation warning
- On confirm, delete the tag —
ON DELETE CASCADEoncontact_tagsremoves all assignments - A separate cleanup step deletes orphaned automation rules that referenced the deleted tag
Automation Dependency Tracking
The Tags list page queries automation rules to count how many reference each tag. This powers
the ⚡ N badge and the dependency drawer. The query checks both trigger_config and
action_config JSONB fields for the tag ID.
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
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.