Skip to content

feat(db): replace UUID with prefixed CUID2 IDs#2162

Merged
koistya merged 1 commit into
mainfrom
database-ids
Feb 18, 2026
Merged

feat(db): replace UUID with prefixed CUID2 IDs#2162
koistya merged 1 commit into
mainfrom
database-ids

Conversation

@koistya

@koistya koistya commented Feb 18, 2026

Copy link
Copy Markdown
Member

Summary

Replace gen_random_uuid() database defaults with application-generated prefixed CUID2 identifiers across all auth tables. The 3-char prefix encodes entity type for instant recognition in logs, URLs, and support tickets — same pattern as Stripe (cus_, sub_), Clerk (user_, org_).

 usr_ght4k2jxm7pqbv01
└─3─┘└──────16──────┘  20 chars total

Why prefixed IDs

  • Debuggability — seeing usr_... in a Sentry trace or log line immediately tells you the entity type without a database lookup
  • Type safety at boundaries — a function receiving an org_ ID can reject a usr_ ID before hitting the DB
  • Double-click selection — underscore-joined strings select as one word in terminals and editors (unlike hyphenated UUIDs)

Why CUID2 over UUID v4

  • Shorter (16-char body vs 32 hex digits), alphanumeric-only (no dashes, URL-safe)
  • Multi-source entropy + SHA3 hash — no timestamp or host leakage (paralleldrive/cuid2)
  • Lazy-initialized generator — no module-level side effects, safe for Cloudflare Workers isolates

Changes

  • db/schema/id.ts (new) — generateAuthId(model) and generateId(prefix) helpers with prefix map, lazy CUID2 init
  • db/schema/*.ts — replace .default(sql'gen_random_uuid()') with .$defaultFn(() => generateAuthId(...))
  • apps/api/lib/auth.ts — wire generateAuthId into Better Auth's advanced.database.generateId hook
  • db/migrations/0000_init.sql — regenerated (removes DB-level DEFAULT clauses)
  • db/package.json — add @paralleldrive/cuid2 dependency
  • docs/ — update schema docs, add docs/specs/prefixed-ids.md spec

Usage

import { generateAuthId, generateId } from "@repo/db";

generateAuthId("user");        // "usr_ght4k2jxm7pqbv01"
generateAuthId("session");     // "ses_m8xk3jvqp2wnba09"
generateId("upl");             // "upl_p4qr7nxlk2vmtc06"

Prefix map

Model Prefix Table
user usr user
session ses session
account idn identity
verification vfy verification
organization org organization
member mem member
invitation inv invitation
passkey pky passkey
subscription sub subscription

Migration

Existing text() columns accept both UUID and prefixed-CUID2 values — no data migration required. New inserts get the new format; legacy rows keep UUIDs.

Test plan

  • bun typecheck passes
  • bun test passes
  • bun db:generate produces no diff (migration already regenerated)
  • Sign up flow creates users with usr_ prefixed IDs
  • Better Auth session/identity/verification records use correct prefixes

Switch all primary keys from `gen_random_uuid()` to application-generated
prefixed CUID2 identifiers (e.g. `usr_ght4k2jxm7pqbv01`). The 3-char
prefix encodes entity type for instant recognition in logs, URLs, and
support tickets — same pattern as Stripe and Clerk.

- Add `db/schema/id.ts` with `generateAuthId` / `generateId` helpers
- Wire `generateAuthId` into Better Auth's `advanced.database.generateId`
- Migrate all schema files from `.default(sql`gen_random_uuid()`)` to
  `.$defaultFn(() => generateAuthId(...))`
- Regenerate `0000_init` migration (removes DB-level defaults)
- Add `@paralleldrive/cuid2` dependency to `db` package
- Add spec at `docs/specs/prefixed-ids.md`
@koistya koistya merged commit f908307 into main Feb 18, 2026
15 checks passed
@koistya koistya deleted the database-ids branch February 18, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant