What a UUID actually is
A UUID is 128 bits with a version nibble that says how those bits were constructed. The canonical string is 32 hex digits and four hyphens (36 characters). RFC 9562 updates the older RFC 4122 layouts and standardizes v7. UUIDs are identifiers, not capabilities. Anyone who can guess or enumerate them should not gain access; if they do, you built a secret into an ID. Generate them with a CSPRNG (Web Crypto, `crypto.randomUUID`, `/dev/urandom`), never with `Math.random()`. PureDevKit’s UUID generator emits v4, v7, and NanoID entirely in the browser.
Comparison: v4 vs v7 vs NanoID vs auto-increment
Pick the column type that matches your access pattern — not a blog fashion. This table is the short version of the rest of the guide.
| Option | Sortable by time? | Hides creation time? | DB index locality | Best for |
|---|---|---|---|---|
| UUID v4 | No | Yes | Poor (random inserts) | Public IDs when order must stay hidden |
| UUID v7 | Yes (approx.) | No — leaks timestamp | Good (appends near end) | Primary keys on write-heavy OLTP |
| NanoID | No (unless you encode time) | Usually yes | Poor if used as PK | Short public URL slugs |
| Auto-increment / SERIAL | Yes | No — sequential | Excellent | Internal-only tables, simple apps |
v4: random and scattered
v4 fills most bits with random data (uuidv4 / `crypto.randomUUID`). Collision risk is negligible at application scale (122 bits of randomness). The downside is index locality: new rows insert at random positions in a B-tree, which can page-split and bloat indexes on hot tables. v4 is still a good public ID when you do not want the identifier to reveal creation time. It is a poor default for a primary key on a write-heavy OLTP table if you have a time-ordered alternative.
v7: time-ordered without v1’s baggage
v7 puts a Unix timestamp in the high bits and random data in the rest. Values roughly sort by creation time, so inserts append near the end of an index. That is usually what you want for primary keys. You do leak an approximate creation timestamp to anyone who sees the ID. If that is a problem (for example, hiding when an account was created), keep v7 internal and expose a random public slug. Do not use v1 in new work: legacy layouts could embed MAC addresses. ULID occupies a similar niche; v7 is the UUID-shaped version that fits existing UUID columns.
When to use UUID v7 (decision guide)
Use v7 as the default UUID primary key in new systems unless one of the exceptions below applies.
- Use v7 when the ID is a database primary key or join key on a write-heavy table.
- Use v7 when you want rough chronological sort without a separate created_at index for locality.
- Prefer v4 (or NanoID) for public URLs when revealing “account created around date X” is a privacy or business risk.
- Prefer SERIAL / IDENTITY when the table is internal-only, single-database, and you want the simplest ops story.
- Prefer NanoID when the ID appears in URLs and you want something shorter than a 36-character UUID.
- Never treat any of these as secrets — size and store API keys separately (prefix + high entropy + hashed at rest).
Code examples (generate and store)
Node 20+ still exposes `randomUUID()` as v4. For v7, use a library that implements RFC 9562, or generate in the browser with PureDevKit and paste into fixtures.
JavaScript / TypeScript
import { randomUUID } from "node:crypto";
import { v7 as uuidv7 } from "uuid"; // uuid@9+
const publicId = randomUUID(); // v4 — fine for opaque public refs
const rowId = uuidv7(); // v7 — better default PK
// Postgres: store as UUID; index locality favors v7 inserts
// CREATE TABLE items (id uuid PRIMARY KEY, ...);NanoID, keys, and prefixes
Public IDs in URLs are often nicer as NanoIDs or base32 strings than as hyphenated UUIDs. Keep them long enough for your collision budget. API keys should look different from row IDs: a prefix (`dk_live_`) plus a high-entropy tail, shown once, stored as SHA-256 on the server. Generate samples with the UUID / NanoID tool, then hash keys on the server — never log the raw secret.