Skip to main content

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

Examples:

Prefix Registry

Every entity type has a unique, short prefix. When adding a new entity, register its prefix here to avoid collisions. Rules for new prefixes:
  • 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-go
In TypeScript (application layer): TypeID TypeScript package: jetify-com/typeid-js
In PostgreSQL (migration/default): The database has a generate_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 uuid package (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

Examples:

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):
Error responses: Always include the request ID so users can reference it in support tickets:
Structured logging:

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

Examples:

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
See the full list in 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

Example: 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?”

  1. Check the Prefix Registry table above. Make sure your prefix doesn’t collide.
  2. Pick a 2-5 letter abbreviation that’s obvious. cls for cluster, tmpl for template, cred for credential.
  3. Add it to the registry table in this document as part of your PR. The migration and application code should match.
  4. 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 the x-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?”

The RESERVED_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

  1. Application layer first. New IDs generated in TypeScript use the TypeID format immediately.
  2. Database defaults later. Update generate_prefixed_id() to use UUIDv7 + Crockford base32 in a future migration.
  3. 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