# Rails + Hotwire

Database-backed products with HTML, accounts and jobs close together.

Rails · Turbo · Stimulus · PostgreSQL

[Complete guide (Markdown)](/stacks/rails-hotwire-postgresql/guide.md) · [Structured record (JSON)](/stacks/rails-hotwire-postgresql/record.json)

Original source review: 2026-10-10. Core guidance rechecked: 2026-10-13. [Check scope](/research-freshness.json). Dependency combinations and setup need project-specific qualification.

## Use cases

- SaaS

- Business CRUD

- Collaboration and workflow products

## Choose it when

- An integrated monolith and server-owned UI fit the product.

- A small team benefits from stable conventions and limited integration work.

## Consider another path when

- The core is an offline-first canvas or requires extensive browser-owned state.

- Hard CPU/latency constraints justify a specialist service.

## Working practices

- Keep Turbo and Stimulus focused on interface behavior; put authorization in explicit policies.

- Run migrations and exercise the generated authentication flow before building product features around it.

- Treat Solid Queue workers as operated processes. Make retries safe and verify recovery after interruption.

- Use system tests for the critical browser journey and rehearse backups and restore before shipping.

## Tools by job

### UI: ERB + Turbo + Stimulus

Server-rendered HTML and selective JavaScript limit duplicated UI state.

Do not force Hotwire onto rich offline editors.

- [https://hotwired.dev/](https://hotwired.dev/)

### Database and migrations: PostgreSQL + Active Record migrations

Relational constraints and conventional migrations fit business data.

Use unique/foreign-key constraints; assess locks and use expand/contract changes for rolling deploys.

- [https://guides.rubyonrails.org/active_record_migrations.html](https://guides.rubyonrails.org/active_record_migrations.html)

### Authentication: Rails authentication generator

Generated local sessions plus explicit per-resource authorization.

Generator is a starting point; signup, authorization and account lifecycle require product decisions.

- [https://guides.rubyonrails.org/getting_started.html](https://guides.rubyonrails.org/getting_started.html)

- [https://github.com/varvet/pundit](https://github.com/varvet/pundit)

- [https://github.com/heartcombo/devise](https://github.com/heartcombo/devise)

### Authorization: Pundit policy classes

Generated local sessions plus explicit per-resource authorization.

Generator is a starting point; signup, authorization and account lifecycle require product decisions.

- [https://guides.rubyonrails.org/getting_started.html](https://guides.rubyonrails.org/getting_started.html)

- [https://github.com/varvet/pundit](https://github.com/varvet/pundit)

- [https://github.com/heartcombo/devise](https://github.com/heartcombo/devise)

### Jobs: Active Job + Solid Queue

Database-backed jobs avoid a mandatory separate broker.

Configure worker capacity, retry limits and idempotency; separate queue DB does not guarantee atomic app-write/enqueue.

- [https://guides.rubyonrails.org/active_job_basics.html](https://guides.rubyonrails.org/active_job_basics.html)

### Tests: Minitest + Rails integration/system tests

Exercise authorization, database constraints and essential browser flows.

Browser tests need their driver/browser; unit tests alone miss UI and transaction races.

- [https://guides.rubyonrails.org/testing.html](https://guides.rubyonrails.org/testing.html)

### Lint and security: RuboCop + Brakeman + bundler-audit

Style, application scanning and dependency advisories cover different risks.

Scans do not establish security; test tenant isolation and unsafe uploads. bundler-audit has GPL-3.0 licensing.

- [https://guides.rubyonrails.org/getting_started.html](https://guides.rubyonrails.org/getting_started.html)

- [https://guides.rubyonrails.org/security.html](https://guides.rubyonrails.org/security.html)

- [https://github.com/rubysec/bundler-audit](https://github.com/rubysec/bundler-audit)

### Deployment: Kamal + generated Dockerfile + PostgreSQL backups

Portable container deployment with application-owned configuration.

Requires registry/server credentials and persistent data strategy; backups and restore drills remain operator work.

- [https://kamal-deploy.org/docs/installation/](https://kamal-deploy.org/docs/installation/)

- [https://guides.rubyonrails.org/getting_started.html](https://guides.rubyonrails.org/getting_started.html)

### Agent tooling: Generators + routes/schema/test feedback + version-matched Rails guides

Conventions make edits easier to locate and verify.

Agent productivity is an inference; no cross-framework coding-success benchmark established.

- [https://rubyonrails.org/ai](https://rubyonrails.org/ai)

### Licensing and hosting: Rails MIT; self-hostable

No mandatory proprietary Rails hosting service.

Dependency licenses remain separate; do not infer all gems are MIT.

- [https://github.com/rails/rails/blob/main/MIT-LICENSE](https://github.com/rails/rails/blob/main/MIT-LICENSE)

### Transactional email: Action Mailer

Framework mailers integrate templates and queued delivery

Configure provider transport and sender; use deliver_later when delivery must be queued; test content and failure behavior. Provider acceptance is not inbox delivery.

- [https://guides.rubyonrails.org/action_mailer_basics.html](https://guides.rubyonrails.org/action_mailer_basics.html)

### Uploads and object storage: Active Storage

Framework attachments support external storage and direct uploads

Run the installed-version Active Storage generator/migrations; configure private storage, authorization, limits and cleanup. Public blob URLs and user-supplied files need an explicit access policy.

- [https://guides.rubyonrails.org/active_storage_overview.html](https://guides.rubyonrails.org/active_storage_overview.html)

### Realtime transport: Action Cable

Rails channels integrate WebSocket messages with application code

Configure chosen cable adapter and connection/channel authorization; test reconnect, revoked membership and host proxy behavior. Broadcast is not durable event history.

- [https://guides.rubyonrails.org/action_cable_overview.html](https://guides.rubyonrails.org/action_cable_overview.html)

Build with the stack’s strengths

## Architecture and developer workflow.

Rails supplies a conventional MVC application and Active Record persistence. Turbo Drive, Frames and Streams support a server-rendered HTML interface with incremental browser updates. Recommendation: keep domain writes on the server and introduce browser-owned state only for interactions that need it; a framework rewrite is not a prerequisite for fixing an inefficient query or incorrect queue contract.

Version scope: Rails guides showing Rails 8.1 examples; current Turbo handbook. These describe target capabilities, not the source application's installed versions. Detect Gemfile.lock, Ruby version and configured adapters before choosing a move. · Reviewed 2026-10-11

### Architecture boundaries

- Recommendation: establish one authoritative domain writer and one schema migration runner per shared database during coexistence. Route a stable tenant or feature boundary, and keep the old application able to read new writes until rollback expires.

- Active Job is the application interface; Solid Queue is the Rails 8 default backend. Inspect the application's actual adapter before inferring a Redis dependency. Separate workers, recurring task configuration and domain writes are distinct responsibilities.

- Recommendation: treat Turbo stream DOM targets, Action Cable channel identifiers, job serialization and session cookies as separate contracts. Sharing a datastore does not prove any of their compatibility.

### Tools and feedback

- In an existing checkout, read Gemfile.lock and environment configuration before adapting generators. In a disposable feature prototype, bin/rails generate model Product name:string and bin/rails db:migrate demonstrate the conventional model/migration path; review generated schema changes before applying them.

- bin/rails generate job guests_cleanup creates an Active Job class; configure its selected backend and worker process separately. Recipe only: use bin/jobs start for a configured Solid Queue application, then exercise enqueue, failure and recovery against its real queue database.

- bin/rails console is useful for inspecting application objects. Recommendation: capture authorized and unauthorized request fixtures, mixed-version writes and browser stream behavior as acceptance checks rather than treating successful boot as migration qualification.

- For a new local-account prototype, bin/rails generate authentication creates User and Session models plus login surfaces. Review account lifecycle and resource authorization separately; do not run it over an established authentication system without an intentional migration design.

### Translations to avoid

- Avoid calling a queue-adapter configuration change a backlog migration: the old backend retains work and Active Job can serialize GlobalID and custom types that need the original Ruby classes and database semantics.

- Avoid claiming atomic domain-write-and-enqueue from a separate Solid Queue database. After-commit enqueue prevents premature execution but is not a transaction across two stores. Recommendation: use a durable outbox when a crash between commit and delivery must be recoverable.

- Avoid the Action Cable async adapter for distributed production delivery; the guide restricts it to development/testing and a single process. Avoid replacing session or lock data with a disposable cache just because both use Redis.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/getting_started.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/active_record_migrations.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/active_job_basics.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/security.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/action_cable_overview.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/caching_with_rails.html)
[turbo.hotwired.dev →](https://turbo.hotwired.dev/handbook/introduction)
[turbo.hotwired.dev →](https://turbo.hotwired.dev/handbook/streams)
[github.com →](https://github.com/sidekiq/sidekiq/wiki/Best-Practices)

For an existing application

## Keep the contracts while you change the stack.

These are documented options and proposed checks. Inspect the actual source application before adopting a transition.

Data and schema

### Inspect the source

Inventory table names, primary keys, timestamps, enum values, constraints, callbacks and the application that currently owns migrations; Active Record migrations define schema evolution but do not describe all application side effects.

### During coexistence

Recommendation: keep the existing writer authoritative while the target reads representative data. Add backward-readable fields first; transfer one domain boundary explicitly. Keep a single migration runner rather than allowing two framework migration histories to modify the same tables independently.

### Target design

Rails Active Record over the selected PostgreSQL schema, with reviewed database constraints and migrations. Recommendation: remove legacy fields only after old code and rollback readers have retired.

### Rollback

Recommendation: route writes back only while the old reader understands new records. Preserve additive schema changes; rehearse reversible migrations separately and reconcile irreversible data changes.

### Benefits and prerequisites

**Recommendation: conventional migrations and model access can concentrate schema work in one maintained application.**
Requires: A named schema owner, compatible value semantics and staged writer transfer.
Tradeoffs: Shared tables constrain both deployments; deleting or rewriting data may require reconciliation rather than migration reversal.

### Checks before transfer

- Create in the old application and read in Rails, then reverse the direction after writer transfer; compare IDs, timestamps and nullable values.

- Run the proposed migration against a disposable copy; verify both deployed versions can still read and update a fixture, including unique and foreign-key failures.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/active_record_migrations.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/getting_started.html)

Jobs and schedules

### Inspect the source

Record adapter, job class, argument serializers/GlobalIDs, queue names, retries, scheduled work, failed work and running leases. Rails exposes these through Active Job while each backend supplies execution behavior.

### During coexistence

Recommendation: retain old consumers for old payloads; route a new job category to a new queue/backend. Transfer its producers and recurring-task owner together. Drain or deliberately transform ready, scheduled, retry, failed and in-flight work; do not run both schedulers for the same business operation.

### Target design

Active Job with configured Solid Queue or the retained qualified adapter. Same-database enqueue may join a domain transaction; a separate queue database needs an explicit delivery design. Recommendation: require business idempotency even when job uniqueness exists.

### Rollback

Recommendation: stop new producers before restoring old routing; retain both consumers through the rollback window and reconcile completed external effects before re-enqueueing.

### Benefits and prerequisites

**SQL-backed job persistence may allow retirement of a dedicated broker.**
Requires: All broker uses inventoried and retired; workers, queue schema, failure monitoring and backlog reconciliation in place.
Tradeoffs: Database work and queue operations remain operational responsibilities; retaining Redis for another capability yields no service-count reduction.

### Checks before transfer

- Replay captured old serializer fixtures using the retained consumer; a deleted GlobalID target must follow the intended failure policy.

- Rollback a domain transaction and confirm no executable job escapes it; kill after an external effect before acknowledgement and verify a retry cannot repeat the business effect.

- Account for every backlog state and observe only one scheduled enqueue for the transferred category.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/active_job_basics.html)
[github.com →](https://github.com/sidekiq/sidekiq/wiki/Best-Practices)

Identity and authorization

### Inspect the source

Distinguish the application's generated Session model from Rails session storage. Capture cookie name, serializer, signing/encryption configuration, expiration, account IDs, logout/revocation and per-resource authorization.

### During coexistence

Recommendation: keep a shared identity authority or a documented session exchange endpoint. If cookies are shared, qualify the complete cookie format and rotation rules; route stable users and retain the old decoder for the agreed window.

### Target design

An intentionally selected Rails authentication implementation with application authorization. CookieStore stores session data on the client; account/session lifecycle rules must be designed separately.

### Rollback

Recommendation: preserve old account IDs and accepted session formats until routing stabilizes; coordinate revocation and key rotation so restoring traffic cannot revive invalidated sessions.

### Benefits and prerequisites

**Recommendation: coherent account and session ownership can preserve login continuity during feature migration.**
Requires: Verified identity mapping, cookie/exchange fixtures and shared revocation behavior.
Tradeoffs: Compatibility code and key rotation increase maintenance; planned reauthentication is an alternative when continuity is unnecessary.

### Checks before transfer

- Authenticate through the old path and visit the new route; then logout, expire or revoke the session and confirm both paths deny access.

- Replay a modified cookie, a stale cookie and a valid session from another tenant against a protected resource; each unauthorized operation must be rejected.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/security.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/getting_started.html)

Realtime transport

### Inspect the source

Capture channel names, connection authentication, subscription commands, DOM target IDs, Turbo stream actions and reconnect behavior. Turbo Streams can arrive over WebSockets, SSE or request responses; Action Cable owns its own client/server protocol.

### During coexistence

Recommendation: keep the current realtime endpoint for old clients; bridge versioned domain events to the selected target renderer. Assign one event publisher per committed change and route a room/user cohort including reconnects, rather than randomly splitting individual socket requests.

### Target design

Turbo streams over the selected transport, with Action Cable using a configured shared adapter when multiple server processes must communicate. Solid Cable is database-backed; Redis remains a documented alternative.

### Rollback

Recommendation: preserve the old event renderer/endpoint and backward-readable stream payloads until the cohort can reconnect there; reconcile already-delivered state rather than blindly rebroadcasting.

### Benefits and prerequisites

**HTML updates can reuse server-rendered partials.**
Requires: Stable DOM targets, authorized subscriptions and compatible event-to-view rendering.
Tradeoffs: A new socket protocol still needs client changes or a bridge; reconnection recovery is an application acceptance requirement.

### Checks before transfer

- Place old and new clients on different server processes; commit one change and verify both authorized clients display it once.

- Disconnect during a write, reconnect and verify the UI converges to the persisted state; unauthorized origins and subscriptions must fail.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/action_cable_overview.html)
[turbo.hotwired.dev →](https://turbo.hotwired.dev/handbook/streams)

Caching and shared state

### Inspect the source

List reconstructible values separately from sessions, counters and locks. Capture key namespaces, serialized values, TTLs and invalidation triggers; Rails.cache and request-local SQL query caching have different lifetimes.

### During coexistence

Recommendation: use a new cache namespace when representations differ, read through to the authoritative data and maintain invalidation for both deployed readers. Keep shared coordination uses on their qualified store while moving only disposable values.

### Target design

A deliberately selected Rails cache store; Solid Cache is database-backed and new Rails 8 applications include it. MemoryStore and RedisCacheStore remain choices with different process/service boundaries.

### Rollback

Recommendation: switch to the previous namespace/store and repopulate from domain data; retain the old backend while any coordination capability still depends on it.

### Benefits and prerequisites

**Database-backed caching can remove a dedicated cache service when it is the last use.**
Requires: Reconstructible contents, tested expiration/invalidation, adequate database capacity and retirement of all other service uses.
Tradeoffs: Cache traffic moves to the database; cache durability does not make it the authoritative domain store.

### Checks before transfer

- Warm both versions, update the source record and verify neither version serves the prior value after its specified invalidation boundary.

- Restart the application or empty only the dedicated cache namespace; requests must rebuild correctly without losing identity or coordination state.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/caching_with_rails.html)

Delivery and recovery

### Inspect the source

Record deployed web and Cable processes, configured workers/dispatchers, recurring.yml tasks and database connections. A web deployment alone does not establish that workers or broadcasts are running.

### During coexistence

Recommendation: give each process a named operational owner; keep existing backups and queue monitoring while deploying the new cohort. Transfer recurring schedule ownership once, after checking overlapping old and new schedules.

### Target design

An explicit Rails process inventory with worker lifecycle, queue failure inspection and the chosen realtime adapter. Recommendation: document separate domain, queue and cache database restore ordering where applicable.

### Rollback

Recommendation: freeze producers and scheduler ownership transfers, restore the previous process configuration, then reconcile work accepted by the new deployment.

### Benefits and prerequisites

**Recommendation: visible process and schedule ownership makes failures and recovery boundaries reviewable.**
Requires: A deployment recipe for the selected adapters and an exercised restore/restart procedure.
Tradeoffs: Coexistence temporarily operates both systems; one healthy HTTP endpoint cannot certify all processes.

### Checks before transfer

- Stop a worker, enqueue a representative task and verify monitoring identifies its growing age; restart and observe the intended completion/failure policy.

- Restore a disposable backup and prove domain records, delayed tasks and session behavior are accounted for before promoting traffic.

[guides.rubyonrails.org →](https://guides.rubyonrails.org/active_job_basics.html)
[guides.rubyonrails.org →](https://guides.rubyonrails.org/action_cable_overview.html)

[Read the migration workflow →](/rewrite/guide.md)

## Setup

- Start in a source directory in a local development workspace with supported Ruby/Rails, PostgreSQL and Git installed; choose and record compatible versions before generation.

- Run rails new product -d postgresql; enter product; configure development/test database access without committing credentials.

- Run bin/rails generate authentication; add Pundit via bundle add pundit and run bin/rails generate pundit:install; implement signup and resource policies deliberately.

- Run bin/rails db:prepare; start bin/rails server. Configure Solid Queue from the installed-version guide and run its worker with bin/jobs when enabled.

- Add bundler-audit to the development group if absent; preserve Gemfile.lock. Define authorization, duplicate-job and migration tests.

- Prepare config/deploy.yml, secrets, database persistence and backup/restore procedure. Only in an authorized deployment run bundle exec kamal setup initially or bundle exec kamal deploy thereafter.

## Verification

Run these in the configured application; they are instructions, not recorded passes.

```
ruby --version
bundle exec rails --version
bin/rails db:migrate:status
bin/rails zeitwerk:check
bin/rails test
bin/rails test:system
bin/rubocop
bin/brakeman
bundle exec bundler-audit check --update
```

## Open questions

- Which signup, SSO/MFA and resource authorization policies are required?

- Does the product fit server-owned screens or need substantial offline/client state?

- Which host, mail/storage services, worker throughput and restore requirements apply?

## Sources

- [Hotwire: HTML Over The Wire](https://hotwired.dev/)

- [Active Record Migrations](https://guides.rubyonrails.org/active_record_migrations.html)

- [Getting Started with Rails](https://guides.rubyonrails.org/getting_started.html)

- [Pundit README](https://github.com/varvet/pundit)

- [Devise README](https://github.com/heartcombo/devise)

- [Active Job Basics](https://guides.rubyonrails.org/active_job_basics.html)

- [Testing Applications](https://guides.rubyonrails.org/testing.html)

- [Securing Rails Applications](https://guides.rubyonrails.org/security.html)

- [bundler-audit README and license](https://github.com/rubysec/bundler-audit)

- [Kamal Installation](https://kamal-deploy.org/docs/installation/)

- [Rails and AI](https://rubyonrails.org/ai)

- [Rails MIT License](https://github.com/rails/rails/blob/main/MIT-LICENSE)

- [Rails Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html)

- [Rails Active Storage](https://guides.rubyonrails.org/active_storage_overview.html)

- [Rails Action Cable](https://guides.rubyonrails.org/action_cable_overview.html)

---

Canonical page: https://stack.menu/stacks/rails-hotwire-postgresql/
