Headlessly
Concepts

Identifiers

Entity IDs, type discriminators, tenant context, and versioning — the meta-fields that power the graph.

Entity ID Format

Every entity receives an auto-generated ID in the format {type}_{sqid}:

import { Contact } from '@headlessly/crm'

const contact = await Contact.create({ name: 'Alice', stage: 'Lead' })
console.log(contact.$id) // 'contact_fX9bL5nRd'

The type prefix is always lowercase. The suffix is generated by sqids -- a library that produces short, unique, URL-safe identifiers.

Examples Across Entities

EntityExample ID
Contactcontact_fX9bL5nRd
Dealdeal_k7TmPvQx
Organizationorganization_pQ8xNfKm
Subscriptionsubscription_nT5xKpRm
Issueissue_e5JhLzXc
Eventevt_tR8kJmNxP
ApiKeyapikey_nT5xKpRmVw

Why sqids

RequirementHow sqids Solves It
Short8-12 characters vs. 36 for UUIDs
URL-safeAlphanumeric only, no special characters
Non-offensiveBuilt-in blocklist prevents generating IDs that contain profanity
Non-sequentialIDs do not reveal creation order or total count
UniqueCollision-free within a tenant namespace

Why Not UUIDs

UUIDs (550e8400-e29b-41d4-a716-446655440000) are 36 characters, contain hyphens, and are impossible to read, type, or remember. They work for machine-to-machine communication but are hostile to developers and agents who interact with IDs directly.

Why Not Sequential IDs

Sequential integers (contact_1, contact_2) leak information: total entity count, creation order, and growth rate. They are also trivially enumerable, which creates security concerns in multi-tenant systems.

The Seven Meta-Fields

Every entity carries seven system-managed fields prefixed with $:

$type

The PascalCase entity type discriminator. Used for routing, serialization, and type-safe parsing:

const contact = await Contact.get('contact_fX9bL5nRd')
console.log(contact.$type) // 'Contact'

const deal = await Deal.get('deal_k7TmPvQx')
console.log(deal.$type) // 'Deal'

The $type field enables polymorphic queries across the graph. When events reference a target, the ID prefix and $type together identify both the entity type and specific instance.

$id

The unique identifier in {type}_{sqid} format. Globally unique within a tenant, parseable to extract the entity type:

// Parse the type from an ID
const id = 'contact_fX9bL5nRd'
const type = id.split('_')[0] // 'contact'

IDs are assigned at creation time and never change. They are the stable reference for an entity across its entire lifecycle.

$context

The tenant namespace URL. Every entity is scoped to exactly one tenant:

const contact = await Contact.get('contact_fX9bL5nRd')
console.log(contact.$context) // 'https://headless.ly/~acme'

The $context ensures complete tenant isolation. Queries, events, and relationships are always scoped to the tenant. See Multi-Tenancy for details.

$version

A monotonically increasing integer that increments on every mutation:

const contact = await Contact.create({ name: 'Alice', stage: 'Lead' })
console.log(contact.$version) // 1

await Contact.update(contact.$id, { stage: 'Qualified' })
const updated = await Contact.get(contact.$id)
console.log(updated.$version) // 2

Versions provide a total ordering of mutations per entity. They are used for optimistic concurrency control, event ordering, and time travel. The version never decreases, even after a rollback (the rollback itself increments the version).

$createdAt

ISO 8601 timestamp of when the entity was created:

console.log(contact.$createdAt) // '2026-01-15T12:00:00Z'

Set once at creation time. Never changes.

$updatedAt

ISO 8601 timestamp of the most recent mutation:

console.log(contact.$updatedAt) // '2026-01-16T09:30:00Z'

Updated on every write operation, including custom verbs.

$createdBy

The ID of the user or agent that created the entity:

console.log(contact.$createdBy) // 'agent_mR4nVkTw'

This field enables audit trails. Combined with the event log, you can trace every entity back to its creator and every subsequent mutation to its actor.

Meta-Fields Are Read-Only

You cannot set meta-fields directly. They are managed by the system:

// This will NOT set the ID -- the system generates it
await Contact.create({
  $id: 'contact_myCustomId',  // Ignored
  name: 'Alice',
  stage: 'Lead',
})

// This will NOT change the version -- mutations increment it automatically
await Contact.update('contact_fX9bL5nRd', {
  $version: 99,  // Ignored
  stage: 'Qualified',
})

Using IDs in Relationships

Entity IDs are the glue that connects the 35-entity graph. Pass an ID string to set a relationship:

import { Deal } from '@headlessly/crm'

await Deal.create({
  name: 'Enterprise License',
  value: 50000,
  contact: 'contact_fX9bL5nRd',
  organization: 'organization_k7TmPvQx',
})

The system validates that the referenced entity exists and belongs to the same tenant. Cross-tenant references are not possible.

ID in URLs

Entity IDs appear in REST URLs and MCP tool calls:

# REST
GET https://crm.headless.ly/~acme/Contact/contact_fX9bL5nRd

# Verb execution
POST https://crm.headless.ly/~acme/Contact/contact_fX9bL5nRd/qualify
headless.ly/mcp#fetch
{ "type": "Contact", "id": "contact_fX9bL5nRd" }

Because IDs are short and URL-safe, they work cleanly in paths, query parameters, and log output without encoding.

On this page