Identifiers
An identifier's job is to name one row and keep naming it. Everything else people ask of one — sortability, guessability, a hint about what it points at — trades against that job. The rule worth adopting early is that an identifier is opaque: pass it back unchanged, compare it exactly, and never read meaning out of its shape.
The three families#
| Kind | Good at | Costs |
|---|---|---|
| Sequential integer | Small, sortable, cheap to index | Enumerable by anyone who has one, and leaks volume — a customer can read your growth from two order numbers |
| Random UUID (version 4) | Unguessable, generated anywhere without coordination | Poor index locality: random inserts scatter across the index rather than appending to it |
| Time-ordered UUID (version 7) | Unguessable enough, and sorts roughly by creation, restoring index locality | Reveals approximately when the row was created, which is occasionally more than you meant to say |
A prefixed identifier — a short type tag followed by the unique part — is a small addition that pays for itself in logs and in support conversations, because a value pasted into a message says what kind of thing it is. Connect's workspace identifiers work this way: a customer workspace carries a ws_ prefix, and the operator's own workspace has a readable name of its own.
Never parse an identifier#
The rule has one justification and it is enough: the shape is an implementation detail of whoever issues the identifier, and it will change. Code that splits on a prefix, extracts a timestamp, or assumes a length breaks silently at the moment the issuer adds a second format — and it breaks for new records only, so half the system keeps working.
- Store the whole value, in a field wide enough for a longer one later.
- Compare exactly. Do not trim, lower-case or normalise unless the issuer documents that as valid.
- Do not sort by it and call that chronological order unless the format guarantees it.
- Do not derive a second identifier from it.
- Do not display it as though it means something, which invites everybody reading the screen to parse it in their head.
An identifier is not a permission#
Unguessable identifiers make a resource hard to find by accident. They do not decide who may act on it, and the moment they are treated as though they do, the authorisation model is “whoever obtained the identifier”, which includes every place it has ever been pasted.
This system produced the lesson concretely. Four mailbox routes took an identifier from the browser and fetched the row with a bare lookup — one the workspace kernel's query filter can miss entirely when the row is already loaded in the session's identity map. The result was routes that answered for any workspace's row. They go through the mailbox console's own scoped lookup now.
Does Connect use opaque identifiers?#
Yes, and in more than one shape, because the things being identified are not alike.
- Workspaces
- A prefixed identifier per customer workspace; the operator's own is named rather than generated
- Records inside a workspace
- Opaque identifiers, scoped by
workspace_idand filtered by the kernel and by row-level security - A person's channel addresses
- Identity — one address on one channel, keyed by kind and value together, so the same string on two channels is two identities
- Documentation and audit references
- Stable identifiers that are never reused, so a reference in a report still resolves years later
The identity case is the interesting one for anybody modelling people. A Person is the canonical human and may hold many identities; the key pairs the channel kind with the value rather than treating an address as globally unique. That is what allows one person to be recognised across email, WhatsApp and the phone without a name ever being used as a key — and names, across scripts and romanisations, make very poor keys.
Two adjacent mistakes#
- Reusing an identifier
- A deleted row's identifier handed to a new row makes every historical reference silently wrong. Identifiers are never reused, even when they look free
- Using a natural key as the identifier
- An email address identifies a person until they change it, at which point every reference to them is orphaned or, worse, points at whoever holds that address next
- Exposing a sequential key externally
- Two invoices tell a recipient how many you issued in between. This is information disclosure with no attacker required
- Assuming uniqueness across systems
- An identifier is unique within whoever issues it. A provider's message identifier and yours are different namespaces and should not share a column
Questions#
Why are identifiers in Connect long and meaningless?
Because the alternative leaks. A short sequential number tells anybody holding two of them how much happened in between, and it lets somebody try the next one. Length and meaninglessness are the cost of an identifier that says nothing except which row it names.
Can I rely on an identifier to keep other people out of a record?
No. Being hard to guess is not the same as being protected. Every request in Connect is filtered by workspace three times — the customer allowlist, the workspace kernel and row-level security in the database — precisely because the identifier itself decides nothing.
Two people share an email address on different channels. Is that one identity?
No. An identity pairs the channel kind with the value, so the same string reached on two channels is two identities, and both can attach to the same Person. That is the model that makes a single view of somebody possible without pretending an address means the same thing everywhere.