Deploying Rails 8's Solid Trio to Production with a Single Database
Rails 8 ships with the Solid stack — Solid Cache, Solid Cable, and Solid Queue — replacing Redis for caching, WebSockets, and background jobs. Database-backed infrastructure with zero external dependencies.
Development was smooth. Production wasn't. This post documents the issues we ran into deploying the Solid trio to a Hatchbox-hosted app with a single PostgreSQL database, the mistakes we made debugging them, and what ultimately fixed things.
The Setup
- Rails 8 with Hotwire (Turbo + Stimulus)
- PostgreSQL 18 in production, 17 locally
- Hatchbox for deployment (single provisioned database)
- Solid Queue, Solid Cache, and Solid Cable
The app is a booking system. Admins manage bookings, and Turbo broadcasts push real-time status updates when bookings are confirmed or cancelled.
Problem 1: Missing Tables
The first error appeared on the first request that touched caching:
PG::UndefinedTable: ERROR: relation "solid_cache_entries" does not exist
What happened
When you run rails solid_cache:install, it generates:
-
config/cache.yml— the configuration -
db/cache_schema.rb— the table definition
There's no migration file. The official approach is to configure a separate cache database in database.yml and run db:prepare in production, which loads schema files for secondary databases.
Our deploy platform, Hatchbox, runs db:migrate — not db:prepare. And db:migrate only runs migration files from db/migrate/. It doesn't load schema files.
The same was true for Solid Cable and its db/cable_schema.rb.
Why it worked locally
In development, the databases had been set up with db:prepare when the project was first created. The tables existed. We never noticed they weren't created through migrations.
Problem 2: One Database, Four Configs
Our database.yml in production had separate entries for each Solid gem:
production:
primary:
url: <%= ENV["DATABASE_URL"] %>
cache:
url: <%= ENV["DATABASE_URL"] %>
migrations_paths: db/cache_migrate
cable:
url: <%= ENV["DATABASE_URL"] %>
migrations_paths: db/cable_migrate
queue:
url: <%= ENV["DATABASE_URL"] %>
migrations_paths: db/queue_migrate
This looks like four databases, but they all point to the same DATABASE_URL. Hatchbox provisions one database. We weren't running separate databases — we were running separate configurations pointing to the same database.
The migrations_paths entries pointed to folders like db/cache_migrate/ and db/cable_migrate/. These folders were empty. The actual table definitions lived in schema files, which db:migrate ignores.
What We Did
The official separate-database approach
The Solid Cache and Solid Cable READMEs recommend separate databases with db:prepare. That's the standard approach and it works well when your hosting allows provisioning multiple databases.
We couldn't do that. Hatchbox gives us one database and runs db:migrate.
The single-database workaround
Both READMEs document what to do in this case. From the Solid Cache README:
"When you omit
database,databases, orconnects_tosettings, Solid Cache automatically uses theActiveRecord::Baseconnection pool."
Solid Cable's README is more explicit:
"Copy the contents of
db/cable_schema.rbinto a normal migration and deletedb/cable_schema.rb"
This isn't the primary recommended approach — it's a workaround for single-database setups. But it's documented and it works.
Here's what we did:
1. Created regular migrations in db/migrate/:
# db/migrate/20260306054425_create_solid_cache_entries.rb
class CreateSolidCacheEntries < ActiveRecord::Migration[8.0]
def up
load Rails.root.join("db/cache_schema.rb")
end
def down
drop_table :solid_cache_entries
end
end
# db/migrate/20260306062125_create_solid_cable_messages.rb
class CreateSolidCableMessages < ActiveRecord::Migration[8.0]
def up
load Rails.root.join("db/cable_schema.rb")
end
def down
drop_table :solid_cable_messages
end
end
A note on this approach: loading the schema files uses force: :cascade, which drops and recreates the table if it already exists. For a first-time setup this is fine, but for an established database you may want to copy the table definition explicitly instead. The Solid Cable README recommends the explicit copy approach.
2. Removed separate database configs from production database.yml:
production:
primary:
<<: *default
url: <%= ENV["DATABASE_URL"] %>
queue:
<<: *default
url: <%= ENV["DATABASE_URL"] %>
migrations_paths: db/queue_migrate
We kept queue because Solid Queue ships with actual migration files in db/queue_migrate/, unlike Cache and Cable which only have schema files.
3. Removed connection directives:
# cable.yml — removed connects_to block
production:
adapter: solid_cable
polling_interval: 0.1.seconds
message_retention: 1.day
# cache.yml — removed database: cache
production:
<<: *default
With these changes, db:migrate creates the tables and both gems fall back to the primary connection pool.
Problem 3: A Misleading Error
After fixing the cache table, we hit a new error when cancelling bookings from the admin panel:
ArgumentError: No unique index found for id
The top of the stack trace pointed to our Turbo broadcast code:
def broadcast_status_update
broadcast_replace_to(
self, "status",
target: "booking_status",
partial: "bookings/status",
locals: { booking: self }
)
end
Our bookings table uses UUID primary keys. We assumed the problem was that ActiveRecord couldn't find a unique index on the UUID id column. We spent time trying various workarounds:
- Switching to
Turbo::StreamsChannel.broadcast_replace_towith string-based channel names - Rendering HTML with
ApplicationController.renderinstead of passingpartial: - Adding an explicit unique index on
bookings.id
None of it worked.
What actually happened
We eventually pulled the full stack trace from Bugsnag:
activerecord/insert_all.rb:165:in `find_unique_index_for'
...
solid_cable/message.rb:15:in `broadcast'
solid_cable/lib/action_cable/subscription_adapter/solid_cable.rb:19:in `broadcast'
...
turbo-rails/app/channels/turbo/streams/broadcasts.rb:13:in `broadcast_replace_to'
app/models/booking.rb:174:in `broadcast_status_update'
The error wasn't in our code. It was in Solid Cable. When our broadcast fired, Turbo handed the message to Action Cable, which handed it to Solid Cable, which tried to insert into the solid_cable_messages table. That table didn't exist yet — the Solid Cable migration hadn't been deployed at that point.
We're not 100% certain about the exact mechanism — whether the error was because the table was missing entirely or because the table existed without proper indexes. The error message ("No unique index found for id") suggests an index issue, but a missing table could also surface differently depending on how ActiveRecord checks for indexes. What we know for certain is that once the migration ran and the table was properly created, the error went away.
What we learned
Read the full stack trace. The error message and top frame made it look like a UUID primary key problem on our bookings table. We spent time adding indexes and rewriting broadcast code. The actual cause was several frames deep in a gem we hadn't thought to check.
It's also a reminder that errors don't always mean what they say. "No unique index found for id" led us to chase index problems when the underlying issue was a missing table.
A note on db:prepare and schema files
During debugging, we also tried switching our deploy command to db:prepare, thinking it would load the schema files. It's worth noting that there are known inconsistencies with how db:prepare handles secondary database schema files. It may not work reliably in all scenarios. The migration approach we ended up with, while not the primary recommended path, turned out to be more predictable.
There's also a known Rails issue where running db:migrate can erroneously empty secondary schema files like queue_schema.rb. Something to watch out for if you're keeping Solid Queue on a separate database config.
Verifying Everything Works
After deploying, we verified each component via the Rails console:
# SolidCache
Rails.cache.write("test_key", "hello")
Rails.cache.read("test_key")
# => "hello"
# SolidCable
SolidCable::Message.count
# => 0
# The original failing action — cancelling a booking
booking = Booking.confirmed.last
booking.soft_delete!(AdminUser.first)
# => No error
Summary
If you're deploying Rails 8's Solid trio to a single-database environment where db:migrate is your deploy command:
Check your deploy command. If it runs
db:migrate(notdb:prepare), schema files for Solid Cache and Solid Cable won't be loaded. You need regular migrations.Create migrations for Solid Cache and Solid Cable. The Solid Cable README documents this: copy the table definitions from
db/cable_schema.rbinto a standard migration. Same for Solid Cache. This is a documented workaround for single-database setups, not the primary recommended approach.Remove separate database configs from
database.ymlif you're using one database. Removeconnects_tofromcable.ymlanddatabasefromcache.yml. Both gems will fall back to the primary connection pool.Keep Solid Queue's separate config. Solid Queue ships with actual migrations in
db/queue_migrate/thatdb:migratecan find.Read the full stack trace. When debugging production errors with the Solid stack, the error origin might be several frames deep in a gem you didn't expect. Don't assume the top frame tells the whole story.
Final Thoughts
The Solid trio works well once properly set up. The default Rails 8 configuration assumes you can provision multiple databases and run db:prepare — a reasonable assumption, but not universal. If your hosting doesn't support that, the workaround is straightforward and documented, though not prominently.
We made our debugging harder by not reading the full stack trace early enough. The lesson there is universal, not specific to the Solid stack.
Top comments (0)