Does Ruby on Rails Work With Payload CMS?
Rails and Payload CMS can work together as a headless setup, but they're fundamentally misaligned architectures—Rails wants to be your full stack while Payload is TypeScript-first.
Quick Facts
How Ruby on Rails Works With Payload CMS
Rails and Payload CMS integrate via REST/GraphQL APIs rather than native coupling. Your Rails app acts as a separate client consuming Payload's headless APIs to fetch content, while Payload runs as an independent Node.js/TypeScript application. This works well for content distribution but creates operational overhead—you're managing two separate applications with different ecosystems, deployment targets, and monitoring requirements.
The developer experience is straightforward: use Rails' built-in HTTP clients (Net::HTTP, HTTParty, or Faraday) to query Payload's REST or GraphQL endpoints, cache responses with Rails.cache, and render content in your views. However, you lose Rails' convention-over-configuration benefits since content schema lives entirely in Payload's TypeScript codebase. Developers must context-switch between Ruby and TypeScript, and debugging content-related issues requires understanding both systems.
This pairing works best when Payload handles complex content workflows (permissions, versioning, localization) that would be tedious to build in Rails, while Rails focuses on business logic and custom features. The architectural separation provides flexibility—you can swap frontends or add mobile apps consuming the same Payload instance—but introduces API latency, cache invalidation complexity, and increased deployment complexity.
Best Use Cases
Rails fetching content from Payload CMS
bundle add httparty# app/services/payload_client.rb
class PayloadClient
include HTTParty
base_uri ENV['PAYLOAD_URL'] || 'http://localhost:3000'
default_timeout 5
def self.fetch_pages
response = get('/api/pages', {
query: { limit: 100 },
headers: { 'Authorization' => "Bearer #{ENV['PAYLOAD_API_KEY']}" }
})
response['docs']
end
def self.fetch_page(slug)
pages = fetch_pages
pages.find { |p| p['slug'] == slug }
end
end
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def show
@page = Rails.cache.fetch("payload_page_#{params[:slug]}", expires_in: 1.hour) do
PayloadClient.fetch_page(params[:slug])
end
render :show
end
endKnown Issues & Gotchas
API latency in page renders: Each view request may trigger multiple Payload API calls, causing slow initial loads without proper caching
Fix: Implement aggressive HTTP caching with ETags, use Rails.cache for API responses, or pre-fetch content at deployment time
TypeScript-first Payload means content schema changes require redeploying Payload, not just Rails—no Rails migrations parity
Fix: Treat Payload as immutable infrastructure; use feature flags in Rails to handle schema transitions gracefully
Authentication mismatch: Payload has its own user/permission system separate from Rails auth, creating dual login management
Fix: Use Payload's API key authentication for Rails, implement separate admin auth in Payload, or build JWT bridging logic
Local development complexity: Running Payload and Rails simultaneously requires Docker Compose or manual process management
Fix: Use docker-compose.yml or dev containers to orchestrate both services locally
Alternatives
- •Next.js + Payload CMS: Both TypeScript-based, native integration, better DX but requires learning JavaScript for backend logic
- •Rails + Strapi: Strapi is Node/JavaScript but simpler than Payload, lighter weight alternative for Rails integration
- •Rails + Contentful: Mature headless CMS with better Rails ecosystem support and official gems, but less extensible than Payload
Resources
Related Compatibility Guides
Explore more compatibility guides