docs/rails_upgrade_assessment.md
Date: 2026-07-24
Repository revision assessed: 56e72eff8d67
Chatwoot should target Rails 8.1.3, but it should not jump from Rails 7.1 to 8.1 in one compatibility change or one production deployment.
The implementation sequence is:
develop and move to Rails 7.2.3.1 while retaining Rails 7.0 framework defaults.Rails itself recommends upgrading one minor version at a time because minor versions may change public APIs and each intermediate release provides the useful deprecation warnings. It also recommends enabling new framework defaults gradually. See the official Rails upgrade guide.
The distinction is:
As of this assessment, the current releases are Rails 7.1.6, 7.2.3.1, 8.0.5, and 8.1.3. See Rails versions on RubyGems.
Applying the published one-year bug-fix and two-year security windows to each series' initial release date gives:
| Series | Role in this plan | Bug-fix support through | Security support through |
|---|---|---|---|
| Rails 7.2 | Short compatibility checkpoint | 2025-08-09, already ended | 2026-08-09 |
| Rails 8.0 | Short compatibility/rollback checkpoint | 2025-11-07, already ended | 2026-11-07 |
| Rails 8.1 | Target | 2026-10-22 | 2027-10-22 |
| Area | Current state | Consequence |
|---|---|---|
| Ruby | 3.4.4 | Already satisfies Rails 7.2 and Rails 8.x. Do not combine another Ruby upgrade with this work. |
| Rails | ~> 7.1, locked at 7.1.5.2 | Four minor-version checkpoints remain: current 7.1 patch, 7.2, 8.0, and 8.1. |
| Framework defaults | config.load_defaults 7.0 | The project is on Rails 7.1 code with Rails 7.0 behavior. Defaults migration is a separate body of work. |
| Initializers | 47 files | Several defaults and framework integrations are explicitly customized. |
| Application models | 109 files | Active Record changes have a broad possible blast radius. |
| Application jobs | 86 files | Active Job and transaction timing changes need representative integration tests. |
| Controllers | 185 files | Routing, redirects, query parsing, cookies, and response serialization need smoke coverage. |
| Specs | 796 total, including 234 Enterprise specs | The existing suite is substantial, but upgrade-specific contract tests are still needed. |
| Enterprise overlay | 427 files under enterprise/app | Eight of the known Rails 8 enum changes are in Enterprise code. |
| Asset stacks | Vite plus Sprockets-backed Super Admin assets | Rails 8 new-app defaults must not be applied mechanically. |
| File storage | Local/S3/GCS plus the Rails Azure service | Rails 8.1 removes the built-in Azure service. |
Two defaults-sensitive surfaces are particularly important:
perform_later/deliver_later call sites; 14 files contain both transaction and enqueue behavior.The assessment used isolated copies of this exact revision, changed only dependency constraints and the minimum code necessary to boot, and did not modify the application worktree.
User using alias_attribute to alias an association:
app/models/user.rb: alias_attribute :conversations, :assigned_conversationsalias_attribute as a database-attribute facility. This should become an explicit method or alias_method.administrate-field-belongs_to_search must move from 0.9.0 to 0.10.0 because 0.9.0 constrains Rails to < 7.2.jbuilder and debug during this checkpoint removes failures that otherwise surface later:
ActiveSupport::ProxyObject, which Rails 8 removes.connection_pool >= 2.3, so Bundler selected 3.0.2, but both Sidekiq scheduler threads then crashed because 7.3.1 calls the removed positional TimedStack#pop API. Sidekiq 7.3.10 explicitly constrains connection_pool < 3; that exact worker line and connection_pool 2.5.5 are required for this checkpoint.Rack::File constant under Rack 3.2. Updating it to 4.0.1 restores its JavaScript endpoint.schema.rb already contain service_name, variant records, and a nullable checksum. Those generated migrations must not be copied blindly.The initial dependency resolution failed for three independent reasons:
devise-secure_password fork constrains Railties to < 8.administrate-field-belongs_to_search 0.10.0 constrains Rails to < 8 and Administrate to < 1.rails-i18n ~> 7.0 constrains Railties to < 8.After locally relaxing the two fork constraints, selecting Rails-compatible dependency versions, and making the minimum code changes:
bin/rails zeitwerk:check passed on Rails 8.1.3.Gemfile and Gemfile.lockapp routeUser association alias fix raises the known floor to 36 files.The resolved Rails 8.1 graph also required or selected these application-level changes:
| Dependency | Current | Resolved Rails 8.1-compatible line | Why it changes |
|---|---|---|---|
administrate | 0.20.1 | 1.0.0 | 0.20.x requires Rails < 8; Super Admin compatibility and assets must be tested. |
administrate-field-belongs_to_search | 0.9.0 | Custom/upstream-compatible release | 0.10.0 still requires Rails < 8 and Administrate < 1. |
devise-secure_password | Chatwoot fork | Upstream 2.2.1 | This is the latest upstream release supporting Devise 4 while allowing Railties 8; 2.2.3 requires Devise 5. |
rails-i18n | 7.0.10 | 8.1.0 | The 7.x line requires Railties < 8. |
jbuilder | 2.11.5 | 2.15.1 | Current version uses removed ActiveSupport::ProxyObject. |
acts-as-taggable-on | 12.0.0 | 13.0.0 | Version 12 does not allow Active Record 8.1. |
audited | 5.4.1 | 5.8.0 | Current constraint does not cover Rails 8. |
hairtrigger | 1.0.0 | 1.3.1 | Current constraint does not cover Active Record 8. |
devise-two-factor | 6.1.0 | 6.4.0 | Current constraint stops below Rails 8.1. |
devise_token_auth | 1.2.5 | 1.2.6 | Current constraint stops below Rails 8.1. |
bullet | 8.0.7 | 8.1.3 | Current version rejects Active Record 8.1 at runtime. |
sidekiq | 7.3.1 | 7.3.10 with connection_pool 2.5.5 | Sidekiq 7.3.1 crashes its schedulers with connection_pool 3. Sidekiq 7.3.10 encodes the safe < 3 constraint. Rails 8.1 also deprecates its built-in adapter in favor of Sidekiq's adapter, so avoid a simultaneous Sidekiq 8 migration. |
sprockets-rails | Transitive | Explicit dependency, 3.5.x | Administrate 1 no longer supplies the transitive dependency, but Chatwoot Super Admin still uses Sprockets. |
debug | 1.8.0 | 1.11.1 | Required for the current Ruby/Rails update tooling to run cleanly. |
The current, Rails 7.2, and experimental Rails 8.1 lock graphs all passed bundle-audit against the advisory database updated on 2026-07-22. That does not replace a release-time audit.
These estimates are for tracked files in the Chatwoot repository. Any remaining companion dependency work is listed separately.
| Path | Exact known minimum | Production-ready estimate | Explanation |
|---|---|---|---|
| Rails 7.2.3.1, version compatibility only | 3 | 3–5 | Gemfile, Gemfile.lock, and the User association alias. |
| Refreshed existing Rails 7.2 PR | 22 | 22 | Also carries Rails-7.2 boundary fixes, Azure adapter replacement, Sidekiq scheduler and profiler compatibility, test stabilization, a production preflight, and the migration guide. |
| Rails 7.2.3.1 plus staged 7.1/7.2 defaults | 3 | 8–15 | Adds explicit defaults, encryption/serialization guards, and focused regression specs. |
| Rails 8.1.3, minimum boot floor from 7.1 | 36 | At least 36 | 31 enum files, two dependency files, User, routes, and schema dumper. |
| Rails 8.1.3, production-ready | 36 | 50–70 | Adds Azure strategy, Super Admin asset fixes, defaults, job/auth/storage/request specs, schema validation, and rollout configuration. |
| External companion repositories | 1 repository | Approximately 2–4 files | Only a fork/replacement of administrate-field-belongs_to_search remains; devise-secure_password is resolved upstream. |
The sequential route does not materially reduce the final total number of changed files. It changes when and how those files are changed:
bin/rails app:update --pretend reported 26 config/bin/public actions for Rails 7.2 and 32 for Rails 8.1, in addition to attempted migrations. Those are candidate generator changes, not a recommended diff. Accepting them wholesale would overwrite or churn Chatwoot-specific Vite, Sprockets, environment, deployment, and initializer choices.
Because config.load_defaults is still 7.0, upgrading the gem alone and activating defaults are different operations.
| Interface/default | Change | Chatwoot exposure | Required action |
|---|---|---|---|
| Active Record encryption digest | Rails 7.1 changes key derivation/digest behavior and disables SHA-1 support for affected legacy ciphertext by default. | 19 encrypted declarations, including inbox/channel credentials, integration tokens, OTP secrets, webhook secrets, and Enterprise Twilio credentials. | Pin legacy-readable behavior first, inventory/decrypt all affected records, test deterministic lookups, rewrite ciphertext if needed, then flip the default in a later deploy. |
| Cache serialization | Rails 7.1 introduces format 7.1. | Redis cache and rolling application deploys. | Keep the old write format during the version deploy, then enable the new format after every old process is gone. Treat rollback compatibility as an acceptance criterion. |
| Message serialization/metadata | Rails 7.1 moves to newer serializers and optimized metadata. | Signed IDs, Active Storage IDs, OAuth account signed IDs, Warden/session-adjacent signatures, and any persisted messages. | Explicitly pin on the first deploy; validate old tokens and signatures; enable separately. |
| Callback ordering | Rails 7.1 changes callback execution defaults. | Message creation, notifications, webhooks, emails, imports, and auditing rely heavily on callbacks. | Add ordering assertions around side effects and enable separately from the version bump. |
| Attribute/association validation | Rails 7.1 changes readonly-attribute and belongs-to foreign-key validation defaults. | High model count and Enterprise extensions. | Run the complete model/request suite and inspect new validation errors before enabling. |
| Test exception handling | Rails 7.1 replaces boolean show_exceptions values with :all, :rescuable, and :none. | config/environments/test.rb still sets true. | Replace it with the intentional symbolic mode and re-run controller/request exception specs. |
| HTML parsing/sanitization | Rails 7.1 defaults use HTML5 variants. | Email/message rendering, Help Center content, and Action Text. | Run content fixtures through sanitizer and rendering regression tests. |
| Enqueue after transaction commit | Rails 7.2 changes the standard job/transaction integration. | 166 enqueue/delivery calls and 14 files combining transactions and enqueueing. | Verify jobs never observe uncommitted/missing data and that rollback does not enqueue side effects. |
| PostgreSQL date decoding and migration timestamps | Rails 7.2 changes decoding and validates migration timestamps. | Reporting, imports, schema setup, and old migration history. | Run reporting/import specs and validate both existing-database and fresh-database setup. |
An AST scan found no explicit return, break, or throw inside Chatwoot transaction blocks, so Rails 7.2's changed non-local-return transaction behavior has no direct syntax hit in the current app. Keep a runtime audit for gem and dynamically composed behavior.
| Interface | Repo finding | Migration |
|---|---|---|
alias_attribute | User aliases conversations to an association. | Replace with an explicit delegating method or alias_method; add a user association spec. |
| Active Job test adapter selection | Tests now consistently respect configured queue_adapter. | Ensure test config uses the test adapter where matcher semantics require it; run job and mailer specs. |
| Framework defaults | Enqueue timing, Active Storage WebP behavior, PostgreSQL date decoding, migration timestamp validation, and YJIT defaults become available. | Activate one risk group at a time after the Rails 7.2 code deploy. |
See the official Rails 7.2 release notes and upgrade guide.
| Interface | Repo finding | Migration |
|---|---|---|
| Enum declaration | Rails 8 removes keyword-form enum declarations. Chatwoot has 39 declarations in 31 files: 23 OSS and 8 Enterprise files. | Change enum status: ... to enum :status, ...; preserve prefixes, suffixes, defaults, scopes, and serialized values; run model/API specs. |
ActiveSupport::ProxyObject | Jbuilder 2.11.5 uses the removed class. | Upgrade Jbuilder before the Rails 8 bump. |
| Active Record internal APIs | Rails removes deprecated connection-pool and schema behavior. Chatwoot subclasses a generic internal schema dumper constant. | Rework or remove the schema dumper monkey patch against the PostgreSQL dumper and verify triggers/indexes/schema output. |
| Active Job transaction setting | Old enqueue_after_transaction_commit configuration is deprecated on the path to removal in 8.1. | Use the supported boolean/per-job behavior and verify Sidekiq jobs around commit and rollback. |
| Azure Active Storage | The service is deprecated in 8.0. | Complete an extracted/custom adapter or customer migration before 8.1. |
| Fresh database migration behavior | db:migrate on a fresh database loads the schema before pending migrations. | Test both fresh schema load and upgrade from the oldest supported production schema. |
| New-app stack defaults | Propshaft, Solid Queue, Solid Cache, Solid Cable, Kamal, and the authentication generator are promoted for new apps. | Do not adopt them as part of the Rails upgrade. Existing Vite, Sprockets, Sidekiq, Redis, and Devise contracts should remain stable. |
See the official Rails 8.0 release notes.
| Interface | Repo finding | Migration |
|---|---|---|
| Azure Active Storage service | Rails removes the built-in service. config/storage.yml still defines service: AzureStorage, selectable with ACTIVE_STORAGE_SERVICE=microsoft. | This is a hard production blocker for Azure-backed installations. Supply a maintained adapter with integration tests or formally migrate/deprecate Azure. |
| Routes | resource :app, only: [:index] fails Rails 8.1 route validation because a singular resource has no collection index action. | Express the route with an explicit scope/get while preserving every existing URL and helper consumed by the frontend. |
| Parameter parsing | Leading-bracket parameter names are no longer normalized and semicolons are no longer query separators. | Add request contract tests and sample production traffic for API, OAuth, and webhook query strings. Communicate any client-facing incompatibility. |
| Routes to multiple paths | Deprecated multi-path route support is removed. | Confirm all routes compile and compare rails routes before and after. |
| Time conversion | to_time now preserves the receiver timezone; Time/TimeWithZone arithmetic removals land. | Test the two application to_time call sites, especially reporting timezone boundaries and DST fixtures. |
schema.rb order | Table columns are sorted alphabetically. | Expect a large one-file diff; isolate it in a schema-only commit and prove no semantic schema loss. |
| Active Job adapter | The Rails built-in Sidekiq adapter is deprecated in favor of the adapter shipped by Sidekiq. Symbol values and the application-level enqueue-after-commit setting are removed. | Update within Sidekiq 7.3.x, verify the loaded adapter origin, and test retries, schedules, unique/cron jobs, mail delivery, and transaction boundaries. |
| Custom job serializers | #klass must be public. | No custom serializer was found in the current tree; retain a boot-time registry check. |
| Redirect/JSON defaults | New 8.1 apps raise on path-relative redirects and stop escaping selected HTML characters in JSON. | When activating 8.1 defaults, audit 73 redirect_to call sites and API snapshots. Version-only boot can retain old behavior. |
| Order-dependent finders | Rails 8.1 deprecates order-dependent first calls without an order. | Capture deprecations in CI and fix only queries where nondeterminism matters. |
ActiveSupport::Configurable | Deprecated in 8.1. | The experimental boot warning originates in omniauth-rails_csrf_protection 1.0.2; track or patch upstream before Rails 8.2. |
See the official Rails 8.1 release notes.
The repository scan also checked the principal removed interfaces that do not currently require an application edit:
| Removed/deprecated interface | Audit result |
|---|---|
ActiveRecord::ConnectionAdapters::ConnectionPool#connection | No application or Enterprise call found. |
Active Job :never, :always, and :default enqueue-after-commit values | No application configuration using these symbols found. |
Application-level deprecated config.active_job.enqueue_after_transaction_commit | No current setting found; transaction/enqueue behavior still needs runtime tests. |
Custom Active Job serializer private #klass | No custom Active Job serializer found. |
| SuckerPunch's internal Rails adapter | SuckerPunch is not the configured queue backend. |
Time-to-TimeWithZone addition and Time#since(Time) | No direct application call pattern found; the two to_time call sites remain in scope. |
| SQLite adapter removals | Chatwoot's supported production path is PostgreSQL; fresh and upgraded PostgreSQL remain in the matrix. |
| Risk | Severity | What could break | Detection and containment |
|---|---|---|---|
| Encrypted credentials become unreadable | Critical | Email inbox passwords, social tokens, webhook secrets, OTP secrets, integration tokens, and Enterprise channel credentials. | Preflight decrypt scan with counts by model/attribute; dual-readable configuration; canary reads and writes; do not remove legacy support until all old processes and ciphertext are gone. |
| Azure file storage stops booting or serving files | Critical for affected installs | Upload, download, preview, direct upload, and purge operations when ACTIVE_STORAGE_SERVICE=microsoft. | Decide support policy before Rails 8.0; run Azure emulator/real-account contract tests; block Rails 8.1 rollout without a passing adapter. |
| Jobs run before commit or disappear on rollback | High | Notifications, email, webhooks, message fan-out, imports, search indexing, and reporting side effects. | Transaction integration specs plus Sidekiq smoke tests; compare enqueue and failure metrics during canary. |
| Authentication/session/signature invalidation | High | Agent sessions, API auth, OTP flows, signed IDs, password policy, OAuth, and Active Storage links. | Mixed-version rolling-deploy test; old cookie/token fixtures; fork compatibility specs; explicit serializer settings. |
| Super Admin loses CSS/JS or fields | High | Administrate 1 changes and loss of transitive Sprockets dependencies. | Make sprockets-rails explicit or migrate intentionally; production asset precompile; browser smoke every Super Admin CRUD path and custom field. |
| API or webhook parameter behavior changes | High | Leading-bracket or semicolon query strings from old/third-party clients. | Production log sampling, request fixtures, contract tests, and a documented client migration if such traffic exists. |
| Enum API/values drift | High | Scopes, predicates, validations, JSON values, database values, Enterprise policies, and background jobs. | Mechanical syntax-only commits split by domain; assert mappings before/after; run affected request/job specs. |
| Schema or fresh install drifts | High | Trigger definitions, schema loader, Active Storage duplicate migrations, and extension setup. | Compare schema objects, indexes, constraints, triggers, and extensions on upgraded and fresh PostgreSQL databases. |
| Time/reporting results shift | Medium | Daily boundaries, unread counts, report grouping, and DST behavior. | Timezone matrix tests for UTC, non-UTC, and DST transition dates. |
| Hidden Enterprise incompatibility | Medium to high | Overrides and prepended modules load after the OSS path appears healthy. | Run zeitwerk:check, the full Enterprise suite, and product smokes with Enterprise enabled at every checkpoint. |
The following work must happen before Rails 8.1 can be considered releasable:
Choose the Azure policy.
ActiveStorage::Service adapter, orPrepare the Rails-constrained Admin dependency.
devise-secure_password is resolved by moving to upstream 2.2.1 in the Rails 7.2 checkpoint.administrate-field-belongs_to_search.Inventory encrypted production data.
Define rolling-deploy compatibility.
Make the Super Admin asset ownership explicit.
Reconcile Active Storage migration history.
service_name, variant record, and nullable checksum changes even though the squashed initial migration already has them.Add upgrade observability.
Each milestone should be independently green and revertible. Framework defaults and serialized write formats should not change in the same deployment as the Rails gem version.
zeitwerk:check, production boot, assets, fresh schema load, and bundle-audit.devise-secure_password to upstream 2.2.1 (S)Gemfile, Gemfile.lock, app/models/user.rb, focused dependency/user specs.administrate-field-belongs_to_search 0.10.0, update Jbuilder/debug, and replace the association alias_attribute.zeitwerk:check, asset builds, and advisory audit.Deployment checkpoint: Deploy Rails 7.2.3.1 with 7.0 defaults, canary it, and prove rollback to 7.1.6.
config.load_defaults 7.2 is possible without an unexplained compatibility override; all product contracts remain stable.Deployment checkpoint: Run Rails 7.2 with 7.2 defaults before beginning Rails 8 removal work.
The enum rewrite should be split into four syntax-only tasks so review can verify value mappings:
conversation, message, attachment, contact, inbox, webhook, notification, and notification_subscription.user, account_user, agent_bot, agent_bot_inbox, assignment_policy, integrations/hook, channel/twilio_sms, and channel/web_widget.article, data_import, data_import_item, custom_filter, custom_attribute_definition, campaign, and macro.sprockets-rails explicit, integrate R0.3, and preserve Vite for the dashboard.zeitwerk:check, schema dump/load, and route compilation pass on both Rails 7.2 and 8.0.Deployment checkpoint: Deploy Rails 8.0.5 and observe it before removing Azure or activating Rails 8 defaults.
config/routes.rb plus route/request/webhook/OAuth specs.zeitwerk:check, full OSS/Enterprise suites, asset builds, fresh/upgrade database paths, all storage services, auth, jobs, Action Cable, mailboxes, APIs, webhooks, and reporting smokes pass.Deployment checkpoint: Canary Rails 8.1.3 with prior defaults and serialized formats. Prove rollback to Rails 8.0.5.
config.load_defaults 8.0 is possible with documented exceptions only.config.load_defaults 8.1 is active.Every Rails-version checkpoint should run:
eval "$(rbenv init -)"
bundle check
bundle exec rails runner 'puts Rails.version'
bundle exec rails zeitwerk:check
bundle exec bundle-audit check --update
bundle exec rspec
bundle exec rubocop <changed-ruby-files>
pnpm build
It should also run production-mode boot and asset precompile using CI-safe secrets, plus:
schema.rb.The Rails migration should not also introduce:
Each may be valuable later, but coupling them to the framework upgrade removes the ability to attribute and safely roll back failures.
Start the work now with Rails 8.1.3 as the declared target and Rails 7.2.3.1/8.0.5 as required deployable checkpoints. Rails 7.2 is too close to security end of life to justify a separate long-term upgrade project, but skipping it would discard the exact deprecation and isolation boundary Rails provides.
The critical path is not the 31 mechanical enum edits. It is:
Once those are settled, the remaining Rails 8 code changes are bounded and the isolated Rails 8.1 boot proves the application can reach the target without a broad rewrite.