ULID and UUIDv7 Identifiers
How Truestamp assigns identifiers - ULID for Item IDs and UUIDv7 for every other resource - both timestamp-prefixed and lexicographically sortable, stored as 128-bit binary in Postgres uuid columns.
Overview
Every resource in Truestamp gets a unique identifier at the moment it is created, and those identifiers are deliberately not random opaque strings. Truestamp uses two closely related schemes: ULID for Item IDs, and UUIDv7 for everything else (Blocks, Teams, Users, entropy observations, and other resources). Both schemes embed a millisecond timestamp at the front of the value, which makes them sortable in generation order and globally unique. Both are stored the same way in the database: as a raw 128-bit (16-byte) binary value in a Postgres uuid column. The two text formats also look nothing alike, which is how the search page can tell what kind of record a pasted identifier refers to without being told.
Why timestamp-prefixed, sortable IDs
A traditional random identifier (such as UUIDv4) tells you nothing about when the thing it names came into existence, and two IDs generated one after another can land anywhere relative to each other. That forces the database to carry and index a separate “created” column just to list things in order, and it scatters newly inserted rows randomly across the index.
ULID and UUIDv7 solve this by putting a 48-bit millisecond timestamp at the most-significant end of the value, followed by random bits. Two useful properties fall out of that layout:
- Lexicographically sortable by time. Because the timestamp is at the front, sorting the IDs as plain strings (or as their binary form) also sorts them in generation order. You can order a list of items or blocks by ID and get chronological order for free.
- Globally unique. The random suffix (80 bits for ULID) makes an accidental collision between two IDs generated in the same millisecond overwhelmingly unlikely, so IDs can be minted independently without coordination.
The timestamp is part of the identifier itself, so given only an ID you can read back the millisecond at which the identifier was generated, with no database lookup.
An important caveat: the embedded timestamp records when the identifier was generated. It is a convenience for ordering and indexing, not a source of trust. Truestamp’s cryptographic timing guarantee (the submission window) comes from the blockchain and proof machinery, not from an ID’s timestamp. See domain-separated hashing for how the load-bearing hashes are constructed.
ULID for Item IDs
Items (the core timestamped records) use ULID identifiers. A ULID is a 26-character string encoded in Crockford Base32, an alphabet of digits and uppercase letters that excludes the ambiguous characters I, L, O, and U. The result is compact, URL-safe (no hyphens), and case-insensitive on input, with a canonical uppercase form.
A ULID is 128 bits: a 48-bit millisecond timestamp followed by 80 bits of cryptographic randomness. A generated ULID looks like this:
01HJHB01T8FYZ7YTR9P5N62K5B
Because input is normalized to uppercase before validation, a lowercase or mixed-case ULID such as 01hjhb01t8fyz7ytr9p5n62k5b is accepted and treated as the same identifier. The millisecond timestamp can be extracted directly from the ULID by reading its leading 48 bits, and can be presented either as a Unix millisecond value or as a full date-time.
UUIDv7 for everything else
Every non-Item resource (Blocks, Teams, Users, entropy observations, and the rest) uses a UUIDv7 identifier. UUIDv7 is the time-ordered UUID variant standardized in RFC 9562. It is written in the familiar 36-character hyphenated UUID form:
018e90d8-06e8-7f9f-bfd7-6730ba98a51b
Like ULID, UUIDv7 places a 48-bit millisecond timestamp at the front, so UUIDv7 values are also lexicographically sortable by generation time and carry an extractable timestamp. Generation uses system time with submillisecond precision to keep IDs monotonic (later-generated IDs sort after earlier ones even within the same millisecond), and the remaining bits are random, giving global uniqueness. UUIDv7 strings are case-insensitive on input and normalized to lowercase.
The two schemes are intentionally symmetric: ULID and UUIDv7 both encode a leading millisecond timestamp plus a random remainder, so both give you the same time-ordered, globally-unique behavior. They differ only in their text encoding (Crockford Base32 versus hyphenated hex) and in where each is used.
Storage as 128-bit binary in Postgres
Both ULID and UUIDv7 are 128-bit values, so both are stored in Postgres uuid columns as raw 16-byte binary. A Postgres uuid column is format-agnostic: it stores 16 bytes and does not impose UUID-specific semantics, which is why it can hold a ULID’s binary form just as well as a UUIDv7’s. Storing the binary form (rather than the text form) keeps identifiers compact and lets the database index and sort them efficiently.
This creates one boundary worth understanding. When code queries the database by an Item’s ID, the ULID’s Crockford Base32 string first has to be turned into the 16-byte binary the uuid column holds, and on the way back the raw binary is turned into the canonical uppercase ULID string. To bridge the ULID text form and the hyphenated hex form Postgres tooling expects, an Item’s ULID is converted to a UUID hex string (36 characters, with hyphens) at the query boundary. That conversion is exact and lossless: it decodes the ULID to its 16 bytes and re-encodes those same bytes as UUID hex. The two representations name the identical 128-bit value, just written in different alphabets.
Citations
- RFC 9562: Universally Unique IDentifiers (UUIDs). Defines UUIDv7, the time-ordered UUID variant used for all non-Item identifiers.
- The ULID specification. Defines the ULID layout (48-bit timestamp + 80-bit randomness) and the Crockford Base32 encoding used for Item identifiers.