Cell

Cell provides tenancy and customer isolation for Rails. It tries to be a simpler and more thorough implementation of things like Apartment.

The core of Cell was written years ago for Rails 1.2, before Apartment was available, and has been developed inside an application ever since. We've aggressively moved it along with the ecosystem, and it still feels more modern than other implementations we've used. We're in the process of making it a more generalized gem. Things are a bit messy now.

Cell tries to hook into Rails at the highest level available (e.g., ActiveJob instead of a particular job adapter), except for one place: PostgreSQL. I think PostgreSQL is the only widely deployed database with the features required to make a reasonable implementation (namely: schemas, roles, row and column level security.)

Cell aggressively uses Ruby 2.5, PostgreSQL 10.5 and Rails 5.2. This means it's probably more appropriate for greenfield projects than bolting on to an existing application on an older stack.

Cell moves forward quickly: Don't expect to be able to hang back on an old version of Rails and get new features in Cell. We version accordingly not to break things, but we don't back-port to old Rubies, old PostgreSQLs, or old Rails versions. We'll totally change how it works overnight.

The aforementioned Apartment gem looks like perfectly viable solution for projects with other constraints.

What it handles:

  • [x] ActiveRecord models
  • [x] Migrations: including shared and global tables
  • [ ] ActionController: An integration point for you
  • [x] ActiveJob: Jobs are executed in the proper context
  • [ ] ActionMailer: Your URIs will work
  • [ ] ActionView: Caching is automatically keyed on the Cell
  • [ ] Redis: optional, via redis-namespace
  • [x] Rake: tasks which run over each Cell

The rest of this README will be updated as the code is extracted.

Installation

Add this line to your application's Gemfile:

gem 'cell'

Tests

The application Cell was ripped out of had heavy test-coverage of the functionality Cell provides, but at the wrong level (e.g., our model, not the Cell behavior which this became). It's proven impossible to extract anything workable out of these into this gem, but we're working on starting from scratch.

How it Works

The most complicated interaction Cell has with Rails exist with ActiveRecord and its migrations. First, what happens in your PostgreSQL database:

Global tables are generated in the default schema, which we'll just call "public", and they stay there. This is what you're used to.

Per-tenant tables are redirected to a schema called "cell_prototype".

Both of these are versioned by 'public.schema_migrations'. e.g., the prototype and public schemas are always considered to be the same version.

When you create a new tenant, the cell_prototype schema is copied to a new schema for the tenant, and the tenant gets its own 'schema_migrations' table, copied from global.

When you 'db:migrate', un-ran migrations start populating public and cell_prototype, but not tenants.

To update existing tenants after a 'db:migrate', you need to run 'cell:db:migrate', which will run the migrations needed to bring each tenant up to date. There are an entire suite of cell: rake tasks that just run the normal task over each tenant. Check out rake -T | grep cell:

Cell activates a tenant by adjusting 'schema_search_path'. The way to use this is YourModel.use {|block }. Note that Cell::Model ends up aliased to YourModel, so Cell::Model.find(...).use works as well.

There are a lot of tricks Cell has to use re: thread safety and the query cache to make this act normally.

In the development mode console, Cell will activate the first tenant for you before you get a REPL.

Usage

First, read the How it Works section. You need to know what's going on in case something breaks.

Please don't use cell unless you're OK with the idea of digging into the code when things go wrong. It's not a drop-in thing yet.

Feel free to open PRs or tickets, though, but I'd rather see "We ended up having to use something else because Cell doesn't handle this" than "Cell must handle this".

I'm now committed to keeping the following section up-to-date, as the only source of piss-poor documentation.

First, use cell in your Gemfile as stated earlier.

First, you need a migration that does the following, and only the following:

class InitializeCell < ActiveRecord::Migration[5.0]
  def change
    initialize_cell!
  end
end

Then you need a migration that creates your tenanted model:

class CreateAccounts < ActiveRecord::Migration[5.0]
  def change
    global do
      create_table :accounts do |t|
        # ...
      end
    end
  end
end

We do the configuration in the model so it can be updated if you're running the (very useful) clusterfuck that is Spring: your settings will update in development.

class Account < ApplicationRecord
  extend Cell::Tenant
end

From there on out, most of the weird shit dealing with Cell will be in migrations. By default, it assumes everything you do in a migration is per-tenant. If you want to escape this, or create shared/global tables, you need to wrap it in a global block.

class TestMigration < ActiveRecord::Migration[5.0]
  def change
    # This will exist in each tenant
    create_table :users do |t|
      t.string :name, null: false
    end

    # This is global, and will be shared across all tenants
    global do
      create_table :capabilities do |t|
        t.string :name, null: false
      end
    end
  end
end

rake db:migrate updates the global and prototype schema, rake cell:db:migrate will run the non-global blocks across all existing tenants.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/metermd/cell.

License

The gem is available as open source under the terms of the MIT License.