← Back to DevBytes

Migrating from Phoenix to Rails: Step-by-Step Guide

Introduction: Understanding the Migration Landscape

Migrating a web application from Phoenix (Elixir) to Rails (Ruby) is a significant undertaking that involves translating not just syntax but entire architectural paradigms. While both frameworks share similarities—convention over configuration, MVC patterns, and a focus on developer productivity—the underlying languages, ecosystems, and tooling differ substantially. This guide provides a complete, step-by-step walkthrough to help you move a Phoenix application into the Rails world with confidence, preserving functionality while taking advantage of Ruby’s mature ecosystem.

Why Migrate from Phoenix to Rails?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Organizations choose this migration for several reasons: access to a larger talent pool of Ruby developers, a broader selection of mature gems, integration with legacy Rails monoliths, or business requirements favoring Ruby infrastructure. Rails offers a massive library of battle-tested gems, extensive documentation, and a vibrant community. For teams already invested in Ruby or those finding Elixir’s functional paradigm a barrier to hiring, moving to Rails can be a strategic long-term decision.

Prerequisites and Planning

Before writing any code, perform a thorough audit. Document every route, controller action, model, schema, background job, WebSocket channel, and third-party integration. Create a spreadsheet mapping each Phoenix component to its Rails equivalent. Set up a fresh Rails application using rails new and ensure Ruby, Rails, and any required native extensions are installed. Plan for a phased rollout—perhaps using a strangler pattern—to minimize downtime and risk.

Step-by-Step Migration Guide

1. Audit Your Phoenix Application

Start by generating a comprehensive list of your application's modules and behaviors. Use mix phx.routes to capture every route. List all Ecto schemas, contexts, and migrations. Identify Phoenix channels and presence modules. Note any Oban workers or scheduled jobs. This audit becomes your blueprint for the Rails rebuild.

2. Setting Up the Rails Project

Create a new Rails application with a similar configuration (e.g., PostgreSQL, skipping unneeded frameworks). Configure the database and install essential gems like devise for authentication, pundit for authorization, and sidekiq for background jobs—matching your Phoenix dependencies as closely as possible.

rails new myapp --database=postgresql --skip-test --skip-action-mailbox --skip-action-text
cd myapp
bundle add devise pundit sidekiq

3. Translating the Database Schema and Models

Ecto Migration to Rails Migration

Convert each Ecto migration into a Rails migration. Phoenix migrations use create table blocks with add macros, while Rails uses a more imperative DSL. Pay close attention to default values, null constraints, and indexes.

Phoenix (Ecto) Migration:

defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :name, :string
      add :role, :string, default: "user"
      timestamps()
    end
    create unique_index(:users, [:email])
  end
end

Rails Migration:

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :name
      t.string :role, default: "user"
      t.timestamps
    end
    add_index :users, :email, unique: true
  end
end

Ecto Schema to ActiveRecord Model

Phoenix schemas are separate from the model logic (often placed in contexts). Rails merges schema definition, validations, associations, and query methods into the model class. Translate schema blocks, embedded schemas, and associations.

Phoenix Schema + Context:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :name, :string
    field :role, :string, default: "user"
    has_many :posts, MyApp.Blog.Post
    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :name, :role])
    |> validate_required([:email])
    |> unique_constraint(:email)
  end
end

Rails Model:

class User < ApplicationRecord
  has_many :posts

  validates :email, presence: true, uniqueness: true
  validates :role, inclusion: { in: %w[user admin] }
end

4. Mapping Routes

Phoenix routes are defined in the router module using macros like get, post, and resources. Rails uses a similar DSL inside config/routes.rb. Translate nested resources, scopes, and pipelines (plugs) into Rails equivalents (namespaces, concerns, before_action filters).

Phoenix Router:

defmodule MyAppWeb.Router do
  use Phoenix.Router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
  end

  scope "/", MyAppWeb do
    pipe_through :browser
    get "/", PageController, :index
    resources "/posts", PostController
  end
end

Rails Routes:

Rails.application.routes.draw do
  root "pages#index"
  resources :posts
end

5. Controllers and Actions

Phoenix controllers return rendered templates or redirects, using assigns to pass data. Rails uses instance variables (@posts) by convention. Translate plug-based authentication and authorization into before_action callbacks or gems like Devise and Pundit.

Phoenix Controller:

defmodule MyAppWeb.PostController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    posts = MyApp.Blog.list_posts()
    render(conn, :index, posts: posts)
  end
end

Rails Controller:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

6. Views and Templates

Replace Embedded Elixir (.eex / .heex) templates with ERB (.erb). The syntax differences are minor: <%= @post.title %> works in both, but Rails uses instance variables from the controller. For component-like structures, consider Rails ViewComponent or partials with locals.

Phoenix Template (HEEX):

<h1>Posts</h1>
<%= for post <- @posts do %>
  <div class="post">
    <h2><%= post.title %></h2>
    <p><%= post.body %></p>
  </div>
<% end %>

Rails ERB Template:

<h1>Posts</h1>
<% @posts.each do |post| %>
  <div class="post">
    <h2><%= post.title %></h2>
    <p><%= post.body %></p>
  </div>
<% end %>

7. Channels to Action Cable

Phoenix Channels provide WebSocket-based real-time communication with topics and a client library. Rails offers Action Cable with similar concepts: channels, connections, and broadcasting. Translate your Phoenix socket definitions and channel modules into Action Cable channels.

Phoenix Channel:

defmodule MyAppWeb.PostChannel do
  use Phoenix.Channel

  def join("post:" <> post_id, _payload, socket) do
    {:ok, socket}
  end

  def handle_in("new_comment", payload, socket) do
    broadcast!(socket, "new_comment", payload)
    {:noreply, socket}
  end
end

Rails Action Cable Channel:

class PostChannel < ApplicationCable::Channel
  def subscribed
    stream_from "post:#{params[:post_id]}"
  end

  def receive(data)
    ActionCable.server.broadcast("post:#{params[:post_id]}", data)
  end
end

8. Background Jobs: Oban to Sidekiq

Oban uses Ecto-backed job queues with worker modules. Sidekiq relies on Redis and defines workers as classes. Translate periodic jobs (Oban Cron) into Sidekiq's scheduler or use whenever gem for cron-style scheduling.

Phoenix Oban Worker:

defmodule MyApp.Workers.SendEmail do
  use Oban.Worker

  def perform(%{"email" => email}) do
    MyApp.Email.send_welcome(email)
    :ok
  end
end

Sidekiq Worker:

class SendEmailWorker
  include Sidekiq::Worker

  def perform(email)
    UserMailer.welcome(email).deliver_now
  end
end

9. Testing and Quality Assurance

Phoenix uses ExUnit with factory libraries like ExMachina. Rails uses RSpec or Minitest. Convert your test suite systematically: controller tests become Rails functional/request specs, model tests become model specs, and channel tests become Action Cable channel specs. Use FactoryBot for test data. Validate every behavior in a staging environment before going live.

10. Deployment and Rollout

Deploy the Rails application alongside the Phoenix one, using a reverse proxy (Nginx or a load balancer) to gradually route traffic. Start with read-only endpoints, then move write operations. Monitor performance, errors, and background jobs closely. Once the Rails app handles all traffic confidently, decommission the Phoenix instance.

Best Practices for a Smooth Transition

Conclusion

Migrating from Phoenix to Rails is a multi-stage journey that touches every layer of your application. By following a structured, step-by-step approach—auditing, translating schemas, routes, controllers, views, real-time features, and jobs—you can systematically rebuild your system on a new foundation without losing functionality. The process demands patience, rigorous testing, and careful rollout planning. With the right preparation and a commitment to incremental delivery, your team can successfully complete the transition and unlock the full benefits of the Rails ecosystem.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles