@@ -212,6 +212,7 @@ import { isPlainObject } from "./Object.js";
212212import { hasNodeBuffer } from "./Platform.js" ;
213213import { err , getOrNull , getOrThrow , ok , Result , trySync } from "./Result.js" ;
214214import { safelyStringifyUnknownValue } from "./String.js" ;
215+ import type { TimeDep } from "./Time.js" ;
215216import type { Literal , Simplify , WidenLiteral } from "./Types.js" ;
216217import { IntentionalNever } from "./Types.js" ;
217218
@@ -1589,41 +1590,27 @@ export const formatSimplePasswordError = (
15891590 ) ;
15901591
15911592/**
1592- * Globally unique identifier.
1593- *
1594- * **Evolu Id** is 16 random bytes from a cryptographically secure random
1595- * generator, encoded as 22-character Base64Url string. This provides strong
1596- * collision resistance for distributed ID generation.
1597- *
1598- * ### Design Rationale
1599- *
1600- * Why Evolu Id over alternatives:
1601- *
1602- * - **NanoID**: No standard binary serialization format, and uses only ~126 bits
1603- * of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
1604- * 128 bits.
1605- * - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
1606- * Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
1607- * standard string representation is verbose.
1608- * - **UUID v7**: Includes timestamp in the ID, which leaks information about when
1609- * data was created. This is a privacy concern for local-first applications
1610- * where creation time must remain private.
1611- *
1612- * Evolu Id provides 128 bits of entropy, compact string representation (22
1613- * characters), standard and native string serialization (Base64Url), and no
1614- * privacy leaks.
1615- *
1616- * ### Future Consideration
1617- *
1618- * For database-heavy workloads where insert performance is critical, a hybrid
1619- * approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
1620- * where H is a keyed hash function and N is a configurable parameter. This
1621- * would maintain spatial locality for database caches (improving insert
1622- * performance by an order of magnitude) while adding entropy to prevent
1623- * timestamp leakage and correlation across systems. The parameter N would allow
1624- * trading off cache locality (larger N = better locality) versus entropy
1625- * distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
1626- * details on this approach.
1593+ * Evolu Id: 16 bytes encoded as a 22‑character Base64Url string.
1594+ *
1595+ * There are three ways to create an Evolu Id:
1596+ *
1597+ * - {@link createId} – default cryptographically secure random bytes
1598+ * (privacy‑preserving)
1599+ * - {@link createIdFromString} – deterministic: first 16 bytes of SHA‑256 of a
1600+ * string
1601+ * - {@link createIdAsUuidv7} – optional: embeds timestamp bits (UUID v7 layout)
1602+ *
1603+ * Privacy: the default random Id does not leak creation time and is safe to
1604+ * share or log. The UUID v7 variant leaks creation time anywhere the Id is
1605+ * copied (logs, URLs, exports); only use it when you explicitly want insertion
1606+ * locality for very large write‑heavy tables and accept timestamp exposure.
1607+ *
1608+ * ### Future
1609+ *
1610+ * A possible hybrid masked‑time approach (`timestamp ^ H(cluster_id, timestamp
1611+ *
1612+ * > > N)`) could provide locality without exposing raw creation time. See
1613+ * > > https://brooker.co.za/blog/2025/10/22/uuidv7.html
16271614 *
16281615 * @category String
16291616 */
@@ -1641,26 +1628,25 @@ export const formatIdError = createTypeErrorFormatter<IdError>(
16411628) ;
16421629
16431630/**
1644- * Creates an {@link Id}.
1631+ * Creates a random {@link Id}. This is the recommended default.
1632+ *
1633+ * Use {@link createIdFromString} for deterministic mapping of external IDs or
1634+ * {@link createIdAsUuidv7} when you accept timestamp leakage for index
1635+ * locality.
16451636 *
16461637 * ### Example
16471638 *
16481639 * ```ts
1649- * // string & Brand<"Id">
16501640 * const id = createId(deps);
1651- *
1652- * // string & Brand<"Id"> & Brand<"Todo">
16531641 * const todoId = createId<"Todo">(deps);
16541642 * ```
16551643 */
16561644export const createId = < B extends string = never > (
16571645 deps : RandomBytesDep ,
1658- ) : [ B ] extends [ never ] ? Id : Id & Brand < B > =>
1659- uint8ArrayToBase64Url ( deps . randomBytes . create ( 16 ) ) as unknown as [ B ] extends [
1660- never ,
1661- ]
1662- ? Id
1663- : Id & Brand < B > ;
1646+ ) : [ B ] extends [ never ] ? Id : Id & Brand < B > => {
1647+ const id = uint8ArrayToBase64Url ( deps . randomBytes . create ( 16 ) ) ;
1648+ return id as unknown as [ B ] extends [ never ] ? Id : Id & Brand < B > ;
1649+ } ;
16641650
16651651/**
16661652 * Creates an {@link Id} from a string using SHA-256.
@@ -1704,6 +1690,42 @@ export const createIdFromString = <B extends string = never>(
17041690 return id as [ B ] extends [ never ] ? Id : Id & Brand < B > ;
17051691} ;
17061692
1693+ /**
1694+ * Creates an {@link Id} embedding timestamp bits (UUID v7 layout) before
1695+ * Base64Url encoding.
1696+ *
1697+ * Tradeoff: better insertion locality / index performance for huge datasets vs
1698+ * leaking creation time everywhere the Id appears. Evolu uses {@link createId}
1699+ * by default to avoid activity leakage; choose this only if you explicitly
1700+ * accept timestamp exposure.
1701+ *
1702+ * ### Example
1703+ *
1704+ * ```ts
1705+ * const id = createIdAsUuidv7({ randomBytes, time });
1706+ * const todoId = createIdAsUuidv7<"Todo">({ randomBytes, time });
1707+ * ```
1708+ */
1709+ export const createIdAsUuidv7 = < B extends string = never > (
1710+ deps : RandomBytesDep & TimeDep ,
1711+ ) : [ B ] extends [ never ] ? Id : Id & Brand < B > => {
1712+ const id = deps . randomBytes . create ( 16 ) ;
1713+
1714+ const timestamp = globalThis . BigInt ( deps . time . now ( ) ) ;
1715+
1716+ id [ 0 ] = globalThis . Number ( ( timestamp >> 40n ) & 0xffn ) ;
1717+ id [ 1 ] = globalThis . Number ( ( timestamp >> 32n ) & 0xffn ) ;
1718+ id [ 2 ] = globalThis . Number ( ( timestamp >> 24n ) & 0xffn ) ;
1719+ id [ 3 ] = globalThis . Number ( ( timestamp >> 16n ) & 0xffn ) ;
1720+ id [ 4 ] = globalThis . Number ( ( timestamp >> 8n ) & 0xffn ) ;
1721+ id [ 5 ] = globalThis . Number ( timestamp & 0xffn ) ;
1722+
1723+ id [ 6 ] = ( id [ 6 ] & 0x0f ) | 0x70 ;
1724+ id [ 8 ] = ( id [ 8 ] & 0x3f ) | 0x80 ;
1725+
1726+ return id as unknown as [ B ] extends [ never ] ? Id : Id & Brand < B > ;
1727+ } ;
1728+
17071729/**
17081730 * Creates a branded {@link Id} Type for a table's primary key.
17091731 *
0 commit comments