PREGO ID Standard
This document is the single source of truth for external-facing entity identifiers across the PREGO platform.
English
1. Core Rule
Every major entity in PREGO must distinguish between:
- Internal database primary key — for joins, indexing, and internal references
- External public/system-facing ID — for APIs, logs, URLs, storage keys, and system-visible references
Internal PK Strategy
- Keep existing
INTEGER PRIMARY KEY AUTOINCREMENTor current internal PK strategy - Use for joins, internal indexing, and internal references
- Never expose raw internal integer IDs externally
External ID Format
[prefix]_[nanoid]| Component | Description |
|---|---|
| prefix | 3-character lowercase identifier for entity type |
| _ | Underscore separator |
| nanoid | URL-safe random string (6, 8, or 10 characters) |
Examples:
usr_A7kP2x(user)ten_M9qL4z(tenant)ses_X2mN8vKp(session)msg_A7kP2xM9qL(message)
2. Prefix Registry
Use meaningful 3-character prefixes for each entity type.
| Prefix | Entity | ID Length | Example |
|---|---|---|---|
usr | user | 6 | usr_A7kP2x |
ten | tenant | 6 | ten_M9qL4z |
app | application | 6 | app_X2mN8v |
wsp | workspace | 6 | wsp_K4pR7y |
mem | membership | 6 | mem_J3nT5w |
ctc | contact | 6 | ctc_H8mS2v |
emp | employee | 6 | emp_L6qW9x |
ses | session | 8 | ses_A7kP2xM9 |
otp | otp request | 8 | otp_X2mN8vKp |
inv | invite | 8 | inv_K4pR7yJ3 |
key | api key | 8 | key_H8mS2vL6 |
dev | device | 8 | dev_M9qL4zA7 |
job | task/job | 10 | job_A7kP2xM9qL |
trc | trace | 10 | trc_X2mN8vKp4z |
pay | payment | 10 | pay_K4pR7yJ3nT |
ivc | invoice | 10 | ivc_H8mS2vL6qW |
aud | audit event | 10 | aud_M9qL4zA7kP |
fil | file | 10 | fil_J3nT5wX2mN |
doc | document | 10 | doc_L6qW9xK4pR |
con | conversation | 10 | con_R7yJ3nT5wX |
msg | message | 10 | msg_S2vL6qW9xK |
rst | reset token | 10 | rst_T5wX2mN8vK |
3. Length Rules
Default: 6-char nanoid
Apply to low-volume entities:
- user, tenant, application, workspace, membership, contact, employee
8-char nanoid
Apply to security-sensitive or medium-volume entities:
- session, otp request, invite, api key, device
10-char nanoid
Apply to high-volume or long-lived entities:
- job, trace, payment, invoice, audit event, file, document, conversation, message, reset token
4. Collision Probability
| Length | Combinations | 1% collision probability |
|---|---|---|
| 6 chars | ~2.1 billion | ~6,500 IDs |
| 8 chars | ~128 billion | ~1.6 million IDs |
| 10 chars | ~7.6 trillion | ~390 million IDs |
The URL-safe alphabet excludes ambiguous characters (0/O, 1/l/I) for 54 total characters.
5. Database Schema Pattern
Every external ID column must be unique.
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT NOT NULL UNIQUE, -- other columns...);
CREATE INDEX idx_users_external_id ON users(external_id);For scoped uniqueness, use a compound unique key:
CREATE TABLE memberships ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT NOT NULL, workspace_id INTEGER NOT NULL, -- other columns... UNIQUE(workspace_id, external_id));6. Collision Handling
All ID generation must include:
- Unique constraint enforcement — Database
UNIQUEconstraint onexternal_id - Retry logic on collision — Up to 3 attempts
- Explicit failure — Throw
IdCollisionErrorif retry budget exhausted
import { generateUniqueId, IdCollisionError } from '@platform/id-utils';
try { const id = await generateUniqueId('user', async (id) => { const exists = await db.query('SELECT 1 FROM users WHERE external_id = ?', [id]); return exists.length > 0; });} catch (error) { if (error instanceof IdCollisionError) { // Handle collision exhaustion (should be extremely rare) }}7. Forbidden Patterns
Do NOT use:
- Raw UUID v4 as public-facing IDs
- Email-derived IDs
- Name-derived IDs
- Raw auto-increment IDs exposed externally
- Prefixless random IDs
- Inconsistent entity naming
8. Storage and API Rules
Use external IDs in:
- API request/response bodies
- URL paths and query parameters
- Log entries and audit traces
- R2/KV storage paths
- System-visible URLs
API Response Format:
{ "user": { "id": "usr_A7kP2x", "email": "user@example.com" }, "tenant": { "id": "ten_M9qL4z", "name": "Acme Corp" }}KV Key Patterns:
| Namespace | Key Pattern |
|---|---|
AUTH_STORE | auth:dev:{emp_id}:{dev_id} |
TENANT_ORIGINS | {ten_id}.pregoi.com |
R2 Path Patterns:
| Bucket | Path Pattern |
|---|---|
prego-usage-raw | usage_raw/tenant_id={ten_id}/dt={date}/... |
prego-contracts | contracts/{year}/{month}/{contract_id}.json |
9. Migration Strategy
This is a safe, non-breaking standardization.
Phase 1: New Entities (Zero Risk)
- Apply new ID standard to any new tables
- Use
@platform/id-utilspackage for generation
Phase 2: Add External ID Columns (Low Risk)
- Add
external_idcolumn to existing tables - Backfill with new format
- APIs expose new format alongside legacy
Phase 3: Tenant Migration (High Risk — Future)
- Create mapping table
- Dual-read period
- Gradual external system migration
Phase 4: Legacy Cleanup (Deferred)
- Remove legacy columns
- Only after 90+ days of no legacy usage
10. Exceptions
These entities keep their current format:
| Entity | Current Format | Reason |
|---|---|---|
| plan | Slug (basic, professional) | Business-meaningful, stable |
| contract | CTR-YYYY-CC-NNNNNN | Legal/compliance requirement |
| billing_customer | cust_{tenant_id} | Stripe correlation |
| subscription | sub_{tenant_id} | Stripe correlation |
| provider_event | Stripe evt_* | External system ID |
| invoice (Stripe) | Stripe in_* | External system ID |
| node | Infrastructure-supplied | Pulumi/infra naming |
| country | ISO code | Standard |
11. Implementation
Use the @platform/id-utils package:
import { generateId, generateUniqueId, isValidId, validateId, assertValidId, ENTITY_PREFIXES,} from '@platform/id-utils';
// Generate IDsconst userId = generateId('user'); // 'usr_A7kP2x'const sessionId = generateId('session'); // 'ses_X2mN8vKp'
// Validate IDsisValidId('usr_A7kP2x', 'user'); // trueassertValidId(req.params.id, 'user'); // throws if invalid
// Parse IDsconst { entityType, prefix, nanoid } = validateId('usr_A7kP2x');See packages/id-utils/README.md for full documentation.
한국어
핵심 규칙
PREGO의 모든 주요 엔티티는 다음을 구분해야 합니다:
- 내부 DB 기본 키 — 조인, 인덱싱, 내부 참조용
- 외부 공개/시스템용 ID — API, 로그, URL, 저장소 키, 시스템 표시용
외부 ID 형식
[prefix]_[nanoid]예시:
usr_A7kP2x(사용자)ten_M9qL4z(테넌트)ses_X2mN8vKp(세션)msg_A7kP2xM9qL(메시지)
접두사 규칙
- 3자 소문자 접두사로 엔티티 타입 식별
- 6자: 저볼륨 엔티티 (user, tenant, application 등)
- 8자: 보안 민감 엔티티 (session, otp, api key 등)
- 10자: 고볼륨 엔티티 (message, audit event, file 등)
데이터베이스 규칙
모든 external_id 컬럼은 고유해야 합니다:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT NOT NULL UNIQUE, -- 기타 컬럼...);충돌 처리
모든 ID 생성 로직은 다음을 포함해야 합니다:
- 고유 제약 조건 적용
- 충돌 시 재시도 로직 (최대 3회)
- 재시도 예산 소진 시 명시적 실패
금지 패턴
다음을 사용하지 마세요:
- 외부에 노출되는 원시 UUID v4
- 이메일 기반 ID
- 이름 기반 ID
- 외부에 노출되는 원시 자동 증가 ID
- 접두사 없는 랜덤 ID
- 일관성 없는 엔티티 명명
마이그레이션 전략
이것은 안전하고 기존 기능을 깨지 않는 표준화입니다:
- 현재 기능 유지
- 활성 API 깨지 않음
- 내부 PK 제거하지 않음
- 누락된 곳에
external_id안전하게 추가 - 필요시 기존 레코드 백필
- 전환 기간 동안 호환성 유지
Related
- Fork-ready platform hub — Platform documentation index
@platform/id-utilsREADME — Package documentation- Control Plane internal-uid.md — Legacy internal UID spec (deprecated)