Better IDs: Switching from UUID to NanoID
Why BlueRobin moved from standard UUIDs to NanoIDs for primary keys: URL aesthetics, database considerations, and a custom alphabet implementation.
Introduction
Unique Identifiers are the spine of any database schema. For years, the UUID (Universally Unique Identifier) has been the de facto standard. It’s robust, collision-resistant, and natively supported by PostgreSQL.
However, as we built BlueRobin’s public-facing document sharing features, we realized that UUIDs (550e8400-e29b-41d4-a716-446655440000) are unwieldy, hard to select by double-clicking, and ugly in URLs. We switched to NanoID.
Why NanoID?
- URL Friendly: Shorter, safer characters.
- Density: Carries more information in fewer bytes.
- Customizable: We can restrict the alphabet to avoid ambiguity (e.g., confusing
land1).
What We’ll Build
In this guide, we will implement a custom ID generator for BlueRobin. You will learn how to:
- Compare ID formats: Visual and technical differences.
- Implement Custom Alphabet: Creating
BlueRobinId(lowercase alphanumeric). - Handle Database Integration: Storing NanoIDS efficiently in PostgreSQL.
Architecture Overview
The ID generation flow is straightforward but critical to understand:
flowchart LR
subgraph Application["🔌 Application Layer"]
Request["📥 Create Entity"]
Factory["🏭 BlueRobinId.Generate()"]
end
subgraph Generator["🔧 NanoID Generator"]
Alphabet["🔤 Custom Alphabet\n(0-9, a-z)"]
Length["📏 8 Characters"]
Crypto["🔒 Crypto RNG"]
end
subgraph Storage["💾 PostgreSQL"]
Column["📊 CHAR(8)\nPrimary Key"]
Index["🔍 B-Tree Index"]
end
Request --> Factory
Factory --> Alphabet
Alphabet --> Length
Length --> Crypto
Crypto --> Column
Column --> Index
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class Application primary
class Generator secondary
class Storage db
Key Components:
- Factory Method:
BlueRobinId.Generate()encapsulates ID creation - Custom Alphabet: 36 characters (lowercase + digits) for URL-friendliness
- Cryptographic RNG: Ensures unpredictable, secure IDs
- Fixed-Width Storage:
CHAR(8)optimizes PostgreSQL indexing
Visual Comparison
Let’s look at the difference in a real-world URL context.
flowchart LR
subgraph UUID ["UUID (Standard)"]
U["/docs/550e8400-e29b-41d4-a716-446655440000"]
end
subgraph NanoID ["NanoID (BlueRobin)"]
N["/docs/abc12345"]
end
UUID --> NanoID
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class UUID db
class NanoID primary
The NanoID is significantly more compact while retaining sufficient entropy for our scale.
Section 1: The NanoID Standard vs. Request
Standard NanoID uses A-Za-z0-9_-. This is great, but it includes case sensitivity (a vs A) and special characters (-, _).
For BlueRobin, we wanted lowercase alphanumeric only for maximum readability and ease of typing on mobile devices.
Custom Alphabet Implementation
We use the NanoId library in .NET but configure a custom alphabet.
using NanoidDotNet;
public static class BlueRobinId
{
// Removing look-alikes if desired, but here we use simple lowercase alpha-num
private const string Alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
private const int Length = 8; // Short, yet collision resistant enough for per-user scope
public static string Generate()
{
return Nanoid.Generate(Alphabet, Length);
}
}
Section 2: Database Storage
In PostgreSQL, a UUID is stored as a 128-bit integer (uuid type). Ideally, we might want to store our ID as text, or char(8).
CREATE TABLE documents (
-- Fixed length is more efficient than TEXT
id CHAR(8) PRIMARY KEY,
-- Owner establishes the scope for collision checking
owner_id CHAR(8) REFERENCES users(id),
title TEXT NOT NULL
);
Performance Consideration
Since NanoIDs are random strings, inserting them into a B-Tree index (like a Primary Key) can cause fragmentation compared to sequential IDs (like UUIDv7 or Integers).
However, BlueRobinId is primarily used for lookup, not sorting. The cost of random insertion is a trade-off we accept for UX. For strictly internal, high-volume logging tables, we stick to UUIDv7 or BIGINT.
Conclusion
Switching to NanoID gave our application a more polished feel. URLs are cleaner, shareable, and less intimidating to non-technical users.
Next Steps: