Extending Engine Models in Rails 4

For a SaaS platform I’m working I decided to build it in Rails instead of Django. To get up up to speed with the latest developments in Rails 4 I bought Ryan Bigg’s book, Multitenancy with Rails.

In the book he takes you through how to set up a multi-tenant system with Rails and Postgres, with the multi-tenant part of the system encapsulated as a Rails engine.

Having the multi-tenant part split out into an engine is nice from a perspective of potential code sharing between applications, but it also adds complexity to the system–if you don’t understand how Rails engines work then you’re going to be in trouble. Given how much work building and running one SaaS platform is, I’m not sure that the additional work is worth it. After all, you are investing significant effort in making a part of your system reusable when you might not ever reuse the code. If you are building SaaS platforms for clients rather than running your own however, then you may find this extra work pays back great dividends.

In this particular implementation the user class is Subscriptions::User, provided by the Subscriptions engine. Today I want to extend it with a method to send a daily summary email to all users. I am making use of the Subscriptions::User model directly, i.e. I haven’t subclassed it in my application. Adding my custom method to the engine model class would break the encapsulation, so I need a way of specifying the method in my app.

Solution

The following approach works well enough and is relatively unobtrusive, though it doesn’t feel 100% right. Usually in Rails there’s a place for everything (convention over configuration) but in this case that doesn’t appear to be so.

In lieu of a specific place to extend our model, we create a new file, config/initializers/subscription_model_extenders.rb and define our method there:

Rails.application.config.to_prepare do
  Subscriptions::User.class_eval do
    def self.send_daily_summaries
      # Invoke custom ActionMailer
    end
  end
end

The first line will cause our code to be run at just the right time, once all files have been required. We then use class_eval to add a static method to the Subscriptions::User class, without touching the code of the engine itself.

Given that the initializers directory doesn’t have a lot of inherent structure, now might be a good time to add a note about what we have done to our internal documentation for future reference.

Discussion

On the topic of SaaS and Rails I found that Ryan’s book helped me get started quickly and I would recommend it. I encountered some bugs in the provided code (which I reported and have now been fixed); this wasn’t too bad but I did find that some of the assumptions Ryan makes don’t quite fit with what I wanted to build. I found (perhaps unsurprisingly) that I needed to do a lot of additional research and design to get the result I wanted, so much so that I feel it could even justify a whole new book on the topic.

One of the main assumptions in the book is that users should be able to be members of any account. In building a SaaS platform I would rather have each company account to be 100% separate to all other accounts, with nothing shared. Depending on your clients, they may have particularly stringent requirements here so before basing your system on the implementation in the book you may want to list out your assumptions and compare them to the system you get if you follow the instructions in the book. If they are significantly different then you will likely find that building the base of your SaaS system will take quite a bit longer than you might expect.