Tenants in Rails

If I had to guess, this should be made into a Gem. Since I'm lacking the time and effort it would take to do this, I am just going to put it up here. Please feel free to read along, I'm going to create a module in lib/ that should be a simple plugin to create a tenant based rails system. The tenant should be abstract and unobtrusive. Can you make improvements? Please feel free to share. This is also assuming a fresh Ruby on Rails app generated as is or close (version 4.2.3 as of this rant).

  1. Create a Tenant model by running the following command:
    rails g tenant name host token
  2. In your lib/ directory, create a file and name it "tenant_scope.rb"

    module TenantScope
      extend self
    
    
      class Error < StandardError
      end
    
    
      def current
        threadsafe_storage[:current_tenant]
      end
    
    
      def current=(tenant)
        threadsafe_storage[:current_tenant] = tenant
      end
    
    
      def with(tenant)
        previous_scope = current
    
    
    
    raise Error.new("Tenant can't be nil in #{self.name}.with") if tenant.nil?
    
    
    self.current = tenant
    yield(current) if block_given?
    
    ensure self.current = previous_scope nil end private def threadsafe_storage Thread.current[:tenant_scope] ||= {} end end
  3. Create a directory in lib/ called "tenant_scope" and place the following files & content into this directory

    1. model_mixin.rb

      module TenantScope
        module ModelMixin
          def self.included(base)
            base.belongs_to :tenant
            base.validates_presence_of :tenant_id
            base.send(:default_scope, -> {
              if TenantScope.current
                return base.where("#{base.table_name}.tenant_id" => TenantScope.current.id)
              end
              raise Error.new('Scoped class method called without a tenant present')
            })
          end
        end
      end
      
    2. rack.rb # TODO: This assumes a host (tld.com) as your tenant, YMMV. An easy first run test could be Tenant.first (just make sure you have a tenant)

      module TenantScope
        class Rack
          attr_reader :request
          def initialize(app)
            @app = app
          end
          def call(env)
            @request = ::Rack::Request.new(env)
            unless tenant = Tenant.find_by(domain: host.split(".").last(2).join("."))
              logger.error "[TenantScope] tenant not found: #{request.host}"
              return [404, {'Content-Type' => 'text/plain', 'Content-Length' => '29' }, ["This tenant does not exist"]]
            end
            logger.debug "[TenantScope] tenant found: #{tenant.name}"
            TenantScope.with(tenant) do
              @app.call(env)
            end
          end
          def logger
            Rails.logger
          end
        end
      end
      
  4. Time to wire it up. Add the two following lines to your application.rb

        config.autoload_paths += %W(#{config.root}/lib)
        config.middleware.insert_before "ActionDispatch::Static", "TenantScope::Rack"
    

Now you should be able to fire up localhost (depending on how you are finding your tenant - as this assumes it's a top level domain). If you're not using POW, then just find the first Tenant until you determine the true pass through algorithms of your tenants.

Now the models that you want to restrict to only a tenant, you will just add this to the file.

include TenantScope::ModelMixin

Now if you really want to console along, all you have to do is set your TenantScope in your console session.

TenantScope.current = Tenant.first

If you have your models all "tenantized" then you should be able to do cool stuff, very simply, the way you're used to.

Model.create(attr: "Whatever")

You will notice that the models properly update and assign things to the stuff that it should be assigning things to (namely the tenant).

Go create some amazing stuff!