r/rails

▲ 10 r/rails

Avoiding SQLite Database Locks in Production

Some tips for working with sqlite, which is great in prod.

It comes down to:

  • Better error tracking (I personally use AppSignal for it’s good Rails integration)
  • SQLite configuration
  • Background job recurring jobs
  • Optimize SQLite batch jobs
bendangelo.me
u/lautan — 1 day ago
▲ 66 r/rails+2 crossposts

Final step in the Learn Rails tutorial series: Product Reviews

My latest tutorial for the Rails Getting Started guide series is now published! It's been fun to work on for the past couple years to showcase all the built-in functionality that Rails has.

rubyonrails.org
u/excid3 — 2 days ago
▲ 11 r/rails+1 crossposts

Ozymandias on Rails. Cartography of a Ruin

When everything is on fire, nothing is on fire. Where do you start when every problem seems intractable? By drawing a map.

baweaver.com
u/keyslemur — 3 days ago
▲ 12 r/rails

How we replaced N one-off foreign keys with a single polymorphic link graph (and the STI gotcha it exposed)

We ship invoicing software (Lucanto.eu) in Rails where documents constantly relate to each other: a quote becomes a proforma, a proforma becomes an invoice, an invoice sometimes gets a credit note. For years we modeled this the obvious way - one FK column per relationship:

class Invoice < Document
  belongs_to :proforma, optional: true
  belongs_to :price_offer, optional: true # the quote
end

class CreditNote < Document
  belongs_to :invoice, optional: true
end

Worked fine until we needed genuine many-to-many: one settlement invoice paying off multiple advance/deposit invoices, one collective invoice consolidating several delivery notes. You can't express that with a single FK column, and a join table per relationship type means N new tables for N relationship types.

What we did instead: one polymorphic join table for every relationship, with a typed enum instead of a dedicated table per relationship kind:

class DocumentLink < ApplicationRecord
  belongs_to :source, polymorphic: true
  belongs_to :target, polymorphic: true

  enum :link_type, {
    derives_from: "derives_from", # invoice <- proforma <- quote
    corrects:     "corrects",     # credit_note -> invoice
    settles:      "settles",      # invoice -> advance tax document
    consolidates: "consolidates", # collective_invoice -> many delivery_notes
    paid_via:     "paid_via",
    references:   "references"
  }
end

Gotcha #1 - STI + polymorphic type column. Document has many STI subtypes (Documents::Invoice, Documents::Proforma, ...), and any of them can be either side of a link. Store source_type/target_type as record.class.name and you get "Documents::Invoice", "Documents::Proforma", etc. - and a unique index on [source_type, source_id, target_type, target_id, link_type] stops actually preventing duplicate links between the same two records once they're looked up through different subtype contexts. We store the AR base_class name instead ("Document", never the STI subclass), so the index means what it's supposed to.

Gotcha #2 - polymorphic links silently break tenant scoping. A real FK + belongs_to at least implies "this column only makes sense within one schema." Bare source_id/target_id against a polymorphic type has no such guarantee - nothing stops linking a document in tenant A to a document in tenant B. We actually shipped that as a real IDOR before catching it: a user could mass-assign another workspace's proforma id and have it render in their own sidebar. Fixed with an explicit validation:

validate :source_and_target_share_workspace

def source_and_target_share_workspace
  return unless source && target
  errors.add(:target, "must belong to the same workspace") if source.workspace_id != target.workspace_id
end

plus scoping every lookup path to the current tenant rather than trusting the polymorphic id alone.

Keeping belongs_to ergonomics. We didn't want every call site rewritten to query DocumentLink directly, so a small DSL sits on top of a Linkable concern:

class Documents::Invoice < Document
  link_belongs_to :proforma, link_type: :derives_from
end

invoice.proforma                  # reads like a normal association
invoice.proforma = some_proforma  # writes through to DocumentLink.link!

Walking the graph. The user-facing payoff is a "deal timeline" - open any document, see the whole chain. That's a BFS over DocumentLink, bidirectional, tenant-scoped, with a hard node cap, because link graphs plus bad data can cycle, and an unbounded walk inside a request is a self-inflicted DoS.

Not "abandon FKs for everything" - a real belongs_to is still simpler and faster for a relationship that's truly 1:1 and never expands. But if you can already picture a many-to-many version of a relationship showing up eventually, modeling it as a link graph from day one is a lot cheaper than the migration we ended up doing.

u/erichstark — 3 days ago
▲ 32 r/rails+39 crossposts

Made a free iOS app to open and read raw Markdown (.md) files on iPhone/iPad — handy for peeking at Logseq pages outside the app

Logseq stores everything as plain .md files, but if you ever open one of those files directly on iOS (from Files, iCloud, Dropbox, a backup, etc.) you just get raw text. I built a small viewer to read them rendered on a phone.

Md Preview:

• Renders GitHub-Flavored Markdown — headings, tables, task lists, footnotes

• Code blocks with syntax highlighting, plus LaTeX math and Mermaid diagrams

• Opens .md / .markdown / .mdx / .rmd / .qmd from Files or the Share Sheet

• 100% on-device — no account, no uploads, no ads, no subscriptions

Free on the App Store: https://apps.apple.com/app/id6760341080

Details: https://markdown.cybergame.ai/

Not a Logseq replacement at all — just a quick way to read loose .md files when you're away from the desktop app. Curious how you all read your graph on the go.

u/Fujima4Kenji — 5 days ago
▲ 20 r/rails+1 crossposts

New jemalloc gem (jemalloc_rb)

Do you use the jemalloc gem?

The original project on GitHub seems to be abandoned for around 12 years, and some incompatibilities with recent Ruby versions have begun to emerge. Additionally, the underlying jemalloc library hasn't been updated during all this time. Because of that, I created a fork and launched a new gem (jemalloc_rb) to keep the project functional, updated, and actively accepting pull requests.

https://github.com/henrique-ft/jemalloc_rb

u/Illustrious-Topic-50 — 4 days ago
▲ 71 r/rails+1 crossposts

I forked Campfire and made it an alternative to Discord

Campfire is a simple open source chat app made by 37Signals. It uses minimalist rails stack with SQLite. It is made for small teams as an alternative to Slack.

I was using it as a public chat server for a community similar to Discord. However, it lacked many features like member management, moderation tools or threads.

Meanwhile they also released activerecord-tenanted gem with less fanfare. It was supposed to be used for their fizzy product but was abandoned in the end as they switched their choice of database back to MySQL.

The idea of having a database per tenant had stuck with me ever since I read this article few years back. When I explored the activerecord tenanted gem, it was ready for action. Almost all the concerns such as migration and tenant resolver were already handled out of the box by the gem. This prompted me to attempt to make Campfire much more like Discord, with a multi-community switcher and all the other features you would expect from a Slack/Discord-like chat platform. That resulted in Sabha.

Sabha.co operates in SaaS mode, where each community has its own database. You can create a community, start inviting members, and when you think it's time to switch, you can download it to self-host on your own server as a single-tenant installation whenever you want.

It is licensed under the MIT license, so you can use it with your brand, domain, and have full control of the data.

It is absurd that everyone defaults to Discord as a place for chat community, even in case open-source projects. Every alternative is a VC-funded enterprise tool attempting to replace Slack, resulting in a complex tech stack that makes self-hosting on a small server impractical.

Video demo: https://x.com/ashwinm/status/2063333911917445366

Github repo: https://github.com/sabha-co/sabha

u/ashwinm — 5 days ago
▲ 14 r/rails+1 crossposts

[OC] I made a simple time tracking webapp

Hi data-diggers,

A while ago I used to track my time using an Excel sheets with lots of macros and strange formulas I only half-understood. I know some other people here do it because that's what inspired me to also do it!

Recently I wanted to start tracking my time again but didn't wanted to do it on an old-school Excel sheet, so I developed a web app with Ruby On Rails.

It features basic features at the moment :

  • Time input via buttons and auto timestamp increment for fast-input
  • Calendar view with hours-per-day in cells (Excel-style, baby!)
  • Statistics (activities per category / activities over time)
  • Basic preferences (sleep schedule, categories setup, language)

The app is completely free, cookie-less and home hosted. Feel free to take a look at my Github (where contributions are welcome!) or directly on the website itself.

Website : https://timetracker.dotsncircles.com/

Github : https://github.com/theiereman/time-tracker

u/ImReaperz — 5 days ago
▲ 0 r/rails+1 crossposts

The N+1 Query Problem

You ship a /posts index page. It renders 50 posts, and for each one it shows the author's name with post.author.name. QA says the page is slow, and the logs are full of repetitive SQL.

  1. What is this problem called, and why is it happening?
  2. How would you detect it, in dev and in a running app?
  3. How do you fix it, and what's the difference between the main fixing strategies?

Answer: N+1 queries

What it is and why it happens

This is the N+1 query problem. One query loads the 50 posts, then Active Record fires one more query per post to load post.author. That's 1 + N queries (1 for posts, 50 for authors) when it could be 2, or even 1. Active Record associations are lazy by default, so the author isn't loaded until you call post.author. Do that inside a loop and you get a round trip per record.

How to detect it

  • Dev logs: you'll see the same SELECT ... FROM authors WHERE id = ? over and over with different IDs. That repetition is the tell.
  • The Bullet gem: built for this. It warns you in dev when you should add eager loading, and also when you're eager-loading something you don't need.
  • An APM in production (Sentry, New Relic, Scout): flags endpoints firing a lot of queries.
  • Tests: assert on query count with something like assert_queries or an RSpec matcher so CI catches a regression before it ships.

How to fix it

Eager-load the association:

# N+1
@posts = Post.all
@posts.each { |p| puts p.author.name }   # 1 + 50 queries

# Fixed
@posts = Post.includes(:author)
@posts.each { |p| puts p.author.name }   # 2 queries

There are four tools, and they don't do the same thing:

Method What it does When to use it
preload Loads the association in a separate query and matches it in memory You just need the data and aren't filtering or ordering by the association
eager_load One LEFT OUTER JOIN that loads everything in a single query You need to filter or order by the association and read its attributes
includes Defaults to preload, but switches to eager_load if you reference the association in a where or order Your usual default. Let Rails decide
joins INNER JOIN for filtering or sorting. Does not load the association into memory You need to filter by the association but don't read its attributes

The trap: joins alone does not preload. If you joins(:author) and then call p.author.name in the loop, you're right back to N+1. Reach for includes or eager_load when you actually read the association's attributes.

u/hasan-dev — 6 days ago
▲ 55 r/rails

StreamVault - a self-hosted media streaming app built with Rails 8 + Hotwire + FFmpeg (just open-sourced)

Hey r/rails - I open-sourced a project I've been working on and thought the stack might be interesting to share here, since it leans heavily on Rails 8 + Hotwire.

StreamVault is a self-hosted media streaming application: search for movies/TV, pick a stream, watch in-browser with a custom video player, library, watch history, continue-watching, recommendations. It uses RealDebrid to cache the files and FFmpeg to transcode on-the-fly when needed.

What's in the stack:

  • Rails 8.1 with Hotwire (Turbo Drive + Stimulus) — the whole frontend is server-rendered views + a handful of Stimulus controllers. No SPA.
  • Solid Queue / Solid Cache / Solid Cable — the new Rails 7.1+ "no-Redis" stack. Jobs, caching, and websockets all backed by Postgres tables.
  • ActionPolicy for authorisation - everything scoped per-user.
  • Active Record Encryption for API keys at rest.
  • Tailwind (via tailwindcss-rails), dark theme, PWA-installable.
  • PostgreSQL in production (SQLite in dev/test).
  • Docker deployment via docker-compose; assets precompiled in the image.

The interesting Rails-y bits:

  • A custom video player written as a Stimulus controller (~47 KB) handling HLS playback, seeking, audio-track selection, subtitle burning, resume, stall recovery, and progress tracking via background jobs.
  • On-the-fly transcoding: the app pipes a RealDebrid HTTPS stream into FFmpeg and serves the remuxed/transcoded output back to the browser as a stream. Deciding "remux vs transcode" based on the source codec/container is a fun bit of logic.
  • A factory service (StreamProvider) that queries Torrentio and Comet in parallel and picks the best candidate, with fallback.
  • Recommendations via TMDB collaborative filtering over the user's watch history.

Repo (full README with architecture, env vars, proxy setup): https://github.com/vitobotta/StreamVault

Curious what people think of the Solid Queue/Cache/Cable stack in production - it's been solid for me but I'd love to hear others' experiences. Also happy to answer questions about the transcoding pipeline or the Stimulus video player.

Edit: some people just love being rude for no particular reason. I came here to share a project I thought others might find useful without having anything to gain from it nor needing to "convince" anyone or justify anything. Some people are truly unbelievable.

u/Sky_Linx — 8 days ago
▲ 34 r/rails+1 crossposts

A native MacOS app for the Rails console

Activepad is a native macOS scratchpad for Rails apps.

Think Rails console, but with a proper editor: multi-line snippets, tabs, code completion. It's also possible to connect to remote instances

I built it because I kept using the Rails console for debugging, quick experiments, etc., but the terminal started feeling awkward once the snippets got longer or I wanted to keep results around.

With Activepad, you open your app, write code, run it, and inspect the result, a nice ui.

Would love to hear what you think :)

If you don't have the means to support the project, but still want to check it out, just leave a comment and I'll DM you a key.

activepad.app
u/JngoJx — 8 days ago
▲ 6 r/rails+1 crossposts

Live-updating comments with Turbo in Rails A Comment model pushes its own creates, updates, and deletes to subscribers over Turbo Streams.

highlit.co
u/Environmental-Yak328 — 7 days ago
▲ 18 r/rails+1 crossposts

Ozymandias on Rails. The Pedestal Inscription

Shelley wrote about a king whose monument outlived everything it was built on. I've spent 15 years inside Rails monoliths that did the same thing. This is the first post about what to do when you're standing in the ruins.

baweaver.com
u/keyslemur — 7 days ago
▲ 14 r/rails+1 crossposts

I built a saga orchestrator for Ruby — DAG execution, async Sidekiq, automatic rollback

After wrestling with distributed transactions across microservices and watching Sidekiq job chains grow into unmaintainable spaghetti, I built Ruby Reactor — a saga pattern implementation for Ruby that handles the hard parts of workflow orchestration.

What it does:

  • Builds a DAG (directed acyclic graph) from your step definitions, so independent steps run in parallel
  • Runs async via Sidekiq with back-pressure and batching
  • Automatically rolls back completed steps when something fails (compensation)
  • Supports interrupts — pause a workflow mid-flight and resume it later via webhook or manual trigger
  • Includes a built-in web dashboard to inspect every execution
  • Has locks, semaphores, and rate limits built in (Redis-backed)
  • Ships with RSpec test helperstest_reactor, mock_step, chainable matchers

Why I built it:

I kept hitting the same wall: complex business transactions that span multiple services need coordination. dry-transaction handles linear pipelines well, but when you need parallel execution, async processing, automatic rollback on failure, or the ability to pause and wait for external events, you're on your own. Trailblazer operations can do some of this, but the undo logic and parallelism are manual.

Ruby Reactor fills the gap — it's the only Ruby library that combines DAG planning + async execution + compensation + interrupts + a dashboard in one package.

Quick example — an e-commerce checkout with fraud detection:

class CheckoutReactor < RubyReactor::Reactor
  input :order_id

  step :reserve_inventory do
    argument :order_id, input(:order_id)
    run { |args| Inventory.reserve(args[:order_id]) }
    undo { |_err, args| Inventory.release(args[:order_id]) }
  end

  step :charge_card do
    argument :order_id, input(:order_id)
    run { |args| Payment.charge(args[:order_id]) }
    undo { |_err, args| Payment.refund(args[:order_id]) }
  end

  # Pause here — wait for Stripe webhook
  interrupt :wait_for_fraud_check do
    wait_for :charge_card
    correlation_id { |ctx| "order-#{ctx.input(:order_id)}" }
    timeout 3600, strategy: :active
  end

  step :ship_order do
    argument :status, result(:wait_for_fraud_check, :status)
    run { |args| Shipping.create_label(args[:order_id]) }
    undo { |_err, args| Shipping.cancel(args[:order_id]) }
  end

  returns :ship_order
end

It's at v0.4.1 now with ~3,300 downloads on Rubygems. The v0.4.0 release just added interrupts, the web dashboard, RSpec helpers, and Redis-backed coordination primitives (locks, semaphores, rate limits, periods).

How it compares:

Feature Ruby Reactor dry-transaction Trailblazer Raw Sidekiq
DAG/Parallel execution Limited Manual
Auto compensation/undo Manual Manual
Interrupts (pause/resume) Manual
Built-in web dashboard
Locks / sem / rate limits Manual
Async with Sidekiq Limited

I'd love honest feedback — especially from people who've built complex workflows in production. What did I miss? What's over-engineered? What would make you actually use this instead of raw Sidekiq jobs?

Repo: https://github.com/arturictus/ruby_reactor Rubygems: gem 'ruby_reactor', '~> 0.5' Docs: Full guides for every feature in the repo's documentation/ directory

Thanks for reading! I'll be in the comments.

u/arturictus — 7 days ago
▲ 40 r/rails+1 crossposts

Rails: The Sharp Parts. A Polymorphic Type Is Not a Foreign Key

I don't like to bury ledes, so when people ask me about polymorphic relationships my answer is simply:

Don't.

baweaver.com
u/keyslemur — 10 days ago
▲ 19 r/rails

The View Layer Rails Couldn’t See

The Rails view-layer question has consolidated. After years of churn — Arbre, Erector, the Slim and Haml DSLs, then the components debate — the community has settled, and at the same time ERB itself is being rebuilt as a first-class, HTML-aware foundation. The interesting part is not which template engine won the argument. It is why it won, and what changed underneath it to make the win durable. The community is moving toward ERB, not away from it, and the reason is a tool called Herb.

davidslv.uk
u/davidslv — 11 days ago