UUID’s in Rails + SQLite shouldn’t be this hard (so I built a gem)

Published: (December 22, 2025 at 07:07 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

TL;DR – UUIDs / ULIDs in a Rails + SQLite app

# Gemfile
gem "sqlite_crypto"
# migration
create_table :users, id: :uuid do |t|
  t.string :email
  t.timestamps
end

That’s it.
Foreign keys are auto‑detected, schema.rb stays clean, everything just works.

[→ GitHub] | [→ RubyGems]

The problem I ran into

I was building a Rails 8 app that used SQLite as its production database (thanks to the new WAL‑mode defaults, better busy‑handler handling, etc.). When I tried to add UUID primary keys, SQLite behaved nothing like PostgreSQL.

What broke

IssuePostgreSQLSQLite (before the gem)
Primary‑key declarationenable_extension 'pgcrypto'id: :uuidschema.rb dumped id: false and a string column
Foreign keysAuto‑detect the UUID typeCreated an INTEGER column, causing mismatched joins
User.firstReturns the chronologically first recordReturns a random UUID‑ordered record (UUID v4 is not time‑sortable)
BoilerplateNoneLots of manual type: :string, limit: 36 and custom generators

What I had to do before writing the gem

  1. Verbose migration syntax

    create_table :users, id: false do |t|
      t.string :id, limit: 36, null: false, primary_key: true
      t.string :email
      t.timestamps
    end
  2. Manual type specification on every foreign key

    create_table :api_keys, id: false do |t|
      t.string :id, limit: 36, null: false, primary_key: true
      t.references :user, null: false, foreign_key: true,
                        type: :string, limit: 36
    end
  3. Custom UUID generation in ApplicationRecord

    class ApplicationRecord
      # example UUID
      "550e8400-e29b-41d4-a716-446655440000"
    end
    
    user.tracking_id  #=> "01ARZ3NDEKTSV4RRFFQ69G5FAV"

Benchmarks

If you’re curious, I prepared a spec especially for checking each ID type’s performance. Run it on your own hardware:

bundle exec rspec --tag performance

Registering custom types

ActiveRecord::Type.register(:uuid, SqliteCrypto::Type::Uuid, adapter: :sqlite3)

The hard part was getting the schema dumper to output clean id: :uuid instead of verbose column definitions. That required prepending modules at exactly the right point in Rails’ initialization sequence.

CI matrix

My CI matrix tests against:

  • Ruby 3.1 – 3.4
  • Rails 7.1 – 8.1

I discovered edge cases that only appear in specific combinations—Rails 8.0’s schema dumper behaved differently than 7.2’s in subtle ways.

Installation

# Gemfile
gem "sqlite_crypto"

If you hit issues, open a GitHub issue. If it helps your project, consider starring the repo—it helps others discover the gem.

Contributing

If you’ve been thinking about contributing to the Ruby ecosystem but haven’t started — I encourage you to do it. Building sqlite_crypto taught me more about Rails internals than years of application development. The community needs tools, and you might be the person to build the next one.

If you see gaps in your Rails + SQLite workflow, feel free to share them with me. I’m genuinely curious about other pain points in this new SQLite‑first world.

Get in touch

Building something with sqlite_crypto? I’d love to hear about it. Drop a comment or find me on GitHub.

Back to Blog

Related posts

Read more »

Ruby website redesigned

Article URL: https://www.ruby-lang.org/en/ Comments URL: https://news.ycombinator.com/item?id=46342859 Points: 151 Comments: 39...