# Identifiers (/reference/concepts/identifiers)



Entity ID Format [#entity-id-format]

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

```typescript
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](https://sqids.org/) -- a library that produces short, unique, URL-safe identifiers.

Examples Across Entities [#examples-across-entities]

| Entity       | Example ID              |
| ------------ | ----------------------- |
| Contact      | `contact_fX9bL5nRd`     |
| Deal         | `deal_k7TmPvQx`         |
| Organization | `organization_pQ8xNfKm` |
| Subscription | `subscription_nT5xKpRm` |
| Issue        | `issue_e5JhLzXc`        |
| Event        | `evt_tR8kJmNxP`         |
| ApiKey       | `apikey_nT5xKpRmVw`     |

Why sqids [#why-sqids]

| Requirement        | How sqids Solves It                                               |
| ------------------ | ----------------------------------------------------------------- |
| **Short**          | 8-12 characters vs. 36 for UUIDs                                  |
| **URL-safe**       | Alphanumeric only, no special characters                          |
| **Non-offensive**  | Built-in blocklist prevents generating IDs that contain profanity |
| **Non-sequential** | IDs do not reveal creation order or total count                   |
| **Unique**         | Collision-free within a tenant namespace                          |

Why Not UUIDs [#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 [#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 [#the-seven-meta-fields]

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

$type [#type]

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

```typescript
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 [#id]

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

```typescript
// 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 [#context]

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

```typescript
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](/docs/reference/concepts/multi-tenancy) for details.

$version [#version]

A monotonically increasing integer that increments on every mutation:

```typescript
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](/docs/reference/concepts/time-travel). The version never decreases, even after a rollback (the rollback itself increments the version).

$createdAt [#createdat]

ISO 8601 timestamp of when the entity was created:

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

Set once at creation time. Never changes.

$updatedAt [#updatedat]

ISO 8601 timestamp of the most recent mutation:

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

Updated on every write operation, including custom verbs.

$createdBy [#createdby]

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

```typescript
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 [#meta-fields-are-read-only]

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

```typescript
// 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 [#using-ids-in-relationships]

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

```typescript
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 [#id-in-urls]

Entity IDs appear in REST URLs and MCP tool calls:

```bash
