ashid

Gem Version Test License: MIT

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.timestamp(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: 0 to 35_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.timestamp(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:

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

Credits

Original ashid library by Dathan Guiley at Wilde Agency. Ruby port by Dan Kozlowski.

License

MIT — see LICENSE.txt.