analytics

Tags, custom fields, and activity log for cross-cutting entity data

Analytics

Tags, custom fields, and an activity log. A cross-cutting data layer that can attach labels, user-defined fields, and audit history to any entity type.

Tables

tags

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations(id), cascade delete
nametextTag label, unique per org
colortextOptional display color
created_attimestamptzRow creation timestamp
updated_attimestamptzAuto-updated on modification
metadatajsonbArbitrary key-value data

taggings

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations(id), cascade delete
tag_iduuidReferences tags(id), cascade delete
entity_typetextTarget entity kind (contact, company, deal, task, ticket, document, project, event)
entity_iduuidID of the tagged entity
created_attimestamptzRow creation timestamp
updated_attimestamptzAuto-updated on modification
metadatajsonbArbitrary key-value data

custom_field_definitions

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations(id), cascade delete
entity_typetextWhich entity type this field applies to (contact, company, deal, task, ticket, document, project)
nametextField label, unique per org and entity type
field_typefield_typeData type for this field, defaults to text
descriptiontextOptional explanation of the field
is_requiredbooleanWhether a value is mandatory, defaults to false
optionsjsonbAllowed values for select and multi_select fields
positionintegerDisplay order, defaults to 0
created_attimestamptzRow creation timestamp
updated_attimestamptzAuto-updated on modification
metadatajsonbArbitrary key-value data

custom_field_values

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations(id), cascade delete
field_iduuidReferences custom_field_definitions(id), cascade delete
entity_typetextTarget entity kind (contact, company, deal, task, ticket, document, project)
entity_iduuidID of the entity this value belongs to
value_texttextText value column
value_numbernumericNumeric value column
value_booleanbooleanBoolean value column
value_datedateDate value column
value_jsonjsonbJSON value column (for select, multi_select, etc.)
created_attimestamptzRow creation timestamp
updated_attimestamptzAuto-updated on modification
metadatajsonbArbitrary key-value data

activities

ColumnTypeDescription
iduuidPrimary key, auto-generated
org_iduuidReferences organizations(id), cascade delete
actor_iduuidReferences users(id), set null on delete. NULL for system-generated activities
entity_typetextTarget entity kind (contact, company, deal, task, ticket, document, project, subscription, invoice, event)
entity_iduuidID of the entity this activity relates to
actionactivity_actionWhat happened
descriptiontextHuman-readable summary
changesjsonbJSON diff, e.g. {"status": {"from": "open", "to": "closed"}}
occurred_attimestamptzWhen the action took place. Can differ from created_at for imported data
created_attimestamptzRow creation timestamp
updated_attimestamptzAuto-updated on modification
metadatajsonbArbitrary key-value data

Enums

field_type

ValueDescription
textFree-form text
numberNumeric value
booleanTrue or false
dateCalendar date
selectSingle choice from a list of options
multi_selectMultiple choices from a list of options
urlURL string
emailEmail address string

activity_action

ValueDescription
createdEntity was created
updatedEntity was updated
deletedEntity was deleted
status_changedStatus field changed
assignedEntity was assigned to a user
commentedA comment was added
viewedEntity was viewed
email_sentAn email was sent
email_receivedAn email was received
note_addedA note was attached
call_loggedA phone call was recorded
stage_changedPipeline stage changed
deal_wonDeal marked as won
deal_lostDeal marked as lost
task_completedTask marked as done
payment_receivedPayment was received

Row-Level Security

All five tables are scoped to the current user's organization via get_user_org_id(). Select, insert, and update are allowed for any org member. Delete requires the is_admin() check.

Dependencies

  • identity -- organizations and users tables, get_user_org_id() and is_admin() functions, set_updated_at() trigger function

Example Queries

Activity feed for a contact:

SELECT
  a.action,
  a.description,
  a.entity_type,
  u.full_name AS actor,
  a.occurred_at
FROM activities a
LEFT JOIN users u ON u.id = a.actor_id
WHERE a.entity_type = 'contact' AND a.entity_id = '<contact_id>'
ORDER BY a.occurred_at DESC
LIMIT 50;

Get all tags for an entity:

SELECT t.name, t.color
FROM tags t
JOIN taggings tg ON tg.tag_id = t.id
WHERE tg.entity_type = 'deal' AND tg.entity_id = '<deal_id>';

Get custom field values for a contact:

SELECT
  cfd.name AS field_name,
  cfd.field_type,
  coalesce(
    cfv.value_text,
    cfv.value_number::text,
    cfv.value_boolean::text,
    cfv.value_date::text,
    cfv.value_json::text
  ) AS value
FROM custom_field_definitions cfd
LEFT JOIN custom_field_values cfv
  ON cfv.field_id = cfd.id
  AND cfv.entity_type = 'contact'
  AND cfv.entity_id = '<contact_id>'
WHERE cfd.entity_type = 'contact'
ORDER BY cfd.position;

Cross-entity activity feed for a deal and its related contact and company:

WITH deal_context AS (
  SELECT id AS entity_id, 'deal' AS entity_type FROM deals WHERE id = '<deal_id>'
  UNION ALL
  SELECT id, 'contact' FROM contacts WHERE id = (SELECT contact_id FROM deals WHERE id = '<deal_id>')
  UNION ALL
  SELECT id, 'company' FROM companies WHERE id = (SELECT company_id FROM deals WHERE id = '<deal_id>')
)
SELECT
  a.action,
  a.description,
  a.entity_type,
  u.full_name AS actor,
  a.changes,
  a.occurred_at
FROM activities a
JOIN deal_context dc ON a.entity_type = dc.entity_type AND a.entity_id = dc.entity_id
LEFT JOIN users u ON u.id = a.actor_id
ORDER BY a.occurred_at DESC
LIMIT 100;

Find entities with a specific tag:

SELECT tg.entity_type, tg.entity_id, tg.created_at
FROM taggings tg
JOIN tags t ON t.id = tg.tag_id
WHERE t.name = 'VIP'
ORDER BY tg.created_at DESC;

Count activities by action type in the last 30 days:

SELECT
  action,
  count(*) AS total
FROM activities
WHERE occurred_at >= now() - interval '30 days'
GROUP BY action
ORDER BY total DESC;