Redprints CFP: an open source CFP management app built with Rails + Inertia.js

At Evil Martians, when it comes to writing custom software, we have a strong open-source-first culture. This means we’re always thinking about which parts of our projects we can open-source as libraries or tools to share with the community. Our portfolio is massive and very diverse, but it’s missing one particular “species” of open source: full-featured applications. But that changes today! Let’s introduce the Redprints CFP, an open source CFP application powering our recent SF Ruby Conference proposal selection process.
Evil Martians have a long story of building internal projects that serve a number of different needs (from CMS to Slack bots to infrastructure control panels). Until recently, these needs were essentially specific to our business work, and open-sourcing them made little sense. However, after building a CFP app (and seeing others doing the same), we realized that it’s time to make the next move and go public.
Hire Evil Martians
Need a custom application or tool for your team or event? We specialize in building developer-focused solutions that work beautifully.
About the name of this project: the term “Redprints” is a word play on “blueprint” and “Red planet” (we’re from Mars, after all). Redprints is a complete, production-ready application that showcases modern development practices while solving real-world problems. It’s designed to be flexible and easy to customize, in other words, starter kit with some extra superpowers.
Check out Redprints CFP on GitHub or go to SF Ruby CFP to see it in action.
The Redprint CFP is the first one in the Redprints series (a few more are waiting in line to be redprintified). Let’s explore its “whys”, “whats”, and “hows”.
Why a DIY CFP app?
To explain why we did this at all, in the first place, we didn’t like any of the existing commercial solutions—especially given their prices and limitations. As regular users of various CFP platforms, we knew we could do better.
Second, we’re always looking for ways to battle-test new technologies in real-world scenarios. Building a CFP app gave us the perfect opportunity to challenge Inertia.js for Rails in a production environment.
Finally, I previously had a pleasant experience using a custom CFP app for reviewing proposals as a member of the EuRuKo programme committee. I knew building one was doable—and could be done well.
The result? A clean, efficient CFP management system that handles everything from proposal submissions to review workflows, all while showcasing the best of Ruby on Rails development with Inertia.
What comes with the Redprints CFP app?
Let’s take a quick look at what you get when you launch the app. (Note that the interface is clean, responsive, and built with accessibility in mind.)
Submitting proposals
Proposal submission experience for speakers
The proposal submission process is straightforward:
- Speakers sign up via social logins (GitHub and Google are supported out of the box; others are easily configurable with a bit of code and OmniAuth plugins)
- The submission form contains familiar fields (with configurable labels and limits) and allows updating both the speaker and the proposal details in one place
- Speakers can also create drafts to be finalized later.
One may ask if the app allows speakers to reuse proposals and send them to different conferences. The answer is obviously (and intentionally): “No”. The app is meant to be used for a single CFP campaign (meaning that the following year, you’ll simply create a new one or wipe out the database).
As frequent participants of both sides of the CFP process (applying and reviewing), we consider sending the same (especially, letter-for-letter) proposal to every event an anti-pattern and want to encourage speakers to write targeted proposals instead.
Administering and reviewing proposals
Backoffice and proposals evaluation
Organizers get an admin dashboard for managing the entire CFP process:
- Setting up evaluation rules and criteria and assigning reviewers.
- Tracking the evaluation progress and making the final decision.
The app supports having different evaluation flows for other tracks or sets of reviewers. Reviewers may or may not see the speaker details (and you can switch between these modes at any time).
With a bit of code (remember, it’s a redprint, not a black-box product), you can modify the proposal distribution among reviewers to your needs. For example, at EuRuKo, each reviewer only reviewed a random subset of proposals. That could be achieved with a few lines of code in the Evaluation::Distribution
class:
def proposal_reviewer_pairs
proposal_ids = proposals.pluck(:id)
reviewer_ids = reviewers.pluck(:id)
- # Each reviewer is assigned to review each proposal by default
- proposal_ids.product(reviewer_ids)
+ pairs = []
+
+ reviewer_ids.each do |reviewer_id|
+ # Use reviewer_id as seed for deterministic randomization
+ rng = Random.new(reviewer_id)
+ # Calculate 2/3 of proposals for this reviewer
+ proposals_count = (proposal_ids.length * 2.0 / 3).round
+ # Shuffle proposals deterministically and take 2/3
+ selected_proposals = proposal_ids.shuffle(random: rng).take(proposals_count)
+ selected_proposals.each do |proposal_id|
+ pairs << [proposal_id, reviewer_id]
+ end
+ end
+
+ pairs
end
Customization made simple
The codebase is designed to minimize the number of potential customization points. You start with a blank state (EXAMPLE Conf CFP), and tune it to your needs. No more wrestling with rigid commercial platforms or building from scratch.
Check out the amount of changes needed to launch the SF Ruby CFP app in this demo PR: redpints-cfp#1.
For example, the color theme uses TailwindCSS 4 and OKLCH, so adjusting it to your needs is as simple as updating a few CSS variables:
Theming with TailwindCSS and OKLCH
The home page is built with React (glued with Rails by Inertia), and we didn’t bother to compete with LLMs in this field, and tried to create a magical template that would work for everyone. Just vibe-code your CFP app main page to your liking!
Still, some “find-and-replace” action would be required to fully tune the app to represent your conference (and not just “EXAMPLE Conference”), but we left some hints (FIXME: ...
) for you (or your AI agent) in the codebase. It’s a redprint, after all.
Now, let’s take a quick overview of some technical features of the app.
Redprint highlights
For every redprint (there will be more, stay tuned!), we want to highlight the tools, techniques, and architectural decisions we made. Even if you don’t want to reuse the whole app, you might find inspiration in these choices.
Ruby on Rails needs no advertisement. Inertia.js is a popular guest on our blog, so you’ve probably heard about it, too. So, we’re gonna focus on less obvious aspects of building this application.
Bolt vibes
Remember our blog post about vibe-coding the SF Ruby website with Bolt.new in a matter of a tea break? You might have guessed that we were in a rush preparing our new conference announcement. We found ourselves in a similar situation when the time came to kick off the CFP process.
That said, our beloved Bolt doesn’t support Rails (spoiler: it kinda does), so we couldn’t build the whole app with prompts using just Bolt.
And frankly speaking, we didn’t want to: Rails is highly productive when building from scratch, even without any AI help, and we still value the joy of programming. At the same time, we had to meet very tough deadlines and build a UI in style with the main website.
So, we chose a handcrafted backend and a vibe-coded frontend. And that was also one of the reasons we went with Inertia.js and React: the ability to quickly craft a good-looking UI that could be easily copied and pasted to a Rails codebase.
In the end, it took us about ~10 hours to go from zero* to production (meaning that we had just enough functionality to start collecting proposals). Amazing it is!
* In our case, “zero” wasn’t just rails new
; we reused another internal Martian application’s Rails foundation to get started. Yes, we applied a redprint to create another one.
Bundlebun-ing all the things
Bundlebun is the gem that gives you the Bun executable and integrates it into Rails’ asset pipeline. A single binary that brings you the whole Node.js runtime!
This gem downloads and manages Bun binaries automatically, making JavaScript tooling setup effortless. It integrates perfectly with existing frontend builders (we use Vite, by the way) and brings the speed of Bun to Rails apps with zero hassle.
Avo for rapid admin development
We’ve started using Avo for building admin panels in our internal apps, and it turned out to be a game-changer for such use cases. Our philosophy: keep everything you don’t need to expose to non-admin users in Avo—save time by not reinventing the wheel.
In the CFP app, we use Avo for back-office tasks: setting up the evaluation process, promoting users to reviewers, re-sending notifications, exporting data, and so on.
Avo also brings powerful data-browsing capabilities (filters, scopes, searching, sorting), so we didn’t even need to fallback to Spreadsheets for that (as we frequently do).
Form objects for the win
The application is built around forms: a proposal form, a review form, and a final results submission form. And all these forms go beyond typical Active Record model responsibilities.
For example, the proposal form manages two models, the proposal itself and the speaker profile shared between all the submitters’ proposals. We aggregate all the fields in a single form object that quacks like a classic model (more precisely, an active model) and also handle draft and final submissions logic in the form class.
The results submission form is also a special case: its purpose is to update the selected proposals’ statuses and initiate the notifications delivery. Here is its simplified version:
class Evaluation
class SubmitForm < ApplicationForm
attribute :proposals, default: []
validate :validate_proposals_format
after_commit :notify_speakers
attr_reader :updated_proposals
def submit!
@updated_proposals = []
locked_proposals = Proposal.lock.where(external_id: proposals.map { it[:id] }).index_by(&:external_id)
proposals.each do
proposal = locked_proposals[it[:id]]
next unless proposal
next unless proposal.submitted?
proposal.update!(status: it[:status])
updated_proposals << proposal
end
true
end
private
def validate_proposals_format
# this method is quite long, but that's what makes this object a form:
# we want to provide meaningful feedback to the user if the input is incorrect
end
def notify_speakers
updated_proposals.each do |proposal|
ProposalDelivery.with(proposal:, speaker: proposal.speaker_profile).public_send(:"proposal_#{proposal.status}").deliver_later
end
end
end
end
Keeping the forms backend in a dedicated abstraction layer, closer to the UI, makes the codebase more straightforward to maintain and comprehend.
Page objects for reliable testing
We used SitePrism page objects for system tests, following patterns from our System of a Test series. Look at the proposal submission RSpec example:
describe "Proposals" do
let_it_be(:user) { create(:user, name: "Marco", email: "marco@evl.ms") }
let(:home_page) { prism.home }
let(:proposal_form_page) { prism.proposal_form }
let(:proposals_page) { prism.proposals }
before { sign_in_as(user) }
specify "user can create a draft" do
home_page.load
home_page.nav.within do |nav|
nav.click_on "Submit proposal"
end
expect(proposal_form_page).to be_displayed
proposal_form_page.form.within do |f|
f.fill_in "Title", with: "How to create a CFP app in 12 hours with Rails"
f.select "General", from: "Track"
f.fill_in "Full Name", with: "Marco"
# Do not fill it—it must be prefilled
# fill_in "Email", with: "marco@evl.ms"
end
click_on "Save Draft"
expect(proposals_page).to be_displayed
expect(proposals_page.proposals).to have_rows(count: 1)
expect(proposals_page.proposals.first_row).to have_text "Draft"
end
end
Page objects made our tests more maintainable and readable, especially important for complex form interactions.
Fan fact: to set up Site Prism for this project, I pointed my Zed Agent to the blog post and asked it to configure everything in the described way—and it worked like a charm! (Now I wonder if I need to build an MCP server serving our blog posts as resources for better integration with agents 🤔.)
More goodies
Here’s a quick tour of other gems and tools that made development smoother, and what’s special about how we use them.
We used Lookbook for mailer previews in this app. However, the overall configuration is similar to other apps where we use it: the lookbook/
folder to keep everything storybook-related; view_component-contrib extensions for sensible defaults and better file organization.
Our Litestream (a tool for SQLite replication) configuration differs from the canonical one in how we store credentials.
We use our beloved Overmind and lefthook Ruby gem wrappers for a more straightforward development workflow.
Oh, and there is a CLAUDE.md with the rules I used for converting Minitest to RSpec (yes, in this direction; no regrets 😁).
Ready to build and launch the next CFP?
Redprints CFP is production-ready and battle-tested. Whether you’re organizing a local meetup or an international conference, this redprint gives you a solid foundation to build upon.
Explore the code, deploy your own instance, and let us know what you build with it!