<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Syed Ghani</title>
    <description>The latest articles on DEV Community by Syed Ghani (@dev_syed_ghani).</description>
    <link>https://dev.to/dev_syed_ghani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961424%2F7fa6a00d-f809-40dc-afc9-4d88fc5319aa.png</url>
      <title>DEV Community: Syed Ghani</title>
      <link>https://dev.to/dev_syed_ghani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev_syed_ghani"/>
    <language>en</language>
    <item>
      <title>Rails GuardDog: Advanced Security Scanner for Rails Applications</title>
      <dc:creator>Syed Ghani</dc:creator>
      <pubDate>Sat, 06 Jun 2026 09:22:10 +0000</pubDate>
      <link>https://dev.to/dev_syed_ghani/rails-guarddog-advanced-security-scanner-for-rails-applications-1a67</link>
      <guid>https://dev.to/dev_syed_ghani/rails-guarddog-advanced-security-scanner-for-rails-applications-1a67</guid>
      <description>&lt;h1&gt;
  
  
  Rails GuardDog: Advanced Security Scanner for Rails
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Today I'm excited to announce &lt;strong&gt;Rails GuardDog v0.1.0&lt;/strong&gt; — an open-source security &lt;br&gt;
scanner for Rails that goes beyond traditional tools like Brakeman.&lt;/p&gt;

&lt;p&gt;While Brakeman is excellent for catching basic Rails vulnerabilities, Rails GuardDog &lt;br&gt;
focuses on newer vulnerability classes that most tools miss: AI/LLM prompt injection, &lt;br&gt;
DoS/ReDoS patterns, supply chain attacks, and more.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Modern Rails applications face new security challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI/LLM Integration&lt;/strong&gt; - How do you prevent prompt injection when integrating with ChatGPT, Claude, or Anthropic?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReDoS Attacks&lt;/strong&gt; - Catastrophic backtracking in regex can bring down your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply Chain Attacks&lt;/strong&gt; - Typosquatted gems that look like popular libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDOR Gaps&lt;/strong&gt; - Objects accessible without proper authorization checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Secrets&lt;/strong&gt; - Hardcoded API keys that Brakeman misses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rails GuardDog detects all of these.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Rails GuardDog?
&lt;/h2&gt;

&lt;p&gt;Rails GuardDog is a lightweight gem that adds comprehensive security scanning &lt;br&gt;
directly to your Rails applications.&lt;/p&gt;
&lt;h3&gt;
  
  
  12 Security Checkers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SQL Injection&lt;/strong&gt; - String interpolation in queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS&lt;/strong&gt; - Unescaped output in views&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF&lt;/strong&gt; - Disabled protection verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mass Assignment&lt;/strong&gt; - &lt;code&gt;permit!&lt;/code&gt; vulnerabilities (fixes Brakeman #1942, #1918)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Redirect&lt;/strong&gt; - User input in redirects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded Secrets&lt;/strong&gt; - API keys, tokens, passwords (always-on, fixes #1989)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DoS/ReDoS&lt;/strong&gt; - Unbounded queries, dangerous regex patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDOR&lt;/strong&gt; - Object access without authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI/LLM Prompt Injection&lt;/strong&gt; - User input flowing to LLMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting&lt;/strong&gt; - Missing rack-attack configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply Chain&lt;/strong&gt; - Typosquatted gems using Levenshtein distance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL&lt;/strong&gt; - Missing field-level authorization&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📊 &lt;strong&gt;Multiple report formats&lt;/strong&gt;: Console, HTML, JSON&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;AST-based analysis&lt;/strong&gt;: Uses parser gem for deep code understanding&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Async support&lt;/strong&gt;: Built-in Sidekiq integration&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Zero dependencies&lt;/strong&gt;: Only requires parser and ast gems&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Production-ready&lt;/strong&gt;: Tested and battle-ready&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;CWE/OWASP mappings&lt;/strong&gt;: Every finding includes vulnerability mappings&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rails-guarddog'&lt;/span&gt;
&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scan your Rails app&lt;/span&gt;
rake guarddog:scan

&lt;span class="c"&gt;# Generate HTML + JSON reports&lt;/span&gt;
rake guarddog:report

&lt;span class="c"&gt;# CI/CD integration (exits 1 if critical found)&lt;/span&gt;
rake guarddog:ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Example Output
&lt;/h2&gt;

&lt;p&gt;====================================================================&lt;br&gt;
Rails GuardDog Security Report&lt;br&gt;
&lt;a href="https://dev.to3%20findings"&gt;CRITICAL&lt;/a&gt;&lt;br&gt;
Mass Assignment — permit! allows ALL parameters&lt;br&gt;
app/controllers/users_controller.rb:15&lt;br&gt;
Fix: Use permit(:name, :email, :age)&lt;br&gt;
AI Injection — User input in LLM prompt&lt;br&gt;
app/services/chat_service.rb:42&lt;br&gt;
Fix: Sanitize: prompt = 'Template: ' + sanitize(params[:text])&lt;br&gt;
&lt;a href="https://dev.to5%20findings"&gt;HIGH&lt;/a&gt;&lt;br&gt;
DoS: Unbounded query without limit&lt;br&gt;
app/controllers/posts_controller.rb:5&lt;br&gt;
Fix: Add .limit(100) or use pagination&lt;/p&gt;
&lt;h2&gt;
  
  
  Comparison with Brakeman
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Brakeman&lt;/th&gt;
&lt;th&gt;Rails GuardDog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL Injection&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Enhanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Extended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mass Assignment&lt;/td&gt;
&lt;td&gt;✅ Partial&lt;/td&gt;
&lt;td&gt;✅ Fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;⚠️ Optional&lt;/td&gt;
&lt;td&gt;✅ Always-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DoS/ReDoS&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ NEW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDOR&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ NEW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Injection&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ ORIGINAL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply Chain&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ ORIGINAL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraphQL&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ BONUS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;At [Your Company/Team], we encountered vulnerabilities that Brakeman couldn't catch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A developer passed user input directly to Claude without sanitization&lt;/li&gt;
&lt;li&gt;We found dangerous regex patterns that could cause ReDoS attacks&lt;/li&gt;
&lt;li&gt;A typosquatted gem almost made it into production&lt;/li&gt;
&lt;li&gt;Several GraphQL resolvers lacked authorization checks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I decided to build a tool that catches these modern vulnerabilities.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-World Example: AI Injection
&lt;/h2&gt;

&lt;p&gt;Here's a vulnerability Rails GuardDog catches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ DANGEROUS - User input flows directly to LLM&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;
  &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"Summarize: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ SAFE - Sanitized input&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;
  &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:text&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"You are helpful."&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s2"&gt;"Summarize: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rails GuardDog would flag the first example immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/guarddog.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guarddog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled_checkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[
  sql_injection xss csrf mass_assignment secrets
  ai_injection idor dos rate_limit
]&lt;/span&gt;

&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guarddog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail_on_severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI/CD Integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/security.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security Scan&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;guarddog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.2'&lt;/span&gt;
          &lt;span class="na"&gt;bundler-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run GuardDog&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rake guarddog:ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guarddog-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;guarddog_report.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Rails GuardDog is available now on RubyGems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rails-guarddog'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RubyGems&lt;/strong&gt;: &lt;a href="https://rubygems.org/gems/rails-guarddog" rel="noopener noreferrer"&gt;https://rubygems.org/gems/rails-guarddog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/yourusername/rails-guarddog" rel="noopener noreferrer"&gt;https://github.com/yourusername/rails-guarddog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues&lt;/strong&gt;: &lt;a href="https://github.com/yourusername/rails-guarddog/issues" rel="noopener noreferrer"&gt;https://github.com/yourusername/rails-guarddog/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT License&lt;/strong&gt;: Open source and free&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Try it in your Rails app: &lt;code&gt;gem 'rails-guarddog'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run: &lt;code&gt;rake guarddog:scan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check the HTML report: &lt;code&gt;rake guarddog:report&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add to CI: &lt;code&gt;rake guarddog:ci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Share feedback: GitHub issues&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Found a bug? Have an idea? Contributions welcome!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Submit issues: &lt;a href="https://github.com/yourusername/rails-guarddog/issues" rel="noopener noreferrer"&gt;https://github.com/yourusername/rails-guarddog/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open PRs: &lt;a href="https://github.com/yourusername/rails-guarddog/pulls" rel="noopener noreferrer"&gt;https://github.com/yourusername/rails-guarddog/pulls&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Star on GitHub ⭐&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions?
&lt;/h2&gt;

&lt;p&gt;Drop a comment below or reach out on GitHub!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Rails GuardDog v0.1.0 is production-ready and available now.&lt;/strong&gt; 🐕&lt;/p&gt;

&lt;h1&gt;
  
  
  Rails #Security #RubyGems #WebDevelopment #AppSecurity #OpenSource
&lt;/h1&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
      <category>scanner</category>
    </item>
    <item>
      <title>I built a gem that finds unused CSS classes in Rails apps — here's the interesting problem I had to solve</title>
      <dc:creator>Syed Ghani</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:30:51 +0000</pubDate>
      <link>https://dev.to/dev_syed_ghani/i-built-a-gem-that-finds-unused-css-classes-in-rails-apps-heres-the-interesting-problem-i-had-to-1acb</link>
      <guid>https://dev.to/dev_syed_ghani/i-built-a-gem-that-finds-unused-css-classes-in-rails-apps-heres-the-interesting-problem-i-had-to-1acb</guid>
      <description>&lt;p&gt;Yesterday I published my first Ruby gem — &lt;a href="https://github.com/sghani001/rails-persona" rel="noopener noreferrer"&gt;rails-persona&lt;/a&gt;, a behavioral analytics library that lets you track user actions directly on ActiveRecord models.&lt;/p&gt;

&lt;p&gt;It had bugs on day one. Wrong &lt;code&gt;File.expand_path&lt;/code&gt; depths, frozen array errors in the Railtie, a migration using &lt;code&gt;jsonb&lt;/code&gt; that broke on SQLite. I fixed them all, one GitHub issue at a time, and learned more about how Rails gems work internally than any tutorial ever gave me.&lt;/p&gt;

&lt;p&gt;That experience gave me the confidence to build another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing rails-css_unused
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="s1"&gt;'rails-css_unused'&lt;/span&gt;, group: :development
bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake css_unused:report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You get a full report of every CSS class that's defined in your stylesheets but never referenced anywhere in your views, components, or JS files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails-css_unused v0.2.1
✔ Scanning views &amp;amp; components
✔ Scanning stylesheets
✔ Comparing &amp;amp; computing ghost classes

Ghost Class Report
Ghost classes (unused): 2
• old-card-header
• legacy-sidebar-btn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No server needed. No browser. Pure static analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it scans
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ERB, HAML, Slim templates&lt;/li&gt;
&lt;li&gt;ViewComponent and Phlex component files&lt;/li&gt;
&lt;li&gt;Stimulus JS controllers (&lt;code&gt;classList.add&lt;/code&gt;, &lt;code&gt;classList.toggle&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ruby component &lt;code&gt;.rb&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;BEM class names (&lt;code&gt;block__element--modifier&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The interesting problem I had to solve in v0.2.1
&lt;/h2&gt;

&lt;p&gt;When I tested the gem on my own &lt;a href="https://github.com/sghani001/Online_Exam_System" rel="noopener noreferrer"&gt;Online Exam System&lt;/a&gt; project, it flagged these as ghost classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• status-approved
• status-cancelled
• status-requested
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But they were very much in use. Here's why the scanner missed them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;status_label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelled?&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-cancelled"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approved?&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-active"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approved?&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-approved"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request_approval?&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Requested approval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-requested"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-draft"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"status-pill &lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;status_class&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;status_label&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classes are assigned to a variable (&lt;code&gt;status_class&lt;/code&gt;) and rendered via &lt;code&gt;&amp;lt;%= status_class %&amp;gt;&lt;/code&gt;. The scanner only picks up string literals in &lt;code&gt;class="..."&lt;/code&gt; attributes — it can't see through variable interpolation.&lt;/p&gt;

&lt;p&gt;The naive fix would be to add &lt;code&gt;ignore_patterns &amp;lt;&amp;lt; /\Astatus-/&lt;/code&gt; to the config. But that feels wrong — you're telling the tool to ignore a whole prefix forever, which masks future real ghost classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The elegant fix
&lt;/h2&gt;

&lt;p&gt;Here's the insight that made this work cleanly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby variable names cannot contain hyphens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;status-cancelled&lt;/code&gt; as a variable would be a syntax error — Ruby parses it as &lt;code&gt;status - cancelled&lt;/code&gt; (subtraction). So any quoted string containing a hyphen is unambiguously a string &lt;em&gt;value&lt;/em&gt;, never a variable name.&lt;/p&gt;

&lt;p&gt;This means we can safely extract hyphenated quoted strings as CSS class names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pattern 1: *_class / *_classes variable assignments&lt;/span&gt;
&lt;span class="no"&gt;DYNAMIC_CLASS_VAR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/\b\w+_(?:class(?:es)?|style|css)\s*=\s*["']([^"'\n]+)["']/&lt;/span&gt;

&lt;span class="c1"&gt;# Pattern 2: any quoted string containing a hyphen&lt;/span&gt;
&lt;span class="c1"&gt;# (unambiguously a string value in Ruby — never a variable name)&lt;/span&gt;
&lt;span class="no"&gt;HYPHENATED_STRING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/["']([a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)+)["']/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these two patterns, the scanner now finds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"status-cancelled"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# ✅ status-cancelled extracted&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s2"&gt;"status-approved"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="c1"&gt;# ✅ status-approved extracted&lt;/span&gt;
&lt;span class="n"&gt;status_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"status-active"&lt;/span&gt;      &lt;span class="c1"&gt;# ✅ status-active extracted&lt;/span&gt;
&lt;span class="n"&gt;button_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"btn btn-primary"&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ btn-primary extracted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;ignore_patterns&lt;/code&gt; workarounds needed. No false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/css_unused.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CssUnused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ignore_classes&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[clearfix sr-only visually-hidden]&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ignore_patterns&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/\Ajs-/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/\Ais-/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/\Ahas-/&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail_on_unused&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# exit 1 in CI&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show_source_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# show which stylesheet each ghost came from&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/sghani001/rails-css_unused" rel="noopener noreferrer"&gt;https://github.com/sghani001/rails-css_unused&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyGems: &lt;a href="https://rubygems.org/gems/rails-css_unused" rel="noopener noreferrer"&gt;https://rubygems.org/gems/rails-css_unused&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;rails-persona (my first gem): &lt;a href="https://github.com/sghani001/rails-persona" rel="noopener noreferrer"&gt;https://github.com/sghani001/rails-persona&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback and PRs welcome. Still learning — but shipping.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>opensource</category>
      <category>css</category>
      <category>ruby</category>
    </item>
    <item>
      <title>I open-sourced a modern acts_as_tenant alternative for Rails 7+</title>
      <dc:creator>Syed Ghani</dc:creator>
      <pubDate>Mon, 01 Jun 2026 15:56:31 +0000</pubDate>
      <link>https://dev.to/dev_syed_ghani/i-open-sourced-a-modern-actsastenant-alternative-for-rails-7-2m5j</link>
      <guid>https://dev.to/dev_syed_ghani/i-open-sourced-a-modern-actsastenant-alternative-for-rails-7-2m5j</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Introducing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rails-tenantify:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Row-Level&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Multi-Tenancy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Rails&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;7+"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modern,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;safe,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;robust&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;row-level&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;multi-tenancy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gem&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ruby&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Rails.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Prevent&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;leaks,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;protect&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bulk&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;writes,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preserve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;background&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;jobs."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails, ruby, opensource, saas&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## The Problem&lt;/span&gt;

Every multi-tenant SaaS app eventually needs to answer the same questions:
&lt;span class="p"&gt;
*&lt;/span&gt; How do we make sure School A never sees School B's data?
&lt;span class="p"&gt;*&lt;/span&gt; How do we scope every query to the right organization?
&lt;span class="p"&gt;*&lt;/span&gt; How do we keep tenant context alive in background jobs and Sidekiq retries?
&lt;span class="p"&gt;*&lt;/span&gt; How do we stop a careless &lt;span class="sb"&gt;`update_all`&lt;/span&gt; from wiping another tenant's rows?

The typical answer is &lt;span class="ge"&gt;*"use acts_as_tenant"*&lt;/span&gt; or &lt;span class="ge"&gt;*"switch to Apartment."*&lt;/span&gt; But in modern Rails development, that often means:
&lt;span class="p"&gt;
*&lt;/span&gt; Fighting unmaintained APIs on Rails 7+
&lt;span class="p"&gt;*&lt;/span&gt; Losing tenant context when a background job retries
&lt;span class="p"&gt;*&lt;/span&gt; Dealing with schema-per-tenant complexity (Apartment) and heavy DevOps overhead
&lt;span class="p"&gt;*&lt;/span&gt; Rolling your own &lt;span class="sb"&gt;`default_scope`&lt;/span&gt; and crossing your fingers that nobody calls &lt;span class="sb"&gt;`unscoped`&lt;/span&gt;

For most Rails apps, you just need &lt;span class="gs"&gt;**row-level tenancy**&lt;/span&gt;: one database, one &lt;span class="sb"&gt;`organization_id`&lt;/span&gt; column, and strict scoping. The pattern is simple. Getting it &lt;span class="gs"&gt;**safe**&lt;/span&gt; in production is not.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## What I Built&lt;/span&gt;

&lt;span class="gs"&gt;**`rails-tenantify`**&lt;/span&gt; is a Ruby gem that adds row-level multi-tenancy directly to your Rails models and controllers. No external services, no extra databases per tenant—just your own PostgreSQL (or SQLite in dev).

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
class Project &amp;lt; ApplicationRecord&lt;br&gt;
  include Tenantify::Scoped&lt;/p&gt;

&lt;p&gt;belongs_to_tenant :organization&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### Set the tenant once per request

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
class ApplicationController &amp;lt; ActionController::Base&lt;br&gt;
  set_tenant_by :subdomain   # acme.yourapp.com → Organization&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### Everything scopes automatically

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
Tenantify.current_tenant = current_organization&lt;/p&gt;

&lt;p&gt;Project.all                      # Only this org's projects&lt;br&gt;
Project.create!(name: "Q2 Roadmap")      # organization_id is set automatically&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### Switch context safely for admins or scripts

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;/p&gt;
&lt;h1&gt;
  
  
  Temporarily switch context
&lt;/h1&gt;

&lt;p&gt;Tenantify.switch_to(other_org) do&lt;br&gt;
  Project.count   # Scoped to other_org&lt;br&gt;
end&lt;/p&gt;
&lt;h1&gt;
  
  
  Intentional bypass
&lt;/h1&gt;

&lt;p&gt;Tenantify.without_tenant do&lt;br&gt;
  Project.delete_all&lt;br&gt;&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

## How it compares to `acts_as_tenant`

While `acts_as_tenant` pioneered this pattern, many teams hit walls on modern Rails—especially around background jobs, bulk SQL, and strict safety. `rails-tenantify` was built from the ground up for Rails 7+ with those gaps in mind.

| Feature | `acts_as_tenant` | `rails-tenantify` |
| --- | --- | --- |
| **Rails 7+ Focus** | Partial | **Yes** |
| **Subdomain Resolver** | DIY | `set_tenant_by :subdomain` |
| **API Header Resolver** | DIY | `set_tenant_by :header` |
| **Sidekiq Retry + Tenant** | Known issues | **Tenant ID preserved in job payload** |
| **`update_all` / `delete_all**` | Unreliable protection | **Raises unless explicitly scoped** |
| **Cross-Tenant Associations** | Manual checks | **Built-in validation** |
| **Unsafe Tenant Swap** | No audit | Options: `:log`, `:raise`, or `:ignore` |
| **Test Helpers** | Partial | Native `with_tenant` / `without_tenant` |

---

## How it compares to `Apartment`

`Apartment` uses separate schemas or databases per tenant. That offers strong isolation, but you pay a steep price in migrations, backups, and connection management.

`rails-tenantify` keeps one database and scopes via a foreign key—the exact tradeoff most B2B SaaS products actually want.

| Feature | `Apartment` | `rails-tenantify` |
| --- | --- | --- |
| **Isolation Model** | Schema / DB per tenant | Row-level Foreign Key |
| **Migrations** | Per-tenant complexity | Standard Rails migration path |
| **Ops Overhead** | Higher | **Lower** |
| **Best Fit For** | Strict regulatory separation | Typical B2B SaaS |

---

## Key Features

### 1. Multi-Resolution Strategies

Resolve your tenant out of the box via subdomains or API headers:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;/p&gt;
&lt;h1&gt;
  
  
  Subdomain resolution (excludes common marketing/admin routes)
&lt;/h1&gt;

&lt;p&gt;set_tenant_by :subdomain, exclude: %w[www admin]&lt;/p&gt;
&lt;h1&gt;
  
  
  API resolution for mobile or JSON clients
&lt;/h1&gt;

&lt;p&gt;set_tenant_by :header, header: "X-Tenant-ID"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Or resolve manually based on the session:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
before_action do&lt;br&gt;
  Tenantify.current_tenant = current_user.organization if user_signed_in?&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 2. Bulletproof Background Jobs

The tenant automatically survives enqueueing and execution via standard **ActiveJob**:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
class ExportJob &amp;lt; ApplicationJob&lt;br&gt;
  def perform&lt;br&gt;
    Tenantify.current_tenant   # Automatically set to the org that enqueued the job&lt;br&gt;
    Project.find_each { |p| p.update!(exported: true) }&lt;br&gt;
  end&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
*Note: If you use **Sidekiq**, the middleware registers automatically and injects the `tenant_id` straight into the job hash.*

### 3. Bulk-Write Protection

Avoid the ultimate multi-tenancy footgun: accidental database-wide updates.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
Project.update_all(status: "archived")   # Raises an error if not explicitly scoped to current tenant&lt;br&gt;
Project.unscoped.update_all(...)        # Raises Tenantify::TenantMismatchError&lt;/p&gt;
&lt;h1&gt;
  
  
  Explicitly allowed bypass:
&lt;/h1&gt;

&lt;p&gt;Tenantify.without_tenant do&lt;br&gt;
  Project.update_all(status: "migrated")&lt;br&gt;&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 4. Cross-Tenant Association Checks

Prevent data leaking through relationships.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
task.project = project_from_other_org&lt;br&gt;
task.valid? # =&amp;gt; false (Errors: "belongs to a different tenant")&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 5. Override Auditing

Catch risky code that unexpectedly swaps tenants mid-request.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
Tenantify.configure { |c| c.audit_overrides = :raise }&lt;/p&gt;

&lt;p&gt;Tenantify.current_tenant = org_a&lt;br&gt;
Tenantify.current_tenant = org_b  # =&amp;gt; Raises Tenantify::TenantOverrideError&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

## Installation &amp;amp; Setup

Add the gem to your `Gemfile`:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
gem "rails-tenantify", "~&amp;gt; 0.1.2", require: "rails-tenantify"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Run `bundle install` and create an initializer at `config/initializers/tenantify.rb`:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;br&gt;
Tenantify.configure do |config|&lt;br&gt;
  config.tenant_model = "Organization"&lt;br&gt;
  config.on_tenant_not_found = :raise   # Options: :raise | :redirect | :null_tenant&lt;br&gt;
  config.audit_overrides = :log          # Options: :log | :raise | :ignore&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Add the tenant foreign key to your tables:

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
rails g model Organization name:string subdomain:string:uniq&lt;br&gt;
rails g migration AddOrganizationToProjects organization:references&lt;br&gt;
rails db:migrate&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;gt; ⚠️ **Note on Naming:** The gem is published as `rails-tenantify` on RubyGems but required as `"rails-tenantify"`. (The old, legacy `tenantify` name on RubyGems belongs to an unrelated, abandoned library from 2016).

### Built-In Testing Helpers (RSpec Friendly)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
ruby&lt;/p&gt;
&lt;h1&gt;
  
  
  spec/rails_helper.rb
&lt;/h1&gt;

&lt;p&gt;RSpec.configure { |c| c.include Tenantify::TestHelpers }&lt;/p&gt;
&lt;h1&gt;
  
  
  In your specs:
&lt;/h1&gt;

&lt;p&gt;it "scopes creation automatically" do&lt;br&gt;
  with_tenant(org_a) do&lt;br&gt;
    exam = Exam.create!(title: "Midterm")&lt;br&gt;
    expect(exam.organization).to eq(org_a)&lt;br&gt;
  end&lt;br&gt;
end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

## Roadmap: Subdomains vs Custom Domains

* **Supported today:** Tenant subdomains on your primary application host (e.g., `greenwood.yourapp.com` → `Organization.find_by(subdomain: "greenwood")`).
* **Coming soon:** Full custom apex domains (e.g., `greenwood.edu` resolving via host-based lookups and custom DNS mapping).

---

## Why I Built This

I was building a multi-tenant Online Exam System in Rails—multiple schools, each with their own admins, teachers, exams, and students. I needed Greenwood School to absolutely *never* see Riverside School's data. Not in the UI, not in a background job, and definitely not via a copy-pasted `update_all` statement in a production console.

I didn't want the DevOps nightmare of managing a schema-per-school, and I didn't want to fork unmaintained tenancy gems. I wanted to run `bin/rails tenantify:verify` after my seeds ran and sleep soundly knowing data isolation was working perfectly.

So, I built `rails-tenantify`, integrated it into production, and open-sourced v0.1.2.

## Links &amp;amp; Feedback

* **GitHub:** [github.com/sghani001/rails-tenantify](https://www.google.com/search?q=https://github.com/sghani001/rails-tenantify)
* **RubyGems:** [rubygems.org/gems/rails-tenantify](https://www.google.com/search?q=https://rubygems.org/gems/rails-tenantify)

If you give it a try, I'd love to hear your thoughts! Open an issue, leave a comment below, or drop a ⭐ on the GitHub repository.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tenant</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built rails-persona — behavioral analytics for Rails with zero external services</title>
      <dc:creator>Syed Ghani</dc:creator>
      <pubDate>Sun, 31 May 2026 16:35:04 +0000</pubDate>
      <link>https://dev.to/dev_syed_ghani/i-built-rails-persona-behavioral-analytics-for-rails-with-zero-external-services-c7k</link>
      <guid>https://dev.to/dev_syed_ghani/i-built-rails-persona-behavioral-analytics-for-rails-with-zero-external-services-c7k</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every SaaS app eventually needs to answer the same questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which features do my users actually use?&lt;/li&gt;
&lt;li&gt;Who are my most active users?&lt;/li&gt;
&lt;li&gt;When did a user last do something meaningful?&lt;/li&gt;
&lt;li&gt;Which users are going inactive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The typical answer is "add Mixpanel" or "set up Segment." But that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending your user data to a third party&lt;/li&gt;
&lt;li&gt;Paying for another service&lt;/li&gt;
&lt;li&gt;Adding a JS snippet&lt;/li&gt;
&lt;li&gt;Learning another dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most Rails apps, this is overkill. The data you need is already in your database.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;rails-persona&lt;/strong&gt; is a Ruby gem that adds model-level behavioral analytics directly to your ActiveRecord models. No external services, no JS, no cookies — just your own database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Persona&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Trackable&lt;/span&gt;

  &lt;span class="n"&gt;persona&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:login&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:export_report&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:view_dashboard&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:upgrade_plan&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then track actions anywhere in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:login&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:upgrade_plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;metadata: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;plan: &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And query behavior instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_frequent_action&lt;/span&gt;       &lt;span class="c1"&gt;# =&amp;gt; :login&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inactive_since?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persona_summary&lt;/span&gt;            &lt;span class="c1"&gt;# =&amp;gt; { login: 42, export_report: 5 }&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;daily_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# =&amp;gt; { "2024-05-28" =&amp;gt; 4, "2024-05-29" =&amp;gt; 7 }&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;peak_hour&lt;/span&gt;                  &lt;span class="c1"&gt;# =&amp;gt; 14  (2pm)&lt;/span&gt;
&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persona_leaderboard&lt;/span&gt;        &lt;span class="c1"&gt;# =&amp;gt; top 10 most active users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it's different from ahoy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ankane/ahoy" rel="noopener noreferrer"&gt;ahoy&lt;/a&gt; is a great gem but it solves a different problem — it tracks HTTP visits and page views. rails-persona tracks what users &lt;strong&gt;do&lt;/strong&gt; inside your app at the model level.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ahoy&lt;/th&gt;
&lt;th&gt;rails-persona&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Focus&lt;/td&gt;
&lt;td&gt;HTTP visits + page views&lt;/td&gt;
&lt;td&gt;Model actions + behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs JS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async built-in&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Sidekiq)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk tracking&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (insert_all!)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class leaderboards&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on any model&lt;/td&gt;
&lt;td&gt;Awkward&lt;/td&gt;
&lt;td&gt;First-class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key features
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Async tracking&lt;/strong&gt; — never slow down a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Persona&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# fires a Sidekiq job&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bulk tracking&lt;/strong&gt; — uses insert_all! under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulk_track!&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:view_dashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:export_report&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open tracking&lt;/strong&gt; — skip the whitelist entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;persona&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;open_tracking!&lt;/span&gt;  &lt;span class="c1"&gt;# any string is valid&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Data pruning&lt;/strong&gt; — keep your DB clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Persona&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max_events_per_record&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auto_prune_after_days&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Works on any model&lt;/strong&gt; — not just User:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Persona&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Trackable&lt;/span&gt;

  &lt;span class="n"&gt;persona&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:viewed&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:shared&lt;/span&gt;
    &lt;span class="n"&gt;track&lt;/span&gt; &lt;span class="ss"&gt;:bookmarked&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persona_class_summary&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; { viewed: 50_420, shared: 890 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rails-persona"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I was the sole engineer on a production SaaS called CinnaLab PRM — an AI-powered Partner Relationship Management platform. I kept wanting to know things like "which users are using the partner portal vs ignoring it?" and "who's about to churn based on inactivity?"&lt;/p&gt;

&lt;p&gt;I didn't want to set up Mixpanel for an internal SaaS. I just wanted to query my own database. rails-persona is what I wish had existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/sghani001/rails-persona" rel="noopener noreferrer"&gt;github.com/sghani001/rails-persona&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RubyGems: &lt;a href="https://rubygems.org/gems/rails-persona" rel="noopener noreferrer"&gt;rubygems.org/gems/rails-persona&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, I'd love feedback — open an issue or leave a comment below!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
