ashid
v1.2.0
Published
Ash IDs - Time-sortable, double-click-selectable unique identifiers using Crockford Base32
Downloads
714
Maintainers
Readme
ashid
Time-sortable unique identifiers with type prefixes.
ashid("user_") → user_1kbg1jmtt4v3x8k9p2m1n
ashid("tx_") → tx_1kbg1jmts7h2w5r8q4n3m
ashid() → 1kbg1jmtr9k5v2x8p4m1n3wInstallation
npm install ashidimport { ashid } from 'ashid';
const userId = ashid('user_'); // "user_1kbg1jmtt4v3x8k9p2m1n"
const shortId = ashid('u'); // "u1kbg1jmtt4v3x8k9p2m1n0w"
const rawId = ashid(); // "1kbg1jmtt4v3x8k9p2m1n0w"Why ashid?
The Problem
UUIDs are opaque. When you see 550e8400-e29b-41d4-a716-446655440000 in a log, a URL, or a database query, you have no idea what it represents. Is it a user? A transaction? An asset? You have to grep, cross-reference, and waste debugging time.
The Solution
ashid generates IDs that tell you what they are:
user_1kbg1jmtt4v3x8k9p2m1n // ← Obviously a user
tx_1kbg1jmts7h2w5r8q4n3m // ← Obviously a transaction
asset_1kbg1jmtr9k5v2x8p4m1n // ← Obviously an assetWhy Crockford Base32?
Douglas Crockford designed Base32 specifically for human-readable identifiers:
- Case-insensitive —
ABCandabcdecode identically - Character correction —
I,L,l→1andO,o→0(no more "is that a zero or an O?") - No ambiguous characters — excludes
I,L,O,Uentirely - Double-click selectable — no hyphens or special characters
- URL-safe — no encoding required
This is the same approach Stripe uses for their IDs (sk_live_..., pi_..., cus_...).
Time-Sorted by Default
ashid embeds a timestamp, so lexicographic sort = chronological sort. Your database indexes cluster naturally. Your logs are in order. No extra work required.
Prefix Formats
ashid supports two formats based on prefix style:
With underscore delimiter (variable length)
ashid('user_') // user_1kbg1jmtt4v3x8k9p2m1n
ashid('order_') // order_1kbg1jmts7h2w5r8q4n3m- Timestamp: variable length (no padding)
- Random: 13 chars (padded)
- Underscore acts as delimiter for reliable parsing
Without underscore (fixed 22-char base)
ashid('u') // u1kbg1jmtt4v3x8k9p2m1n0w (1 + 22 = 23 chars)
ashid() // 1kbg1jmtt4v3x8k9p2m1n0w (22 chars)- Timestamp: 9 chars (zero-padded)
- Random: 13 chars (padded)
- Fixed length enables parsing without delimiter
API Reference
ashid(prefix?: string): string
Generate a new ID with optional prefix.
import { ashid } from 'ashid';
ashid(); // "1kbg1jmtt4v3x8k9p2m1n0w"
ashid('u'); // "u1kbg1jmtt4v3x8k9p2m1n0w"
ashid('user_'); // "user_1kbg1jmtt4v3x8k9p2m1n"Ashid.create(prefix?, time?, randomLong?): string
Create an ID with explicit timestamp and random values (useful for testing).
import { Ashid } from 'ashid';
Ashid.create('user_', 1733140800000, 8234567890123n);parseAshid(id: string): [string, string, string]
Parse an ID into components: [prefix, encodedTimestamp, encodedRandom].
import { parseAshid } from 'ashid';
const [prefix, ts, rand] = parseAshid('user_1kbg1jmtt4v3x8k9p2m1n');
// prefix = "user_"
// ts = "1kbg1jmtt"
// rand = "4v3x8k9p2m1n"Ashid.prefix(id: string): string
Extract the prefix from an ID.
Ashid.prefix('user_1kbg1jmtt4v3x8k9p2m1n'); // "user_"
Ashid.prefix('1kbg1jmtt4v3x8k9p2m1n0w'); // ""Ashid.timestamp(id: string): number
Extract the timestamp (milliseconds since epoch).
Ashid.timestamp('user_1kbg1jmtt4v3x8k9p2m1n'); // 1733140800000Ashid.random(id: string): bigint
Extract the random portion.
Ashid.random('user_1kbg1jmtt4v3x8k9p2m1n'); // 8234567890123nAshid.isValid(id: string): boolean
Validate an ID.
Ashid.isValid('user_1kbg1jmtt4v3x8k9p2m1n'); // true
Ashid.isValid('invalid'); // falseAshid.normalize(id: string): string
Normalize an ID (lowercase + fix ambiguous characters).
Ashid.normalize('USER_1KBG1JMTT4V3X8K9P2M1N'); // "user_1kbg1jmtt4v3x8k9p2m1n"
Ashid.normalize('user_IKBGIJMTT...'); // "user_1kbg1jmtt..." (I→1)Format
[prefix][timestamp][random]
↓ ↓ ↓
user_ 1kbg1jmtt 4v3x8k9p2m1n
Prefix: Letters + optional underscore (user defined)
Timestamp: Variable or 9 chars Crockford Base32 (milliseconds since epoch)
Random: 13 chars Crockford Base32 (cryptographically secure)Timestamp range: 0 (Unix epoch) to 35184372088831 (Dec 12, 3084)
Crockford Base32 Encoding
The encoding excludes ambiguous characters and maps lookalikes during decoding:
| If you type | Decodes as |
|-------------|------------|
| I, i, L, l | 1 |
| O, o | 0 |
| U, u | v |
Read IDs aloud without confusion. Type without shift key. Copy without encoding errors.
Real-World Use Cases
API Resources
const userId = ashid('user_'); // user_1kbg1jmtt4v3x8k9p2m1n
const orderId = ashid('order_'); // order_1kbg1jmts7h2w5r8q4n3m
const invoiceId = ashid('inv_'); // inv_1kbg1jmtr9k5v2x8p4m1nDatabase Primary Keys
// IDs sort chronologically without additional indexes
const posts = await db.query('SELECT * FROM posts ORDER BY id');Log Debugging
[ERROR] Payment failed for user_1kbg1jmtt4v3x8k9p2m1n on order_1kbg1jmts7h2w5r8q4n3m
// Instantly know: it's a user and an order. No grepping required.URL Slugs
https://app.example.com/users/user_1kbg1jmtt4v3x8k9p2m1n
https://app.example.com/orders/order_1kbg1jmts7h2w5r8q4n3m
// Self-documenting URLs that are still opaque enough for securityHow ashid Compares
| Feature | ashid | uuid | nanoid | cuid2 | ulid | |---------|-------|------|--------|-------|------| | Type prefixes | ✅ Built-in | ❌ | ❌ | ❌ | ❌ | | Time-sortable | ✅ | ❌ | ❌ | ❌ | ✅ | | Human-readable | ✅ Crockford Base32 | ❌ Hex | ⚠️ Base64 | ⚠️ | ⚠️ | | Case-insensitive | ✅ | ✅ | ❌ | ✅ | ❌ | | Character correction | ✅ I→1, O→0 | ❌ | ❌ | ❌ | ❌ | | Double-click selectable | ✅ | ❌ Hyphens | ✅ | ✅ | ✅ | | URL-safe | ✅ | ⚠️ Needs encoding | ✅ | ✅ | ✅ | | Zero dependencies | ✅ | ✅ | ✅ | ✅ | ✅ | | Self-documenting in logs | ✅ | ❌ | ❌ | ❌ | ❌ |
The key difference: When you see user_1kbg1jmtt4v3x8k9p2m1n in a log, you know it's a user. When you see 550e8400-e29b-41d4-a716-446655440000, you have to grep.
Inspired By
ashid stands on the shoulders of giants:
- Stripe's ID format — The
sk_,pi_,cus_prefix convention - Douglas Crockford's Base32 — Human-friendly encoding
- ULID — Time-sortable unique identifiers
- TypeID — Type-safe, K-sortable IDs (similar goals, different implementation)
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
MIT
Author
Created by Dathan Guiley at Wilde Agency.
Built by Wilde Agency
