ashid
Time-sortable unique identifiers with type prefixes. Ruby port of the ashid library, wire-compatible with the TypeScript and Kotlin implementations.
Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate("tx") # => "tx_1je3kvrg1000075n93qdpk"
Ashid.generate # => "1je3kvrg000007fn17wx6b"
Why ashid?
UUIDs are opaque. When you see 550e8400-e29b-41d4-a716-446655440000 in a log, you have no idea what it represents.
ashid generates IDs that tell you what they are:
user_1je3kvrg000007fn17wx6b ← Obviously a user
tx_1je3kvrg1000075n93qdpk ← Obviously a transaction
asset_1je3kvrg200005j6x7eygm ← Obviously an asset
Features
- Type prefixes — Self-documenting IDs, like Stripe (
sk_,pi_,cus_). - Time-sortable — Lexicographic sort = chronological sort.
- Crockford Base32 — Case-insensitive,
I/L→1,O→0(no more "is that a zero or an O?"). - Double-click selectable — No hyphens or special characters.
- URL-safe — No encoding required.
- Cross-language — Round-trips with the TypeScript and Kotlin libraries.
- Zero runtime dependencies — Just stdlib
SecureRandom.
Installation
gem install ashid
Or in a Gemfile:
gem "ashid"
Quick Start
require "ashid"
Ashid.generate # => "1je3kvrg000007fn17wx6b"
Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate4("tok") # => "tok_14d2pf2dbsqqg0zvebn63pags1" (random-only, like UUIDv4)
id = Ashid.generate("user")
Ashid.parse(id) # => {prefix: "user_", encoded_timestamp: "...", encoded_random: "..."}
Ashid.(id) # => 1733140800000
Ashid.time(id) # => 2024-12-02 12:00:00 UTC
Ashid.valid?(id) # => true
API Reference
Ashid.generate(prefix = nil, time:, random:)
Generate a time-sortable ID, optionally with a prefix.
Ashid.generate # => "1je3kvrg000007fn17wx6b"
Ashid.generate("user") # => "user_1je3kvrg000007fn17wx6b"
Ashid.generate("user", time: 1_733_140_800_000, random: 8_234_567_890_123)
prefix(positional, optional): alphanumeric chars are kept and lowercased; everything else is stripped. The trailing_delimiter is added automatically — passing"user","user_", or"user-"yields the same result.time:(keyword, defaults to current ms): Integer milliseconds since Unix epoch. Range:0to35_184_372_088_831(Dec 12, 3084).random:(keyword, defaults to a secure 64-bit random Integer): non-negative Integer.
Raises ArgumentError for negative time, time exceeding the max, or negative random.
Ashid.generate4(prefix = nil, random1:, random2:)
Generate a random-only ID (UUIDv4 equivalent), without time-sortability. 26-char base, ~106 bits of entropy. Useful for tokens, secrets, or any case where unpredictability matters more than ordering.
Ashid.generate4 # => "14d2pf2dbsqqg0zvebn63pags1" (26 chars)
Ashid.generate4("tok") # => "tok_14d2pf2dbsqqg0zvebn63pags1"
Ashid.parse(id)
Parse an ID into its components.
Ashid.parse("user_1je3kvrg000007fn17wx6b")
# => {prefix: "user_", encoded_timestamp: "1je3kvrg0", encoded_random: "00007fn17wx6b"}
Raises Ashid::InvalidIdError for nil, empty, or malformed input.
Ashid.prefix(id), Ashid.timestamp(id), Ashid.time(id), Ashid.random(id), Ashid.random_bytes(id)
Convenience accessors:
id = Ashid.generate("user")
Ashid.prefix(id) # => "user_"
Ashid.(id) # => 1733140800000 (Integer ms)
Ashid.time(id) # => 2024-12-02 12:00:00 UTC (Time object)
Ashid.random(id) # => 8234567890123 (Integer)
Ashid.random_bytes(id) # => "\x00\x00..." (8-byte ASCII-8BIT String)
Ashid.valid?(id)
Returns true/false. Never raises, even on nil or non-String input.
Ashid.valid?("user_1je3kvrg000007fn17wx6b") # => true
Ashid.valid?(nil) # => false
Ashid.valid?("garbage") # => false
Ashid.normalize(id)
Canonicalize an ID: lowercase the prefix and resolve Crockford lookalikes (I/L → 1, O → 0, U → V).
Ashid.normalize("USER_IJE3KVRGOOOOO7FNI7WX6B")
# => "user_1je3kvrg000007fn17wx6b"
Format
[prefix_]?[timestamp][random]
↓ ↓ ↓
user_ 1je3kvrg0 00007fn17wx6b
- With prefix: variable-length timestamp + 13-char padded random.
- Without prefix: 9-char zero-padded timestamp + 13-char padded random = fixed 22 chars.
generate4(random-only): 13 + 13 = 26-char base.
Crockford alphabet: 0123456789abcdefghjkmnpqrstvwxyz. Excludes i, l, o, u. On decode, lookalikes map: I/L → 1, O → 0, U → V.
Cross-language compatibility
ashid IDs round-trip byte-for-byte across:
- ashid (TypeScript/JavaScript)
- ashid (Kotlin/Java)
- This Ruby gem
A test fixture in test/fixtures/parity.json verifies parity against IDs generated by the TS reference implementation.
Comparison
| Feature | ashid | UUID | nanoid | ULID | SecureRandom.uuid |
|---|---|---|---|---|---|
| Type prefixes | Built-in | No | No | No | No |
| Time-sortable | Yes | No | No | Yes | No |
| Human-readable encoding | Crockford Base32 | Hex | Base64 | Base32 | Hex |
| Case-insensitive | Yes | Yes | No | No | Yes |
| Lookalike correction | Yes (I→1, O→0) | No | No | No | No |
| Double-click selectable | Yes | No (hyphens) | Yes | Yes | No |
| URL-safe | Yes | Needs encoding | Yes | Yes | Needs encoding |
| Stdlib only | Yes | (gem) | (gem) | (gem) | Yes |
Inspired by
- Stripe's ID format — the
sk_,pi_,cus_prefix convention - Crockford Base32 — human-friendly encoding
- ULID — time-sortable IDs
- TypeID — type-safe, K-sortable IDs
Credits
Original ashid library by Dathan Guiley at Wilde Agency. Ruby port by Dan Kozlowski.
License
MIT — see LICENSE.txt.