OpaqueId
A Ruby gem for generating cryptographically secure, collision-free opaque IDs for ActiveRecord models. OpaqueId provides a drop-in replacement for nanoid.rb
using Ruby's built-in SecureRandom
methods with optimized algorithms for unbiased distribution.
Table of Contents
- Features
- Installation
- Requirements
- Using Bundler (Recommended)
- Manual Installation
- From Source
- Troubleshooting
- Quick Start
- 1. Generate Migration and Update Model
- 2. Run Migration
- 3. Use in Your Models
- Usage
- Standalone ID Generation
- Real-World Examples
- ActiveRecord Integration
- Rails Generator
- Configuration Options
- Configuration Details
- Built-in Alphabets
-
ALPHANUMERIC_ALPHABET
(Default) STANDARD_ALPHABET
- Alphabet Comparison
- Custom Alphabets
- Alphabet Selection Guide
-
- Algorithm Details
- Fast Path Algorithm (64-character alphabets)
- Unbiased Path Algorithm (other alphabets)
- Algorithm Selection
- Performance Benchmarks
- Generation Speed (IDs per second)
- Memory Usage
- Collision Probability
- Performance Characteristics
- Real-World Performance
- Performance Optimization Tips
- Error Handling
- Security Considerations
- Cryptographic Security
- Security Best Practices
- Security Recommendations
- Threat Model Considerations
- Security Audit Checklist
- Use Cases
- E-Commerce Applications
- API Development
- Content Management Systems
- User Management
- Background Job Systems
- Short URL Services
- Real-Time Applications
- Analytics and Tracking
- Use Case Summary
- Development
- Prerequisites
- Setup
- Development Commands
- Project Structure
- Testing Strategy
- Release Process
- Development Guidelines
- Contributing
- Reporting Issues
- Issue Guidelines
- Code of Conduct
- Community Guidelines
- Getting Help
- License
- License Summary
- Full License Text
- License Compatibility
- Copyright
- Third-Party Licenses
- Legal Disclaimer
- Code of Conduct
- Acknowledgements
Features
-
๐ Cryptographically Secure: Uses Ruby's
SecureRandom
for secure ID generation - โก High Performance: Optimized algorithms with fast paths for 64-character alphabets
- ๐ฏ Collision-Free: Built-in collision detection with configurable retry attempts
- ๐ง Highly Configurable: Customizable alphabet, length, column name, and validation rules
- ๐ Rails Integration: Seamless ActiveRecord integration with automatic ID generation
-
๐ฆ Rails Generator: One-command setup with
rails generate opaque_id:install
- ๐งช Well Tested: Comprehensive test suite with statistical uniformity tests
- ๐ Rails 8.0+ Compatible: Built for modern Rails applications
Installation
Requirements
- Ruby 3.2.0 or higher
- Rails 8.0 or higher
- ActiveRecord 8.0 or higher
Using Bundler (Recommended)
Add this line to your application's Gemfile:
gem 'opaque_id'
And then execute:
bundle install
Manual Installation
If you're not using Bundler, you can install the gem directly:
gem install opaque_id
From Source
To install from the latest source:
# In your Gemfile
gem 'opaque_id', git: 'https://github.com/nyaggah/opaque_id.git'
bundle install
Troubleshooting
Rails Version Compatibility: If you're using an older version of Rails, you may need to check compatibility. OpaqueId is designed for Rails 8.0+.
Ruby Version: Ensure you're using Ruby 3.2.0 or higher. Check your version with:
ruby --version
Quick Start
1. Generate Migration and Update Model
rails generate opaque_id:install users
This will:
- Create a migration to add an
opaque_id
column with a unique index - Automatically add
include OpaqueId::Model
to yourUser
model
2. Run Migration
rails db:migrate
3. Use in Your Models
class User < ApplicationRecord
include OpaqueId::Model
end
# IDs are automatically generated on creation
user = User.create!(name: "John Doe")
puts user.opaque_id # => "V1StGXR8_Z5jdHi6B-myT"
# Find by opaque ID
user = User.find_by_opaque_id("V1StGXR8_Z5jdHi6B-myT")
user = User.find_by_opaque_id!("V1StGXR8_Z5jdHi6B-myT") # raises if not found
Usage
Standalone ID Generation
OpaqueId can be used independently of ActiveRecord for generating secure IDs in any Ruby application:
Basic Usage
# Generate with default settings (21 characters, alphanumeric)
id = OpaqueId.generate
# => "V1StGXR8_Z5jdHi6B-myT"
# Custom length
id = OpaqueId.generate(size: 10)
# => "V1StGXR8_Z5"
# Custom alphabet
id = OpaqueId.generate(alphabet: OpaqueId::STANDARD_ALPHABET)
# => "V1StGXR8_Z5jdHi6B-myT"
# Custom alphabet and length
id = OpaqueId.generate(size: 8, alphabet: "ABCDEFGH")
# => "ABCDEFGH"
# Generate multiple IDs
ids = 5.times.map { OpaqueId.generate(size: 8) }
# => ["V1StGXR8", "Z5jdHi6B", "myT12345", "ABCdefGH", "IJKlmnoP"]
Standalone Use Cases
Background Job IDs
# Generate unique job identifiers
class BackgroundJob
def self.enqueue(job_class, *args)
job_id = OpaqueId.generate(size: 12)
# Store job with unique ID
puts "Enqueued job #{job_class} with ID: #{job_id}"
job_id
end
end
job_id = BackgroundJob.enqueue(ProcessDataJob, user_id: 123)
# => "V1StGXR8_Z5jd"
Temporary File Names
# Generate unique temporary filenames
def create_temp_file(content)
temp_filename = "temp_#{OpaqueId.generate(size: 8)}.txt"
File.write(temp_filename, content)
temp_filename
end
filename = create_temp_file("Hello World")
# => "temp_V1StGXR8.txt"
Cache Keys
# Generate cache keys for different data types
class CacheManager
def self.user_cache_key(user_id)
"user:#{OpaqueId.generate(size: 6)}:#{user_id}"
end
def self.session_cache_key
"session:#{OpaqueId.generate(size: 16)}"
end
end
user_key = CacheManager.user_cache_key(123)
# => "user:V1StGX:123"
session_key = CacheManager.session_cache_key
# => "session:V1StGXR8_Z5jdHi6B"
Webhook Signatures
# Generate webhook signatures
class WebhookService
def self.generate_signature(payload)
timestamp = Time.current.to_i
nonce = OpaqueId.generate(size: 16)
signature = "#{timestamp}:#{nonce}:#{payload.hash}"
signature
end
end
signature = WebhookService.generate_signature({ user_id: 123 })
# => "1703123456:V1StGXR8_Z5jdHi6B:1234567890"
Database Migration IDs
# Generate unique migration identifiers
def create_migration(name)
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
unique_id = OpaqueId.generate(size: 4)
"#{timestamp}_#{unique_id}_#{name}"
end
migration_name = create_migration("add_user_preferences")
# => "20231221143022_V1St_add_user_preferences"
Email Tracking IDs
# Generate email tracking pixel IDs
class EmailService
def self.tracking_pixel_id
OpaqueId.generate(size: 20, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
end
end
tracking_id = EmailService.tracking_pixel_id
# => "V1StGXR8Z5jdHi6BmyT12"
# Use in email template
# <img src="https://example.com/track/#{tracking_id}" width="1" height="1" />
API Request IDs
# Generate request IDs for API logging
class ApiLogger
def self.log_request(endpoint, params)
request_id = OpaqueId.generate(size: 12)
Rails.logger.info "Request #{request_id}: #{endpoint} - #{params}"
request_id
end
end
request_id = ApiLogger.log_request("/api/users", { page: 1 })
# => "V1StGXR8_Z5jd"
Batch Processing IDs
# Generate batch processing identifiers
class BatchProcessor
def self.process_batch(items)
batch_id = OpaqueId.generate(size: 10)
puts "Processing batch #{batch_id} with #{items.count} items"
items.each_with_index do |item, index|
item_id = "#{batch_id}_#{index.to_s.rjust(3, '0')}"
puts "Processing item #{item_id}: #{item}"
end
batch_id
end
end
batch_id = BatchProcessor.process_batch([1, 2, 3, 4, 5])
# => "V1StGXR8_Z5"
# => Processing item V1StGXR8_Z5_000: 1
# => Processing item V1StGXR8_Z5_001: 2
# => ...
Real-World Examples
API Keys
# Generate secure API keys
api_key = OpaqueId.generate(size: 32, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
# => "V1StGXR8_Z5jdHi6B-myT1234567890AB"
# Store in your API key model
class ApiKey < ApplicationRecord
include OpaqueId::Model
self.opaque_id_column = :key
self.opaque_id_length = 32
end
Short URLs
# Generate short URL identifiers
short_id = OpaqueId.generate(size: 6, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
# => "V1StGX"
# Use in your URL shortener
class ShortUrl < ApplicationRecord
include OpaqueId::Model
self.opaque_id_column = :short_code
self.opaque_id_length = 6
end
File Uploads
# Generate unique filenames
filename = OpaqueId.generate(size: 12, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
# => "V1StGXR8_Z5jd"
# Use in your file upload system
class Upload < ApplicationRecord
include OpaqueId::Model
self.opaque_id_column = :filename
self.opaque_id_length = 12
end
ActiveRecord Integration
Basic Usage
class Post < ApplicationRecord
include OpaqueId::Model
end
# Create a new post - opaque_id is automatically generated
post = Post.create!(title: "Hello World", content: "This is my first post")
puts post.opaque_id # => "V1StGXR8_Z5jdHi6B-myT"
# Create multiple posts
posts = Post.create!([
{ title: "Post 1", content: "Content 1" },
{ title: "Post 2", content: "Content 2" },
{ title: "Post 3", content: "Content 3" }
])
posts.each { |p| puts "#{p.title}: #{p.opaque_id}" }
# => Post 1: V1StGXR8_Z5jdHi6B-myT
# => Post 2: Z5jdHi6B-myT12345
# => Post 3: myT12345-ABCdefGH
Custom Configuration
OpaqueId provides extensive configuration options to tailor ID generation to your specific needs:
Basic Customization
class User < ApplicationRecord
include OpaqueId::Model
# Use a different column name
self.opaque_id_column = :public_id
# Custom length and alphabet
self.opaque_id_length = 15
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
# Require ID to start with a letter
self.opaque_id_require_letter_start = true
# Remove specific characters
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l']
# Maximum retry attempts for collision resolution
self.opaque_id_max_retry = 5
end
API Key Configuration
class ApiKey < ApplicationRecord
include OpaqueId::Model
# Use 'key' as the column name
self.opaque_id_column = :key
# Longer IDs for better security
self.opaque_id_length = 32
# Alphanumeric only for API keys
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
# Remove confusing characters
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
# More retry attempts for high-volume systems
self.opaque_id_max_retry = 10
end
# Generated API keys will look like: "V1StGXR8Z5jdHi6BmyT1234567890AB"
Short URL Configuration
class ShortUrl < ApplicationRecord
include OpaqueId::Model
# Use 'code' as the column name
self.opaque_id_column = :code
# Shorter IDs for URLs
self.opaque_id_length = 6
# URL-safe characters only
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
# Remove confusing characters for better UX
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
# Require letter start for better readability
self.opaque_id_require_letter_start = true
end
# Generated short codes will look like: "V1StGX"
File Upload Configuration
class Upload < ApplicationRecord
include OpaqueId::Model
# Use 'filename' as the column name
self.opaque_id_column = :filename
# Medium length for filenames
self.opaque_id_length = 12
# Alphanumeric with hyphens for filenames
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET + "-"
# Remove problematic characters for filesystems
self.opaque_id_purge_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
end
# Generated filenames will look like: "V1StGXR8-Z5jd"
Session Token Configuration
class Session < ApplicationRecord
include OpaqueId::Model
# Use 'token' as the column name
self.opaque_id_column = :token
# Longer tokens for security
self.opaque_id_length = 24
# URL-safe characters for cookies
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
# Remove confusing characters
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
# More retry attempts for high-concurrency
self.opaque_id_max_retry = 8
end
# Generated session tokens will look like: "V1StGXR8_Z5jdHi6B-myT123"
Custom Alphabet Examples
# Numeric only
class Order < ApplicationRecord
include OpaqueId::Model
self.opaque_id_alphabet = "0123456789"
self.opaque_id_length = 8
end
# Generated: "12345678"
# Uppercase only
class Product < ApplicationRecord
include OpaqueId::Model
self.opaque_id_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
self.opaque_id_length = 6
end
# Generated: "ABCDEF"
# Custom character set
class Invite < ApplicationRecord
include OpaqueId::Model
self.opaque_id_alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No confusing chars
self.opaque_id_length = 8
end
# Generated: "ABCDEFGH"
Finder Methods
# Find by opaque ID (returns nil if not found)
user = User.find_by_opaque_id("V1StGXR8_Z5jdHi6B-myT")
if user
puts "Found user: #{user.name}"
else
puts "User not found"
end
# Find by opaque ID (raises ActiveRecord::RecordNotFound if not found)
user = User.find_by_opaque_id!("V1StGXR8_Z5jdHi6B-myT")
puts "Found user: #{user.name}"
# Use in controllers for public-facing URLs
class PostsController < ApplicationController
def show
@post = Post.find_by_opaque_id!(params[:id])
# This allows URLs like /posts/V1StGXR8_Z5jdHi6B-myT
end
end
# Use in API endpoints
class Api::UsersController < ApplicationController
def show
user = User.find_by_opaque_id(params[:id])
if user
render json: { id: user.opaque_id, name: user.name }
else
render json: { error: "User not found" }, status: 404
end
end
end
Rails Generator
Basic Usage
rails generate opaque_id:install users
Custom Column Name
rails generate opaque_id:install users --column-name=public_id
What the Generator Does
-
Creates Migration: Adds
opaque_id
column with unique index -
Updates Model: Automatically adds
include OpaqueId::Model
to your model - Handles Edge Cases: Detects if concern is already included, handles missing model files
Configuration Options
OpaqueId provides comprehensive configuration options to customize ID generation behavior:
Option | Type | Default | Description | Example Usage |
---|---|---|---|---|
opaque_id_column |
Symbol |
:opaque_id |
Column name for storing the opaque ID | self.opaque_id_column = :public_id |
opaque_id_length |
Integer |
21 |
Length of generated IDs | self.opaque_id_length = 32 |
opaque_id_alphabet |
String |
ALPHANUMERIC_ALPHABET |
Character set for ID generation | self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET |
opaque_id_require_letter_start |
Boolean |
false |
Require ID to start with a letter | self.opaque_id_require_letter_start = true |
opaque_id_purge_chars |
Array<String> |
[] |
Characters to remove from generated IDs | self.opaque_id_purge_chars = ['0', 'O', 'I', 'l'] |
opaque_id_max_retry |
Integer |
3 |
Maximum retry attempts for collision resolution | self.opaque_id_max_retry = 10 |
Configuration Details
opaque_id_column
- Purpose: Specifies the database column name for storing opaque IDs
-
Use Cases: When you want to use a different column name (e.g.,
public_id
,external_id
,key
) -
Example:
self.opaque_id_column = :public_id
โ IDs stored inpublic_id
column
opaque_id_length
- Purpose: Controls the length of generated IDs
- Range: 1 to 255 characters (practical limit)
- Performance: Longer IDs are more secure but use more storage
-
Examples:
-
6
โ Short URLs:"V1StGX"
-
21
โ Default:"V1StGXR8_Z5jdHi6B-myT"
-
32
โ API Keys:"V1StGXR8_Z5jdHi6B-myT1234567890AB"
-
opaque_id_alphabet
- Purpose: Defines the character set used for ID generation
-
Built-in Options:
ALPHANUMERIC_ALPHABET
,STANDARD_ALPHABET
- Custom: Any string of unique characters
- Security: Larger alphabets provide more entropy per character
-
Examples:
-
"0123456789"
โ Numeric only:"12345678"
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
โ Uppercase only:"ABCDEF"
-
"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
โ No confusing chars:"ABCDEFGH"
-
opaque_id_require_letter_start
- Purpose: Ensures IDs start with a letter for better readability
- Use Cases: When IDs are user-facing or need to be easily readable
- Performance: Slight overhead due to rejection sampling
-
Example:
true
โ"V1StGXR8_Z5jdHi6B-myT"
,false
โ"1StGXR8_Z5jdHi6B-myT"
opaque_id_purge_chars
- Purpose: Removes problematic characters from generated IDs
- Use Cases: Avoiding confusing characters (0/O, 1/I/l) or filesystem-unsafe chars
- Performance: Minimal overhead, applied after generation
-
Examples:
-
['0', 'O', 'I', 'l']
โ Removes visually similar characters -
['/', '\\', ':', '*', '?', '"', '<', '>', '|']
โ Removes filesystem-unsafe characters
-
opaque_id_max_retry
- Purpose: Controls collision resolution attempts
- Use Cases: High-volume systems where collisions are more likely
- Performance: Higher values provide better collision resolution but may slow down creation
-
Examples:
-
3
โ Default, good for most applications -
10
โ High-volume systems with many concurrent creations -
1
โ When you want to fail fast on collisions
-
Built-in Alphabets
OpaqueId provides two pre-configured alphabets optimized for different use cases:
ALPHANUMERIC_ALPHABET
(Default)
OpaqueId::ALPHANUMERIC_ALPHABET
# => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
Characteristics:
- Length: 62 characters
- Characters: A-Z, a-z, 0-9
- URL Safety: โ Fully URL-safe
- Readability: โ High (no confusing characters)
- Entropy: 62^n possible combinations
- Performance: โก Fast path (64-character optimization)
Best For:
- API keys and tokens
- Public-facing URLs
- User-visible identifiers
- Database primary keys
- General-purpose ID generation
Example Output:
OpaqueId.generate(size: 8, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
# => "V1StGXR8"
STANDARD_ALPHABET
OpaqueId::STANDARD_ALPHABET
# => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
Characteristics:
- Length: 64 characters
- Characters: A-Z, a-z, 0-9, -, _
- URL Safety: โ Fully URL-safe
- Readability: โ High (no confusing characters)
- Entropy: 64^n possible combinations
- Performance: โก Fast path (64-character optimization)
Best For:
- Short URLs and links
- File names and paths
- Configuration keys
- Session identifiers
- High-performance applications
Example Output:
OpaqueId.generate(size: 8, alphabet: OpaqueId::STANDARD_ALPHABET)
# => "V1StGXR8"
Alphabet Comparison
Feature | ALPHANUMERIC_ALPHABET | STANDARD_ALPHABET |
---|---|---|
Character Count | 62 | 64 |
URL Safe | โ Yes | โ Yes |
Performance | โก Fast | โก Fastest |
Entropy per Character | ~5.95 bits | 6 bits |
Collision Resistance | High | Highest |
Use Case | General purpose | High performance |
Custom Alphabets
You can also create custom alphabets for specific needs:
# Numeric only (10 characters)
numeric_alphabet = "0123456789"
OpaqueId.generate(size: 8, alphabet: numeric_alphabet)
# => "12345678"
# Uppercase only (26 characters)
uppercase_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
OpaqueId.generate(size: 6, alphabet: uppercase_alphabet)
# => "ABCDEF"
# No confusing characters (58 characters)
safe_alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz23456789"
OpaqueId.generate(size: 8, alphabet: safe_alphabet)
# => "ABCDEFGH"
# Filesystem safe (63 characters)
filesystem_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
OpaqueId.generate(size: 12, alphabet: filesystem_alphabet)
# => "V1StGXR8_Z5jd"
# Base64-like (64 characters)
base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
OpaqueId.generate(size: 16, alphabet: base64_alphabet)
# => "V1StGXR8/Z5jdHi6B"
Alphabet Selection Guide
Choose ALPHANUMERIC_ALPHABET
when:
- Building APIs or web services
- IDs will be user-visible
- You need maximum compatibility
- General-purpose ID generation
Choose STANDARD_ALPHABET
when:
- Building high-performance applications
- Creating short URLs or links
- You need maximum entropy
- File names or paths are involved
Create custom alphabets when:
- You need specific character sets
- Avoiding certain characters (0/O, 1/I/l)
- Working with legacy systems
- Special formatting requirements
Algorithm Details
OpaqueId implements two optimized algorithms for secure ID generation, automatically selecting the best approach based on alphabet size:
Fast Path Algorithm (64-character alphabets)
When using 64-character alphabets (like STANDARD_ALPHABET
), OpaqueId uses an optimized bitwise approach:
# Simplified algorithm for 64-character alphabets
def generate_fast(size, alphabet)
result = ""
size.times do
# Get random byte from SecureRandom
byte = SecureRandom.random_number(256)
# Use bitwise AND to get index 0-63
index = byte & 63
result << alphabet[index]
end
result
end
Advantages:
- โก Maximum Performance: Direct bitwise operations, no rejection sampling
- ๐ฏ Perfect Distribution: Each character has exactly 1/64 probability
- ๐ Cryptographically Secure: Uses
SecureRandom
as entropy source - ๐ Predictable Performance: Constant time complexity O(n)
Why 64 characters?
- 64 = 2^6, allowing efficient bitwise operations
-
byte & 63
extracts exactly 6 bits (0-63 range) - No modulo bias since 256 is divisible by 64
Unbiased Path Algorithm (other alphabets)
For alphabets with sizes other than 64, OpaqueId uses rejection sampling:
# Simplified algorithm for non-64-character alphabets
def generate_unbiased(size, alphabet, alphabet_size)
result = ""
size.times do
loop do
# Get random byte
byte = SecureRandom.random_number(256)
# Calculate index using modulo
index = byte % alphabet_size
# Check if within unbiased range
if byte < (256 / alphabet_size) * alphabet_size
result << alphabet[index]
break
end
# Reject and try again (rare occurrence)
end
end
result
end
Advantages:
- ๐ฏ Perfect Uniformity: Eliminates modulo bias through rejection sampling
- ๐ Cryptographically Secure: Uses
SecureRandom
as entropy source - ๐ง Flexible: Works with any alphabet size
- ๐ Statistically Sound: Mathematically proven unbiased distribution
Rejection Sampling Explained:
- When
byte % alphabet_size
would create bias, the byte is rejected - Only bytes in the "unbiased range" are used
- Rejection rate is minimal (typically <1% for common alphabet sizes)
Algorithm Selection
def generate(size:, alphabet:)
alphabet_size = alphabet.size
if alphabet_size == 64
generate_fast(size, alphabet) # Fast path
else
generate_unbiased(size, alphabet, alphabet_size) # Unbiased path
end
end
Performance Benchmarks
Generation Speed (IDs per second)
Alphabet Size | Algorithm | Performance | Relative Speed |
---|---|---|---|
64 characters | Fast Path | ~2,500,000 IDs/sec | 100% (baseline) |
62 characters | Unbiased | ~1,200,000 IDs/sec | 48% |
36 characters | Unbiased | ~1,100,000 IDs/sec | 44% |
26 characters | Unbiased | ~1,000,000 IDs/sec | 40% |
10 characters | Unbiased | ~900,000 IDs/sec | 36% |
Benchmarks run on Ruby 3.2.0, generating 21-character IDs
Memory Usage
Algorithm | Memory per ID | Memory per 1M IDs |
---|---|---|
Fast Path | ~21 bytes | ~21 MB |
Unbiased | ~21 bytes | ~21 MB |
Memory usage is consistent regardless of algorithm choice
Collision Probability
For 21-character IDs with different alphabets:
Alphabet | Characters | Collision Probability (1 in) |
---|---|---|
STANDARD_ALPHABET | 64 | 2.9 ร 10^37 |
ALPHANUMERIC_ALPHABET | 62 | 1.4 ร 10^37 |
Numeric (0-9) | 10 | 1.0 ร 10^21 |
Binary (0-1) | 2 | 2.1 ร 10^6 |
Collision probability calculated using birthday paradox formula
Performance Characteristics
Fast Path (64-character alphabets)
- Time Complexity: O(n) where n = ID length
- Space Complexity: O(n)
- Rejection Rate: 0% (no rejections)
- Distribution: Perfect uniform
- Best For: High-performance applications, short URLs
Unbiased Path (other alphabets)
- Time Complexity: O(n ร (1 + rejection_rate)) where rejection_rate โ 0.01
- Space Complexity: O(n)
- Rejection Rate: <1% for most alphabet sizes
- Distribution: Perfect uniform (mathematically proven)
- Best For: General-purpose applications, custom alphabets
Real-World Performance
# Benchmark example
require 'benchmark'
# Fast path (STANDARD_ALPHABET - 64 characters)
Benchmark.measure do
1_000_000.times { OpaqueId.generate(size: 21, alphabet: OpaqueId::STANDARD_ALPHABET) }
end
# => 0.400000 seconds
# Unbiased path (ALPHANUMERIC_ALPHABET - 62 characters)
Benchmark.measure do
1_000_000.times { OpaqueId.generate(size: 21, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET) }
end
# => 0.830000 seconds
Performance Optimization Tips
- Use 64-character alphabets when possible for maximum speed
-
Prefer
STANDARD_ALPHABET
overALPHANUMERIC_ALPHABET
for performance-critical applications - Batch generation is more efficient than individual calls
- Avoid very small alphabets (2-10 characters) for high-volume applications
- Consider ID length - longer IDs take proportionally more time
Error Handling
# Invalid size
OpaqueId.generate(size: 0)
# => raises OpaqueId::ConfigurationError
# Empty alphabet
OpaqueId.generate(alphabet: "")
# => raises OpaqueId::ConfigurationError
# Collision resolution failure
# => raises OpaqueId::GenerationError after max retry attempts
Security Considerations
Cryptographic Security
OpaqueId is designed with security as a primary concern:
-
๐ Cryptographically Secure: Uses Ruby's
SecureRandom
for entropy generation - ๐ฏ Unbiased Distribution: Implements rejection sampling to eliminate modulo bias
- ๐ซ Non-Sequential: IDs are unpredictable and don't reveal creation order
- ๐ Collision Resistant: Automatic collision detection and resolution
- ๐ Statistically Sound: Mathematically proven uniform distribution
Security Best Practices
โ DO Use OpaqueId For:
- Public-facing identifiers (user IDs, post IDs, order numbers)
- API keys and authentication tokens
- Session identifiers and CSRF tokens
- File upload names and temporary URLs
- Webhook signatures and verification tokens
- Database migration identifiers
- Cache keys and job identifiers
โ DON'T Use OpaqueId For:
- Passwords or password hashes (use proper password hashing)
- Encryption keys (use dedicated key generation libraries)
- Sensitive data (IDs are not encrypted, just opaque)
- Sequential operations (where order matters)
- Very short IDs (less than 8 characters for security-critical use cases)
Security Recommendations
ID Length Guidelines
Use Case | Minimum Length | Recommended Length | Reasoning |
---|---|---|---|
Public URLs | 8 characters | 12-16 characters | Balance security vs. URL length |
API Keys | 16 characters | 21+ characters | High security requirements |
Session Tokens | 21 characters | 21+ characters | Standard security practice |
File Names | 8 characters | 12+ characters | Prevent enumeration attacks |
Database IDs | 12 characters | 16+ characters | Long-term security |
Alphabet Selection for Security
-
STANDARD_ALPHABET
: Best for high-security applications (64 characters = 6 bits entropy per character) -
ALPHANUMERIC_ALPHABET
: Good for general use (62 characters = ~5.95 bits entropy per character) - Custom alphabets: Avoid very small alphabets (< 16 characters) for security-critical use cases
Entropy Calculations
For 21-character IDs:
- STANDARD_ALPHABET: 2^126 โ 8.5 ร 10^37 possible combinations
- ALPHANUMERIC_ALPHABET: 2^124 โ 2.1 ร 10^37 possible combinations
- Numeric (0-9): 2^70 โ 1.2 ร 10^21 possible combinations
Threat Model Considerations
Information Disclosure
- โ OpaqueId prevents: Sequential ID enumeration, creation time inference
- โ ๏ธ OpaqueId doesn't prevent: ID guessing (use proper authentication)
Brute Force Attacks
- Protection: Extremely large ID space makes brute force impractical
- Recommendation: Combine with rate limiting and authentication
Timing Attacks
- Protection: Constant-time generation algorithms
- Recommendation: Use consistent ID lengths to prevent timing analysis
Security Audit Checklist
When implementing OpaqueId in security-critical applications:
- ID Length: Using appropriate length for threat model
- Alphabet Choice: Using alphabet with sufficient entropy
- Collision Handling: Proper error handling for rare collisions
- Rate Limiting: Implementing rate limits on ID-based endpoints
- Authentication: Proper authentication before ID-based operations
- Logging: Not logging sensitive IDs in plain text
- Database Indexing: Proper indexing for performance and security
- Error Messages: Not revealing ID existence in error messages
Use Cases
E-Commerce Applications
Order Management
class Order < ApplicationRecord
include OpaqueId::Model
# Generate secure order numbers
opaque_id_length 16
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
end
# Usage
order = Order.create!(customer_id: 123, total: 99.99)
# => #<Order id: 1, opaque_id: "K8mN2pQ7rS9tU3vW", ...>
# Public-facing order tracking
# https://store.com/orders/K8mN2pQ7rS9tU3vW
Product Catalog
class Product < ApplicationRecord
include OpaqueId::Model
# Shorter IDs for product URLs
opaque_id_length 12
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
product = Product.create!(name: "Wireless Headphones", price: 199.99)
# => #<Product id: 1, opaque_id: "aB3dE6fG9hI", ...>
# SEO-friendly product URLs
# https://store.com/products/aB3dE6fG9hI
API Development
API Key Management
class ApiKey < ApplicationRecord
include OpaqueId::Model
# Long, secure API keys
opaque_id_length 32
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
opaque_id_require_letter_start true # Start with letter for readability
end
# Usage
api_key = ApiKey.create!(user_id: 123, name: "Production API")
# => #<ApiKey id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5xY1zA4bC6dE8fG", ...>
# API authentication
# Authorization: Bearer K8mN2pQ7rS9tU3vW5xY1zA4bC6dE8fG
Webhook Signatures
class WebhookEvent < ApplicationRecord
include OpaqueId::Model
# Unique event identifiers
opaque_id_length 21
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
event = WebhookEvent.create!(
event_type: "payment.completed",
payload: { order_id: "K8mN2pQ7rS9tU3vW" }
)
# => #<WebhookEvent id: 1, opaque_id: "aB3dE6fG9hI2jK5lM8nP", ...>
# Webhook delivery
# POST https://client.com/webhooks
# X-Event-ID: aB3dE6fG9hI2jK5lM8nP
Content Management Systems
Blog Posts
class Post < ApplicationRecord
include OpaqueId::Model
# Medium-length IDs for blog URLs
opaque_id_length 14
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
post = Post.create!(title: "Getting Started with OpaqueId", content: "...")
# => #<Post id: 1, opaque_id: "aB3dE6fG9hI2jK", ...>
# Clean blog URLs
# https://blog.com/posts/aB3dE6fG9hI2jK
File Uploads
class Attachment < ApplicationRecord
include OpaqueId::Model
# Secure file identifiers
opaque_id_length 16
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
end
# Usage
attachment = Attachment.create!(
filename: "document.pdf",
content_type: "application/pdf"
)
# => #<Attachment id: 1, opaque_id: "K8mN2pQ7rS9tU3vW", ...>
# Secure file access
# https://cdn.example.com/files/K8mN2pQ7rS9tU3vW
User Management
User Profiles
class User < ApplicationRecord
include OpaqueId::Model
# Public user identifiers
opaque_id_length 12
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
user = User.create!(email: "user@example.com", name: "John Doe")
# => #<User id: 1, opaque_id: "aB3dE6fG9hI2", ...>
# Public profile URLs
# https://social.com/users/aB3dE6fG9hI2
Session Management
class Session < ApplicationRecord
include OpaqueId::Model
# Secure session tokens
opaque_id_length 21
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
session = Session.create!(user_id: 123, expires_at: 1.week.from_now)
# => #<Session id: 1, opaque_id: "aB3dE6fG9hI2jK5lM8nP", ...>
# Session cookie
# session_token=aB3dE6fG9hI2jK5lM8nP
Background Job Systems
Job Tracking
class Job < ApplicationRecord
include OpaqueId::Model
# Unique job identifiers
opaque_id_length 18
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
end
# Usage
job = Job.create!(
job_type: "email_delivery",
status: "pending",
payload: { user_id: 123, template: "welcome" }
)
# => #<Job id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5x", ...>
# Job status API
# GET /api/jobs/K8mN2pQ7rS9tU3vW5x/status
Short URL Services
URL Shortening
class ShortUrl < ApplicationRecord
include OpaqueId::Model
# Very short IDs for URL shortening
opaque_id_length 6
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
short_url = ShortUrl.create!(
original_url: "https://very-long-url.com/path/to/resource",
user_id: 123
)
# => #<ShortUrl id: 1, opaque_id: "aB3dE6", ...>
# Short URL
# https://short.ly/aB3dE6
Real-Time Applications
Chat Rooms
class ChatRoom < ApplicationRecord
include OpaqueId::Model
# Medium-length room identifiers
opaque_id_length 10
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
end
# Usage
room = ChatRoom.create!(name: "General Discussion", owner_id: 123)
# => #<ChatRoom id: 1, opaque_id: "aB3dE6fG9h", ...>
# WebSocket connection
# ws://chat.example.com/rooms/aB3dE6fG9h
Analytics and Tracking
Event Tracking
class AnalyticsEvent < ApplicationRecord
include OpaqueId::Model
# Unique event identifiers
opaque_id_length 20
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
end
# Usage
event = AnalyticsEvent.create!(
event_type: "page_view",
user_id: 123,
properties: { page: "/products", referrer: "google.com" }
)
# => #<AnalyticsEvent id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5xY1", ...>
# Event tracking pixel
# <img src="/track/K8mN2pQ7rS9tU3vW5xY1" />
Use Case Summary
Use Case | ID Length | Alphabet | Reasoning |
---|---|---|---|
Order Numbers | 16 chars | Alphanumeric | Balance security vs. readability |
Product URLs | 12 chars | Standard | SEO-friendly, secure |
API Keys | 32 chars | Alphanumeric | High security, letter start |
Webhook Events | 21 chars | Standard | Standard security practice |
Blog Posts | 14 chars | Standard | Clean URLs, good security |
File Uploads | 16 chars | Alphanumeric | Secure, collision-resistant |
User Profiles | 12 chars | Standard | Public-facing, secure |
Sessions | 21 chars | Standard | High security requirement |
Background Jobs | 18 chars | Alphanumeric | Unique, trackable |
Short URLs | 6 chars | Standard | Very short, still secure |
Chat Rooms | 10 chars | Standard | Medium length, secure |
Analytics | 20 chars | Alphanumeric | Unique, high volume |
Development
Prerequisites
- Ruby: 3.2.0 or higher
- Rails: 8.0 or higher (for generator testing)
- Bundler: Latest version
Setup
-
Clone the repository:
git clone https://github.com/nyaggah/opaque_id.git cd opaque_id
-
Install dependencies:
bundle install
-
Run the setup script:
bin/setup
Development Commands
Testing
# Run all tests
bundle exec rake test
# Run specific test files
bundle exec ruby -Itest test/opaque_id_test.rb
bundle exec ruby -Itest test/opaque_id/model_test.rb
bundle exec ruby -Itest test/opaque_id/generators/install_generator_test.rb
# Run tests with verbose output
bundle exec rake test TESTOPTS="--verbose"
Code Quality
# Run RuboCop linter
bundle exec rubocop
# Auto-correct RuboCop offenses
bundle exec rubocop -a
# Run RuboCop on specific files
bundle exec rubocop lib/opaque_id.rb
Interactive Development
# Start interactive console
bin/console
# Example usage in console:
# OpaqueId.generate
# OpaqueId.generate(size: 10, alphabet: OpaqueId::STANDARD_ALPHABET)
Local Installation
# Install gem locally for testing
bundle exec rake install
# Uninstall local version
gem uninstall opaque_id
Project Structure
opaque_id/
โโโ lib/
โ โโโ opaque_id.rb # Main module and core functionality
โ โโโ opaque_id/
โ โ โโโ model.rb # ActiveRecord concern
โ โ โโโ version.rb # Version constant
โ โโโ generators/
โ โโโ opaque_id/
โ โโโ install_generator.rb
โ โโโ templates/
โ โโโ migration.rb.tt
โโโ test/
โ โโโ opaque_id_test.rb # Core module tests
โ โโโ opaque_id/
โ โ โโโ model_test.rb # Model concern tests
โ โ โโโ generators/
โ โ โโโ install_generator_test.rb
โ โโโ test_helper.rb # Test configuration
โโโ tasks/ # Project management and documentation
โโโ opaque_id.gemspec # Gem specification
โโโ Gemfile # Development dependencies
โโโ Rakefile # Rake tasks
โโโ README.md # This file
Testing Strategy
Test Coverage
- Core Module: ID generation, error handling, edge cases
- ActiveRecord Integration: Model callbacks, finder methods, configuration
- Rails Generator: Migration generation, model modification
- Performance: Statistical uniformity, benchmark tests
- Error Handling: Invalid inputs, collision scenarios
Test Database
- Uses in-memory SQLite for fast, isolated testing
- No external database dependencies
- Automatic cleanup between tests
Release Process
Version Management
-
Update version in
lib/opaque_id/version.rb
- Update CHANGELOG.md with new features/fixes
- Run tests to ensure everything works
- Commit changes with conventional commit message
- Create release using rake task
Release Commands
# Build and release gem
bundle exec rake release
# This will:
# 1. Build the gem
# 2. Create a git tag
# 3. Push to GitHub
# 4. Push to RubyGems
Development Guidelines
Code Style
- Follow RuboCop configuration
- Use conventional commit messages
- Write comprehensive tests for new features
- Document public APIs with examples
Git Workflow
- Use feature branches for development
- Write descriptive commit messages
- Keep commits focused and atomic
- Test before committing
Performance Considerations
- Benchmark new features
- Consider memory usage for high-volume scenarios
- Test with various alphabet sizes
- Validate statistical properties
Contributing
Reporting Issues
We welcome bug reports and feature requests! Please help us improve OpaqueId by reporting issues on GitHub:
- ๐ Bug Reports: Create an issue
- ๐ก Feature Requests: Create an issue
- ๐ Documentation: Create an issue
Issue Guidelines
When reporting issues, please include:
For Bug Reports
-
Ruby version:
ruby --version
-
Rails version:
rails --version
(if applicable) -
OpaqueId version:
gem list opaque_id
- Steps to reproduce: Clear, minimal steps
- Expected behavior: What should happen
- Actual behavior: What actually happens
- Error messages: Full error output
- Code example: Minimal code that reproduces the issue
For Feature Requests
- Use case: Why is this feature needed?
- Proposed solution: How should it work?
- Alternatives considered: What other approaches were considered?
- Additional context: Any other relevant information
Code of Conduct
This project is intended to be a safe, welcoming space for collaboration. Everyone interacting in the OpaqueId project's codebases, issue trackers, and community spaces is expected to follow the Code of Conduct.
Community Guidelines
- Be respectful: Treat everyone with respect and kindness
- Be constructive: Provide helpful feedback and suggestions
- Be patient: Maintainers are volunteers with limited time
- Be specific: Provide clear, detailed information in issues
- Be collaborative: Work together to solve problems
Getting Help
- Documentation: Check this README and inline code documentation
- Issues: Search existing issues before creating new ones
- Discussions: Use GitHub Discussions for questions and general discussion
License
OpaqueId is released under the MIT License. This is a permissive open source license that allows you to use, modify, and distribute the software with minimal restrictions.
License Summary
You are free to:
- โ Use OpaqueId in commercial and non-commercial projects
- โ Modify the source code to suit your needs
- โ Distribute copies of the software
- โ Include OpaqueId in proprietary applications
- โ Sell products that include OpaqueId
You must:
- ๐ Include the original copyright notice and license text
- ๐ Include the license in any distribution of the software
You are not required to:
- โ Share your modifications (though contributions are welcome)
- โ Use the same license for your project
- โ Provide source code for your application
Full License Text
The complete MIT License text is available in the LICENSE.txt file in this repository.
License Compatibility
The MIT License is compatible with:
- GPL: Can be included in GPL projects
- Apache 2.0: Compatible with Apache-licensed projects
- BSD: Compatible with BSD-licensed projects
- Commercial: Can be used in proprietary, commercial software
Copyright
Copyright (c) 2025 Joey Doey. All rights reserved.
Third-Party Licenses
OpaqueId uses the following dependencies:
- ActiveRecord: MIT License
- ActiveSupport: MIT License
- SecureRandom: Part of Ruby standard library (Ruby License)
Legal Disclaimer
This software is provided "as is" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
Code of Conduct
Everyone interacting in the OpaqueId project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Acknowledgements
OpaqueId is heavily inspired by nanoid.rb, which is a Ruby implementation of the original NanoID project. The core algorithm and approach to secure ID generation draws from the excellent work done by the NanoID team.
The motivation and use case for OpaqueId was inspired by the insights shared in "Why we chose NanoIDs for PlanetScale's API" by Mike Coutermarsh, which highlights the benefits of using opaque, non-sequential identifiers in modern web applications.
We're grateful to the open source community for these foundational contributions that made OpaqueId possible.