UUID’s in Rails + SQLite shouldn’t be this hard (so I built a gem)
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
| Issue | PostgreSQL | SQLite (before the gem) |
|---|---|---|
| Primary‑key declaration | enable_extension 'pgcrypto' → id: :uuid | schema.rb dumped id: false and a string column |
| Foreign keys | Auto‑detect the UUID type | Created an INTEGER column, causing mismatched joins |
User.first | Returns the chronologically first record | Returns a random UUID‑ordered record (UUID v4 is not time‑sortable) |
| Boilerplate | None | Lots of manual type: :string, limit: 36 and custom generators |
What I had to do before writing the gem
-
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 -
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 -
Custom UUID generation in
ApplicationRecordclass 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.
Links
- GitHub Repository – https://github.com/yourname/sqlite_crypto
- RubyGems – https://rubygems.org/gems/sqlite_crypto
- Changelog – https://github.com/yourname/sqlite_crypto/blob/main/CHANGELOG.md
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.