<?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: Jason Meller</title>
    <description>The latest articles on DEV Community by Jason Meller (@terracatta).</description>
    <link>https://dev.to/terracatta</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F227092%2Ffb0383b8-b859-4753-a46b-788b573220f2.jpeg</url>
      <title>DEV Community: Jason Meller</title>
      <link>https://dev.to/terracatta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/terracatta"/>
    <language>en</language>
    <item>
      <title>A Rails Multi-Tenant Strategy That's ~30 Lines and "Just Works"</title>
      <dc:creator>Jason Meller</dc:creator>
      <pubDate>Tue, 09 Nov 2021 15:33:14 +0000</pubDate>
      <link>https://dev.to/kolide/a-rails-multi-tenant-strategy-thats-30-lines-and-just-works-58cd</link>
      <guid>https://dev.to/kolide/a-rails-multi-tenant-strategy-thats-30-lines-and-just-works-58cd</guid>
      <description>&lt;p&gt;When engineering a new SaaS app, how you plan to handle customer data tenancy is usually one of the first decisions you and your team will need to make. If you are writing a Rails app and decide on a multi-tenant strategy (one instance of the app serves many customers), then this article is for you.&lt;/p&gt;

&lt;p&gt;I contend that modern Rails has everything you need to build a multi-tenant strategy that scales, is easy for others to use, and can be written in just a handful of lines of code. Kolide (&lt;a href="https://angel.co/company/kolideco/jobs/1638302-rails-engineer" rel="noopener noreferrer"&gt;btw we're hiring&lt;/a&gt;) has been using this simple strategy since the inception of its product, and it's been one of the most elegant and easiest to understand parts of our code-base. &lt;/p&gt;

&lt;p&gt;So before reaching for a gem, consider if the following meets your needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code
&lt;/h3&gt;

&lt;p&gt;The entire implementation is contained in just two files and requires no additional dependencies.&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;# app/models/concerns/account_ownable.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AccountOwnable&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Account is actually not optional, but we not do want&lt;/span&gt;
    &lt;span class="c1"&gt;# to generate a SELECT query to verify the account is&lt;/span&gt;
    &lt;span class="c1"&gt;# there every time. We get this protection for free&lt;/span&gt;
    &lt;span class="c1"&gt;# because of the `Current.account_or_raise!`     &lt;/span&gt;
    &lt;span class="c1"&gt;# and also through FK constraints.&lt;/span&gt;
    &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;optional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;default_scope&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;account: &lt;/span&gt;&lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account_or_raise!&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/current.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Current&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CurrentAttributes&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;resets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MissingCurrentAccount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;account_or_raise!&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MissingCurrentAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"You must set an account with Current.account="&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;

    &lt;span class="n"&gt;account&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;      &lt;span class="o"&gt;=&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;account&lt;/span&gt;
    &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zone&lt;/span&gt;         &lt;span class="o"&gt;=&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;time_zone&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;That's it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using the Code In Practice
&lt;/h3&gt;

&lt;p&gt;To use this code, simply mix-in the concern into any standard ActiveRecord model like so...&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;ApiKey&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="c1"&gt;# assumes table has a column named `account_id`&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;AccountOwnable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When a user of ours signs in, all we need to do is simply set &lt;code&gt;Current.user&lt;/code&gt; in our authentication controller concern which is mixed into our &lt;code&gt;ApplicationController&lt;/code&gt;&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;# app/controllers/concerns/require_authentication.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RequireAuthentication&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:ensure_authenticated_user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_authenticated_user&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&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;find_by_valid_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;signin_path&lt;/span&gt;
    &lt;span class="k"&gt;end&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;For this small amount of effort we now get the following benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Because of the &lt;code&gt;default_scope&lt;/code&gt;, once a user is signed in, data from sensitive models is automatically scoped to their account. We just don't need to think about it, no matter how complicated our query chaining gets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Again, because of the &lt;code&gt;default_scope&lt;/code&gt; creating new records for these &lt;code&gt;AccountOwnable&lt;/code&gt; models will automatically set the &lt;code&gt;account_id&lt;/code&gt; for us. One less thing to think about.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In situations where we are outside of the standard Rails request/response paradigm (ex: in an ActiveJob) any &lt;code&gt;AccountOwnable&lt;/code&gt; models will raise if &lt;code&gt;Current.account&lt;/code&gt; is not set. This &lt;em&gt;forces&lt;/em&gt; us to constantly think about how we are scoping data for customer needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The situations where we need to enumerate through more than one tenant's data at a time are still possible but now require a &lt;code&gt;Model.unscoped&lt;/code&gt; which can be easily scanned for in linters requiring engineers to justify their rationale on a per use-case basis.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing that became slightly annoying was constantly setting &lt;code&gt;Current.account =&lt;/code&gt; in the Rails console. To make that much easier we wrote a simple console command.&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;# lib/kolide/console.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Console&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Current account switched to &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# in config/application.rb&lt;/span&gt;
&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'kolide/console'&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;ConsoleMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt; &lt;span class="ss"&gt;:include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Kolide&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Console&lt;/span&gt;
  &lt;span class="no"&gt;TOPLEVEL_BINDING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;Kolide&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Console&lt;/span&gt; &lt;span class="c1"&gt;# PRY&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now we simply run &lt;code&gt;t 1&lt;/code&gt; when we want to switch the tenant with an id of 1. Much better.&lt;/p&gt;

&lt;p&gt;In the test suite, you should also reset &lt;code&gt;Current&lt;/code&gt; before each spec/test as it's not done for you automatically. For us that was simply a matter of adding...&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;# spec/spec_helper.rb&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;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now we don't have to worry about our global state being polluted when running our specs serially in the same process.&lt;/p&gt;
&lt;h3&gt;
  
  
  Concerns we had that didn't end up being an issue
&lt;/h3&gt;

&lt;p&gt;Kolide has been successfully using this strategy since the inception of our Ruby on Rails SaaS app. While we arrived at this strategy in the first few days of our app's formation, we definitely were less confident in the approach. Here is a list of concerns we held and how they ended up panning out.&lt;/p&gt;
&lt;h4&gt;
  
  
  Will this approach be acceptable to our customers?
&lt;/h4&gt;

&lt;p&gt;Kolide is a device security company, and since our buyers are likely to be either security engineers or security minded IT staff, the bar we need to meet is much higher than the normal SaaS company. We were nervous that an app-enforced constraint would &lt;em&gt;feel&lt;/em&gt; flimsy, despite how well it works in practice.&lt;/p&gt;

&lt;p&gt;In reality, we found the opposite. Customers were mostly ambivalent about our app-enforced constraint approach. Why? It's because it's an approach that's common among their other vendors and matches their pre-conceived expectations about how most SaaS software works. Matched expectations = less concern.&lt;/p&gt;

&lt;p&gt;In prior iterations of our app where we did extreme things like spin up separate Kubernetes namespaces and DBs for each customer, we found our efforts were paradoxically met with more concern, not less.  This concern manifested as additional process, review, and ultimately unnecessary friction as our buyers grappled to bring more and more technical folks into the procurement process to simply understand the unfamiliar architecture.&lt;/p&gt;

&lt;p&gt;With our current approach, our development and deployment story is simpler, and simplicity has significant security advantages.&lt;/p&gt;
&lt;h4&gt;
  
  
  Current.rb is too magical, will multi-threading in production cause someone's &lt;code&gt;default_scope&lt;/code&gt; to leak to another request?
&lt;/h4&gt;

&lt;p&gt;There is a lot of consternation in the Rails community when DHH introduced the &lt;code&gt;CurrentAttributes&lt;/code&gt; paradigm in Rails 5.2. DHH talks about his rationale for adding this in his Youtube video entitled, "Using globals when the price is right".&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/D7zUOtlpUPw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Ryan Bigg on the other-hand felt this addition to Rails would cause developers to write a lot of code with unpredictable behavior expressed these views in his blog post entitled, &lt;a href="https://ryanbigg.com/2017/06/current-considered-harmful" rel="noopener noreferrer"&gt;"Rails' CurrentAttributes considered harmful"&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After reading more into the original PR...&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/rails/rails/pull/29180" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        ActiveSupport::CurrentAttributes provides a thread-isolated attributes singleton
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#29180&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/dhh" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F2741%3Fv%3D4" alt="dhh avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/dhh" rel="noopener noreferrer"&gt;dhh&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/rails/rails/pull/29180" rel="noopener noreferrer"&gt;&lt;time&gt;May 22, 2017&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;Abstract super class that provides a thread-isolated attributes singleton.
Primary use case is keeping all the per-request attributes easily available to the whole system.&lt;/p&gt;
&lt;p&gt;The following full app-like example demonstrates how to use a Current class to
facilitate easy access to the global, per-request attributes without passing them deeply
around everywhere:&lt;/p&gt;
&lt;div class="highlight highlight-source-ruby js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# app/models/current.rb&lt;/span&gt;
&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Current&lt;/span&gt; &amp;lt; &lt;span class="pl-v"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="pl-v"&gt;CurrentAttributes&lt;/span&gt;
  &lt;span class="pl-en"&gt;attribute&lt;/span&gt; &lt;span class="pl-pds"&gt;:account&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-pds"&gt;:user&lt;/span&gt;
  &lt;span class="pl-en"&gt;attribute&lt;/span&gt; &lt;span class="pl-pds"&gt;:request_id&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-pds"&gt;:user_agent&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-pds"&gt;:ip_address&lt;/span&gt; 
  
  &lt;span class="pl-en"&gt;resets&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Time&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;zone&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;nil&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  
  &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;user&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;user&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
    &lt;span class="pl-smi"&gt;super&lt;/span&gt;
    &lt;span class="pl-smi"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;account&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;user&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;account&lt;/span&gt;
    &lt;span class="pl-v"&gt;Time&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;zone&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;user&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;time_zone&lt;/span&gt;
  &lt;span class="pl-k"&gt;end&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;

&lt;span class="pl-c"&gt;# app/controllers/concerns/authentication.rb&lt;/span&gt;
&lt;span class="pl-k"&gt;module&lt;/span&gt; &lt;span class="pl-v"&gt;Authentication&lt;/span&gt;
  &lt;span class="pl-en"&gt;extend&lt;/span&gt; &lt;span class="pl-v"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="pl-v"&gt;Concern&lt;/span&gt;

  &lt;span class="pl-en"&gt;included&lt;/span&gt; &lt;span class="pl-k"&gt;do&lt;/span&gt;
    &lt;span class="pl-en"&gt;before_action&lt;/span&gt; &lt;span class="pl-pds"&gt;:set_current_authenticated_user&lt;/span&gt;
  &lt;span class="pl-k"&gt;end&lt;/span&gt;

  &lt;span class="pl-k"&gt;private&lt;/span&gt;
    &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;set_current_authenticated_user&lt;/span&gt;
      &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;User&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;find&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;cookies&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;signed&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-pds"&gt;:user_id&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
    &lt;span class="pl-k"&gt;end&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;

&lt;span class="pl-c"&gt;# app/controllers/concerns/set_current_request_details.rb&lt;/span&gt;
&lt;span class="pl-k"&gt;module&lt;/span&gt; &lt;span class="pl-v"&gt;SetCurrentRequestDetails&lt;/span&gt;
  &lt;span class="pl-en"&gt;extend&lt;/span&gt; &lt;span class="pl-v"&gt;ActiveSupport&lt;/span&gt;::&lt;span class="pl-v"&gt;Concern&lt;/span&gt;

  &lt;span class="pl-en"&gt;included&lt;/span&gt; &lt;span class="pl-k"&gt;do&lt;/span&gt;
    &lt;span class="pl-en"&gt;before_action&lt;/span&gt; &lt;span class="pl-k"&gt;do&lt;/span&gt;
      &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;request_id&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;request&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;uuid&lt;/span&gt;
      &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user_agent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;request&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user_agent&lt;/span&gt;
      &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;ip_address&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;request&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;ip&lt;/span&gt;
    &lt;span class="pl-k"&gt;end&lt;/span&gt;
  &lt;span class="pl-k"&gt;end&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;  

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;ApplicationController&lt;/span&gt; &amp;lt; &lt;span class="pl-v"&gt;ActionController&lt;/span&gt;::&lt;span class="pl-v"&gt;Base&lt;/span&gt;
  &lt;span class="pl-en"&gt;include&lt;/span&gt; &lt;span class="pl-v"&gt;Authentication&lt;/span&gt;
  &lt;span class="pl-en"&gt;include&lt;/span&gt; &lt;span class="pl-v"&gt;SetCurrentRequestDetails&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;MessagesController&lt;/span&gt; &amp;lt; &lt;span class="pl-v"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;create&lt;/span&gt;
    &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;account&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;message_params&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-k"&gt;end&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Message&lt;/span&gt; &amp;lt; &lt;span class="pl-v"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="pl-en"&gt;belongs_to&lt;/span&gt; &lt;span class="pl-pds"&gt;:creator&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-pds"&gt;default&lt;/span&gt;: &lt;span class="pl-c1"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-en"&gt;after_create&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; |&lt;span class="pl-s1"&gt;message&lt;/span&gt;| &lt;span class="pl-v"&gt;Event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;record&lt;/span&gt;: &lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;

&lt;span class="pl-k"&gt;class&lt;/span&gt; &lt;span class="pl-v"&gt;Event&lt;/span&gt; &amp;lt; &lt;span class="pl-v"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="pl-en"&gt;before_create&lt;/span&gt; &lt;span class="pl-k"&gt;do&lt;/span&gt;
    &lt;span class="pl-smi"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;request_id&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;request_id&lt;/span&gt;
    &lt;span class="pl-smi"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user_agent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;user_agent&lt;/span&gt;
    &lt;span class="pl-smi"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;ip_address&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Current&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;ip_address&lt;/span&gt;
  &lt;span class="pl-k"&gt;end&lt;/span&gt;
&lt;span class="pl-k"&gt;end&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
Current should only be used for a few, top-level globals, like account, user, and request details.
The attributes stuck in Current should be used by more or less all actions on all requests. If you start
sticking controller-specific attributes in there, you're going to create a mess.&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rails/rails/pull/29180" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;I found a lot of thoughtful consideration for how to make this code truly thread-safe which convinced me to bet big on this approach. &lt;/p&gt;

&lt;p&gt;Now over two years later, I can say with confidence that in our app, which serves nearly 1,800 HTTP requests per second across hundreds of different tenants, this code works as described in a real production setting. &lt;/p&gt;

&lt;h4&gt;
  
  
  Will setting &lt;code&gt;Current.account&lt;/code&gt; in asynchronous jobs or in the test suite become tiresome or problematic?
&lt;/h4&gt;

&lt;p&gt;No, the ceremony here is worth it because it forces us to think carefully about how we are acting on our customer's data. Situations where we need to iterate through more than one customer's data are also trivial to achieve through an &lt;code&gt;#each&lt;/code&gt; block.&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;ApiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unscoped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_each&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;api_key&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;
  &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert_to_new_format&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;We are now sharing this simplistic approach because it's worked so well for us at Kolide. I imagine other burgeoning Rails apps considering a multi-tenant strategy will appreciate being able to do this in their own codebase vs offload something so important to an external gem.&lt;/p&gt;

&lt;p&gt;We will continue to share our learnings as Kolide grows. In fact prepping for future scale is what I like best about this approach. By marking all of our records with a tenant identifier like &lt;code&gt;account_id&lt;/code&gt; we gain the future option of leveraging more sophisticated solutions at the PostgreSQL level &lt;a href="https://edgeguides.rubyonrails.org/active_record_multiple_databases.html" rel="noopener noreferrer"&gt;like multi-DB sharding&lt;/a&gt; or &lt;a href="https://www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/" rel="noopener noreferrer"&gt;even products like Citus&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I hope you found this post useful. If you found any errors in this guide or suggestions to improve it, please reach out in the comments or hit me up on twitter &lt;a href="https://twitter.com/jmeller" rel="noopener noreferrer"&gt;@jmeller&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://angel.co/company/kolideco/jobs/1638302-rails-engineer" rel="noopener noreferrer"&gt;Oh, and we're hiring!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>saas</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Migrate a Rails 6 App From sass-rails to cssbundling-rails</title>
      <dc:creator>Jason Meller</dc:creator>
      <pubDate>Thu, 21 Oct 2021 02:59:07 +0000</pubDate>
      <link>https://dev.to/kolide/how-to-migrate-a-rails-6-app-from-sass-rails-to-cssbundling-rails-4l41</link>
      <guid>https://dev.to/kolide/how-to-migrate-a-rails-6-app-from-sass-rails-to-cssbundling-rails-4l41</guid>
      <description>&lt;p&gt;At Kolide (&lt;a href="https://angel.co/company/kolideco/jobs/1638302-rails-engineer" rel="noopener noreferrer"&gt;btw we're hiring&lt;/a&gt;), we move swiftly to adopt to new versions of Ruby, Rails, and other major dependencies within a few months of them becoming available. We are at our happiest when we get to use the latest language and framework features. Additionally, forging ahead to uncharted waters allows us to contribute back bug reports, PRs, and guides for other rubyists also interested on being on the bleeding edge.&lt;/p&gt;

&lt;p&gt;To that end, I wanted to share our recent experience with upgrading our production Rails 6.1 app from sprockets/sass-rails to the brand-new cssbundling-rails. If you are considering an eventual transition to Rails 7, this is a great first step in that direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;cssbundling-rails&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Or, more precisely, why move aways from &lt;code&gt;sass-rails&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the webpacker gem was released as part of &lt;a href="https://weblog.rubyonrails.org/2017/2/23/Rails-5-1-beta1/" rel="noopener noreferrer"&gt;Rails 5.1 release in 2017&lt;/a&gt;, DHH made it clear that while webpacker &lt;em&gt;could&lt;/em&gt; be used to bundle CSS, he highly recommended to keep it simple and continue to use the Rails asset pipeline. &lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-808349072734027776-654" src="https://platform.twitter.com/embed/Tweet.html?id=808349072734027776"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-808349072734027776-654');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=808349072734027776&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Heeding this advice, many Rails projects (like ours) continue to dutifully serve their SCSS files via sprockets and the &lt;code&gt;sass-rails&lt;/code&gt; gem, the same way it's been doing circa Rails 3.1 in 2011.&lt;/p&gt;

&lt;p&gt;This sprockets setup has always worked great, but lately some serious bit-rot has set in. Over the last few years, the &lt;a href="https://sass-lang.com" rel="noopener noreferrer"&gt;Sass Team&lt;/a&gt; has deprecated both its original ruby-based version of &lt;code&gt;sass&lt;/code&gt;, and more recently, the &lt;code&gt;libsass/sassc&lt;/code&gt; library in favor of &lt;a href="https://sass-lang.com/dart-sass" rel="noopener noreferrer"&gt;dart-sass&lt;/a&gt;. As of this writing, I could not find any sprockets compatible versions of &lt;code&gt;dart-sass&lt;/code&gt;. Further, as time marches on, the sassc gem is beginning to accumulate some pretty &lt;a href="https://github.com/sass/sassc-ruby/issues/197" rel="noopener noreferrer"&gt;nasty bugs&lt;/a&gt; and &lt;a href="https://github.com/sass/sassc-ruby/issues/200" rel="noopener noreferrer"&gt;inefficiencies&lt;/a&gt;. With no fixes on the horizon, it's time to move on.&lt;/p&gt;

&lt;p&gt;This is where the newly minted &lt;a href="https://github.com/rails/cssbundling-rails" rel="noopener noreferrer"&gt;&lt;code&gt;cssbundling-rails&lt;/code&gt;&lt;/a&gt; comes in. Inspired by the also new &lt;a href="https://github.com/rails/jsbundling-rails" rel="noopener noreferrer"&gt;&lt;code&gt;jsbundling-rails&lt;/code&gt;&lt;/a&gt; library, it allows folks to leverage &lt;code&gt;yarn/npm&lt;/code&gt; to build a much simpler and more canonical CSS processing pipeline with a setup that will be familiar to JS developers.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rails" rel="noopener noreferrer"&gt;
        rails
      &lt;/a&gt; / &lt;a href="https://github.com/rails/cssbundling-rails" rel="noopener noreferrer"&gt;
        cssbundling-rails
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Bundle and process CSS in Rails with Tailwind, PostCSS, and Sass via Node.js.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CSS Bundling for Rails&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Use &lt;a href="https://tailwindcss.com" rel="nofollow noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;, &lt;a href="https://getbootstrap.com/" rel="nofollow noopener noreferrer"&gt;Bootstrap&lt;/a&gt;, &lt;a href="https://bulma.io/" rel="nofollow noopener noreferrer"&gt;Bulma&lt;/a&gt;, &lt;a href="https://postcss.org" rel="nofollow noopener noreferrer"&gt;PostCSS&lt;/a&gt;, or &lt;a href="https://sass-lang.com/" rel="nofollow noopener noreferrer"&gt;Dart Sass&lt;/a&gt; to bundle and process your CSS, then deliver it via the asset pipeline in Rails. This gem provides installers to get you going with the bundler of your choice in a new Rails application, and a convention to use &lt;code&gt;app/assets/builds&lt;/code&gt; to hold your bundled output as artifacts that are not checked into source control (the installer adds this directory to &lt;code&gt;.gitignore&lt;/code&gt; by default).&lt;/p&gt;
&lt;p&gt;You develop using this approach by running the bundler in watch mode in a terminal with &lt;code&gt;yarn build:css --watch&lt;/code&gt; (and your Rails server in another, if you're not using something like &lt;a href="https://github.com/puma/puma-dev" rel="noopener noreferrer"&gt;puma-dev&lt;/a&gt;). You can also use &lt;code&gt;./bin/dev&lt;/code&gt;, which will start both the Rails server and the CSS build watcher (along with a JS build watcher, if you're also using &lt;code&gt;jsbundling-rails&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Whenever the bundler detects changes to any…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rails/cssbundling-rails" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;With our mission set, let's roll up our sleeves and get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading a Rails 6.x App
&lt;/h2&gt;

&lt;p&gt;According to its gemspec, &lt;code&gt;cssbundling-rails&lt;/code&gt; is not just for new Rails 7 apps, it's also compatible with 6.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 - Prepare Your &lt;code&gt;Gemfile&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Our goal is to not just transition to &lt;code&gt;cssbundling-rails&lt;/code&gt;, but to also remove &lt;code&gt;sass-rails&lt;/code&gt; gem.  To get started, remove &lt;code&gt;sass-rails&lt;/code&gt; and any other potential references to sass like &lt;code&gt;sass-ruby&lt;/code&gt; and &lt;code&gt;sassc&lt;/code&gt; (if defined).&lt;/p&gt;

&lt;p&gt;Next, add &lt;code&gt;gem cssbundling-rails, '&amp;gt;= 0.2.4'&lt;/code&gt; (the version at the time of this writing) and run &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Prepare Your SCSS Files
&lt;/h3&gt;

&lt;p&gt;First, let's take some inventory. Open up &lt;code&gt;config/initializers/assets.rb&lt;/code&gt; and at the bottom of that file you will see something like the following:&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;# Precompile additional assets.&lt;/span&gt;
&lt;span class="c1"&gt;# application.js, application.css, and all non-JS/CSS in the app/assets&lt;/span&gt;
&lt;span class="c1"&gt;# folder are already added.&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;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;precompile&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sx"&gt;%w( sessions.css staff.css marketing.css )&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;If the &lt;code&gt;Rails.application.config.assets.precompile&lt;/code&gt; line is uncommented, take note of the &lt;code&gt;.css&lt;/code&gt; files referenced in the array. In addition to &lt;code&gt;application.css&lt;/code&gt; these are all separate top-level CSS files that we will need to convert to the new format.&lt;/p&gt;

&lt;p&gt;For each file, (or just &lt;code&gt;application.[s]css&lt;/code&gt;) you should do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If you haven't already, convert the files to use SCSS's &lt;code&gt;@import&lt;/code&gt; syntax instead of the sprockets magic comments like &lt;code&gt;*= require_self&lt;/code&gt; or &lt;code&gt;*= require_tree&lt;/code&gt;. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rename each file to match this format &lt;code&gt;&amp;lt;name&amp;gt;.sass.scss&lt;/code&gt; (so &lt;code&gt;application.scss&lt;/code&gt; would become &lt;code&gt;application.sass.scss&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3 - Run the Installation (And Then Fix What It Broke)
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;./bin/rails css:install:sass&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you receive an overwrite warning for &lt;code&gt;app/assets/stylesheets/application.sass.scss&lt;/code&gt;, you can respond with &lt;code&gt;N&lt;/code&gt; when prompted.&lt;/p&gt;

&lt;p&gt;After the installation completes, we need to clean up a few things.&lt;/p&gt;

&lt;p&gt;First, in our case, the installation inserted an extra &lt;code&gt;stylesheet_link_tag&lt;/code&gt; at the bottom of &lt;code&gt;app/views/layout/application.html.erb&lt;/code&gt;. You should delete this extra line.&lt;/p&gt;

&lt;p&gt;Second, while the installation command updates the &lt;code&gt;app/assets/config/manifest.js&lt;/code&gt; file with a few new lines, it often doesn't remove explicit references to any sass files. After the upgrade, ours looks like this:&lt;/p&gt;

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

&lt;span class="c1"&gt;//= link_tree ../images&lt;/span&gt;
&lt;span class="c1"&gt;//= link_tree ../fonts&lt;/span&gt;
&lt;span class="c1"&gt;//= link_tree ../builds&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: We added the line for &lt;code&gt;fonts&lt;/code&gt; as we use the &lt;code&gt;font-url&lt;/code&gt; helper in our SCSS files. These fonts didn't need to be explicitly included in the manifest before because sprockets would include them dynamically as they were referenced in the source CSS file. After this upgrade sprockets isn't processing the file so it's important that we ensure it's in the manifest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally, the &lt;code&gt;build:css&lt;/code&gt; script the installation creates in &lt;code&gt;package.json&lt;/code&gt; is only sufficient if you only have one main &lt;code&gt;application.scss&lt;/code&gt;, if you have other files you need to output, you are going to need to modify the script's contents. If this is the case, my suggestion is to create a new file called &lt;code&gt;bin/build-css&lt;/code&gt; and do something like the following:&lt;/p&gt;

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

&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

./node_modules/sass/sass.js &lt;span class="se"&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css &lt;span class="se"&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/sessions.sass.scss:./app/assets/builds/sessions.css &lt;span class="se"&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/staff.sass.scss:./app/assets/builds/staff.css &lt;span class="se"&gt;\&lt;/span&gt;
  ./app/assets/stylesheets/marketing.sass.scss:./app/assets/builds/marketing.css &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-source-map&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node_modules &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;$@&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;$@&lt;/code&gt; at the bottom ensures we pass along any additional arguments like &lt;code&gt;--watch&lt;/code&gt; when this is invoked via &lt;code&gt;bin/dev&lt;/code&gt; (more on that later).&lt;/p&gt;

&lt;p&gt;Now in the &lt;code&gt;package.json&lt;/code&gt; file do the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:css"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./bin/build-css"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;Don't forget to also run &lt;code&gt;chmod 755 ./bin/build-css&lt;/code&gt; in your terminal before moving on to the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 - Handle &lt;code&gt;asset-url&lt;/code&gt; And Friends
&lt;/h3&gt;

&lt;p&gt;Often in Rails, you need to reference files from the &lt;code&gt;app/assets/images&lt;/code&gt; or &lt;code&gt;app/asset/fonts&lt;/code&gt; folders directly in CSS. Since sprockets computes hashes for each asset, you can't just hard-code the name of the asset in there. To work around this, sprockets introduced helper functions like &lt;code&gt;asset-url&lt;/code&gt;, &lt;code&gt;font-url&lt;/code&gt;, and &lt;code&gt;image-url&lt;/code&gt; that resolve the relative path to the asset correctly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dart-sass&lt;/code&gt; has no knowledge of sprockets, so before we finalize the build we need sprockets to run through each file quickly and add these assets paths in. &lt;a href="https://github.com/rails/sprockets-rails/pull/476" rel="noopener noreferrer"&gt;While official support for this is pending&lt;/a&gt;, we arrived at a workaround that seems to do the trick:&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;# config/initializers/asset_url_processor.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AssetUrlProcessor&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:environment&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;context_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/(\w*)-url\(\s*["']?(?!(?:\#|data|http))([^"'\s)]+)\s*["']?\)/&lt;/span&gt;&lt;span class="p"&gt;)&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;_match&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="s2"&gt;"url(&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asset_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vg"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="vg"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&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;Sprockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_postprocessor&lt;/span&gt; &lt;span class="s2"&gt;"text/css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;AssetUrlProcessor&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This regex will match these url functions and convert their contents to the appropriate location of the asset on disk in both development and production. &lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 - Test It Out With &lt;code&gt;bin/dev&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Before trying this out, you'll likely want to clear out any sprockets cache in &lt;code&gt;tmp/&lt;/code&gt;. To do that, you can simply run &lt;code&gt;bin/rake tmp:clear&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As part of the earlier &lt;code&gt;./bin/rails css:install:sass&lt;/code&gt; command, a new file called &lt;code&gt;bin/dev&lt;/code&gt; was created. Additionally, a new gem dependency called &lt;code&gt;foreman&lt;/code&gt; and its associated config file &lt;code&gt;Procfile.dev&lt;/code&gt; was installed.&lt;/p&gt;

&lt;p&gt;By running &lt;code&gt;bin/dev&lt;/code&gt; you are now invoking &lt;code&gt;foreman&lt;/code&gt; which will read the &lt;code&gt;Profile&lt;/code&gt; and simultaneously run the &lt;code&gt;rails server&lt;/code&gt; and the &lt;code&gt;yarn build:css --watch&lt;/code&gt; commands. This should give you a very similar development experience to the original setup where you can make changes to CSS files and, after a refresh, those changes will be immediately reflected in the browser.&lt;/p&gt;

&lt;p&gt;If all went well, &lt;code&gt;bin/dev&lt;/code&gt; should start right up and a visit to your app locally should "just work."&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits &amp;amp; Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;A big thank you to &lt;a href="https://github.com/alxjrvs" rel="noopener noreferrer"&gt;Alex Jarvis&lt;/a&gt; for leading the charge on this upgrade at Kolide and collaborating with me.&lt;/p&gt;

&lt;p&gt;I hope you found this guide useful. If you found any errors in this guide or suggestions to improve it, please reach out in the comments or hit me up on twitter &lt;a href="https://twitter.com/jmeller" rel="noopener noreferrer"&gt;@jmeller&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://angel.co/company/kolideco/jobs/1638302-rails-engineer" rel="noopener noreferrer"&gt;Oh, and we're hiring!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>sass</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
