UUIDs & IDs Strategy
This document defines how we generate, format, and use UUIDs and IDs across the platform. It covers resource IDs, request IDs, organization slugs, and API keys. Follow these conventions when adding new entities, endpoints, or services.Resource IDs (TypeID format)
Every persisted entity gets a TypeID — a type-prefixed, UUIDv7-based identifier encoded in Crockford base32.Format
Prefix Registry
Every entity type has a unique, short prefix. When adding a new entity, register its prefix here to avoid collisions.| Prefix | Entity | Schema | Notes |
|---|---|---|---|
usr | User | auth | User identifier |
org | Organization | auth | Organization identifier |
mem | Organization member | auth | Organization member identifier |
inv | Invitation | auth | Invitation identifier |
proj | Project | atlas | Project identifier |
pmem | Project member | atlas | Project member identifier |
cls | Cluster | atlas | Future |
srv | Bare metal server | atlas | Future |
evt | Audit event | atlas | Future |
tmpl | Cluster template | atlas | Future |
cred | Cloud credential | atlas | Future |
- 2-5 lowercase ASCII letters (no digits, no underscores)
- Must be an obvious abbreviation of the entity name
- Must not collide with any existing prefix
- Add to this table before writing the migration
Generating Resource IDs
In Golang (application layer): TypeID Golang package: jetify-com/typeid-gogenerate_prefixed_id(prefix) function. Use it as the column default:
Note: The current PostgreSQL function uses UUIDv4 with stripped dashes (not TypeID encoding). We plan to migrate this to match the TypeID format. Until then, IDs generated in Postgres vs. the application layer will look slightly different but both are valid. The prefix is the important part for debugging.
Decoding a Resource ID
If you need to extract the underlying UUID (for logging, debugging, or cross-referencing):Why TypeID over alternatives?
We evaluated nanoid, cuid2, ULID, KSUID, and bare UUIDs. TypeID won because:- It’s a spec, not a library. The format is defined at github.com/jetify-com/typeid. If the reference library is abandoned, we can replace it with ~60 lines of code wrapping the
uuidpackage (which has 385M+ weekly downloads). - Type prefix is baked into the ID. A grep for
org_in logs instantly filters to organizations. With bare UUIDs you’d need a join or additional context. - UUIDv7 under the hood. The underlying ID is an IETF standard (RFC 9562). We’re not locked into a niche format.
- Crockford base32 encoding. Shorter than hex UUIDs (26 chars vs 32), no ambiguous characters, lowercase by default.
Request IDs
Every API request gets a request ID for end-to-end tracing. These are not persisted as database entities — they live in logs, headers, and error responses.Format
| Segment | Description | Length |
|---|---|---|
req_ | Fixed prefix (identifies this as a request) | 4 chars |
{region} | Deployment region code | 2-3 chars |
{timestamp_hex} | Date.now() as hex | 12 hex chars |
{entropy_hex} | Random bytes via Web Crypto | 16 hex chars |
Why not TypeID for requests?
Request IDs embed the region for grep-based debugging. When an incident hits,grep req_us1 access.log instantly narrows to the US-1 region. TypeID’s base32 encoding would obscure this.
Generating Request IDs
Using Request IDs
Middleware (API layer):Organization Slugs
Organization slugs appear in URLs (/orgs/{slug}/...) and optionally as subdomains. They are user-chosen, derived from the organization name, and must be globally unique.
Format
Reserved Slugs
Certain slugs are blocked to prevent route collisions and subdomain hijacking:- Route collisions:
new,create,edit,delete,settings,members,billing,invitations - Subdomain collisions:
api,admin,www,app,cdn,auth,console,docs,status - Platform identity:
k0rdent,atlas,arc,mirantis,platform,system
packages/shared/src/utils/org-slug.ts.
Utilities
API Keys
API keys use the existing format and do not follow the TypeID convention. Keys are secrets — they’re not user-facing identifiers that need to be human-readable.Format
| Segment | Description |
|---|---|
k0r_ | Platform prefix |
{type} | Key type: live, test, ci |
{random} | Base62-encoded random bytes |
k0r_live_7Hs9kQ2mNpR4xYzA...
See docs/api-key-system-design.md for the full API key design.
Common Patterns & Recipes
Creating a new entity with a TypeID
Validating an ID format in an API handler
Passing IDs between services
Always pass TypeIDs as-is (with prefix). Never strip the prefix for storage or transport — the whole point is that the prefix travels with the ID.Logging with request context
Edge Cases & Guidance
”I need a new entity type. What prefix do I use?”
- Check the Prefix Registry table above. Make sure your prefix doesn’t collide.
- Pick a 2-5 letter abbreviation that’s obvious.
clsfor cluster,tmplfor template,credfor credential. - Add it to the registry table in this document as part of your PR. The migration and application code should match.
- If you’re unsure, open a thread in
#platform-eng— prefix collisions are painful to fix after the fact.
”My entity doesn’t need a prefix (it’s internal-only).”
It still gets one. Internal entities end up in logs, error messages, and debugging sessions. The prefix costs nothing and saves time when someone is grepping through logs at 2am.”Can I use a different ID format for my table?”
Discuss with the team first. The whole point of a unified strategy is consistency. If you have a legitimate reason (e.g., you’re integrating with an external system that mandates UUIDs), document the exception in your PR and get a review from the platform team.”The PostgreSQL generate_prefixed_id function doesn’t match TypeID encoding.”
Known issue. The Postgres function currently uses prefix_ + hex-encoded UUIDv4 (dashes stripped). The application layer uses TypeID (prefix + Crockford base32-encoded UUIDv7). Both are valid and the prefix is the important part. We plan to align these in a future migration. In the meantime, don’t write code that assumes a specific encoding for the suffix — always treat the suffix as opaque.
”Should request IDs be stored in the database?”
No. Request IDs live in logs, response headers, and error payloads. If you need to correlate a request to a database row, log both the request ID and the resource ID together. Storing request IDs in tables adds write overhead for no benefit.”A user is passing a request ID from the client — should I trust it?”
Accept it via thex-request-id header for tracing continuity, but validate the format. If it doesn’t match req_{region}-{hex}-{hex}, generate a new one. Never let a malformed request ID propagate into logs.
”I’m adding a reserved slug — where do I update?”
TheRESERVED_SLUGS set lives in packages/shared/src/utils/org-slug.ts. Add your reserved word there and add a test case. Common reasons to reserve a slug: new top-level route, new subdomain, new platform concept.
”When in doubt…”
Open a discussion in#platform-eng. ID format decisions are easy to make and hard to undo. A 5-minute conversation prevents months of migration pain.
Migration Notes
Current state (as of migration 0003)
- Database IDs use
generate_prefixed_id(prefix)→prefix_+ hex UUIDv4 (no dashes) - Org slugs derived from email username + random hex suffix
- Request IDs not yet standardized across all services
Target state
- Database defaults aligned with TypeID format (prefix + Crockford base32 UUIDv7)
- Org slugs user-chosen from org name
- Request IDs standardized with region embedding
generate_prefixed_id()Postgres function updated to produce TypeID-compatible output
Migration path
- Application layer first. New IDs generated in TypeScript use the TypeID format immediately.
- Database defaults later. Update
generate_prefixed_id()to use UUIDv7 + Crockford base32 in a future migration. - No backfill needed. Existing IDs remain valid — they have the right prefix, just a different suffix encoding. All code should treat the suffix as opaque.
Quick Reference
| What | Format | Example |
|---|---|---|
| Resource ID | {prefix}_{base32_uuidv7} | proj_01h455d8sc2shh7cysq5bn60y1 |
| Request ID | req_{region}-{timestamp_hex}-{entropy_hex} | req_us1-018f4a2b3c4d-a1b2c3d4e5f6g7h8 |
| Organization slug | {lowercase-alphanumeric-hyphens} | acme-corp |
| API key | k0r_{type}_{base62} | k0r_live_7Hs9kQ2mNpR4xYzA... |
| Randomness source | crypto.getRandomValues() | Universal across all runtimes |