20121119

subdomains in rails apps: a current brief

Subdomains (or second level domains) are still a nice way to present separate interfaces to a single web app — commonly to give a certain kind of customers the feel-n-touch of a dedicated app install.

Here are most of the related aspects based on a particular Rails3 project:

  • some model Group, representing a customer, or rather a group of users, has a string column domain (migration not shown, but don’t forget to index by that column)

    in app/models/group.rb:

    has_many :users
    before_validation :downcase_domain, :if => :domain_changed?
    validates :domain, presence: true, uniqueness: true, length: {maximum: 255}, format: /^[0-9a-z-]+$/
    
    
    def host
      "#{domain}.#{ROOT_DOMAIN}"
    end
    
    
    protected
    def downcase_domain
      self.domain = domain.to_s.downcase
    end
    
  • under assumption that you’re using devise and your (group’s) user identity model is User (updated for everchanging devise 2.0.4)

    in app/models/user.rb:

    belongs_to :group
    devise :database_authenticatable, ..., :request_keys => [:app_subdomain]
    
    
    def self.find_for_authentication(conditions={})
      group = Group.find_by_domain(conditions.delete(:app_subdomain))
      return nil unless group.present?
      conditions[:group_id] = group.id
      super
    end
    
  • you’ll have some admin interface where the groups are managed (e.g. active_admin, not recommended)

    in app/admin/groups.rb:

    link_to group.domain, root_url(host: group.host)
    
  • you will certainly want to do something specific in config/routes.rb

  • you’ll want some handy helper method, to know which guvnor you’re serving

    in app/controllers/application_controller.rb:

    helper_method :current_group
    def current_group
      return @current_group if defined? @current_group
      @current_group = request.app_subdomain && Group.find_by_domain(request.app_subdomain)
    end
    
  • you’ll need an initializer of some sort to set a constant and monkey-patch the request class, so…

    in config/initializers/subdomain.rb:

    ROOT_DOMAIN ||= ENV['ROOT_DOMAIN'] or raise "ROOT_DOMAIN must be set"
    
    
    # (): paranoid monkey patching :()
    class ActionDispatch::Request
      def app_subdomain
        return @app_subdomain if defined? @app_subdomain
        @@app_hostname_regex ||= /^(?:([0-9a-z-]+).)?#{Regexp.escape(ROOT_DOMAIN)}$/
        raise 'Wrong domain' unless host.downcase =~ @@app_hostname_regex
        @app_subdomain = $1
      end
    end
    
  • then, for your production (and staging) environment on, say, heroku, you’ll have to setup your lovely app domain name (with wildcard subdomains) and set the environment variable ROOT_DOMAIN to it

  • for test environment, which is also good for the handy circleci

    in config/environments/test.rb:

    ROOT_DOMAIN = 'lvh.me'  # yes, it's a magic domain for 127.0.0.1 //smackaho.st RIP
    
  • for other environment cases, be sure to set either ROOT_DOMAIN or ENV['ROOT_DOMAIN'] as appropriate

  • if you use factories (and girls, factory_girl)

    in spec/factories.rb:

    factory :group do
      sequence(:domain) {|n| "dom-#{n}"}  # or better still, use `forgery` with some smart randomness
      ...
    end
    
  • if you use capybara (2.0.0 at least, recommended) and rspec (rspec-rails 2.12.0 at least)

    in spec/spec_helper.rb: (in Spork.prefork block if you use spork, recommended)

    Capybara.always_include_port = true  # unless you `visit` external sites in your feature specs
    

    and then in some spec/features/..._spec.rb:

    visit("http://some_domain.#{ROOT_DOMAIN}/some_path")
    # or
    visit(some_url host: @group.host)  # if you're playing dirty, using pre-fabricated data and route helpers, recommended
    
  • in some spec/controllers/..._spec.rb you’ll have to include something like this:

    before(:each) do
      request.host = @group.host
    end
    
  • don’t forget the specs for domains in spec/models/group_spec.rb and other relevant places

May your sub-domains be obedient to their master.

20120608

app cache manifest should be public

As a rule of thumb, as of today, if you don't want trouble, make your HTML5 application cache manifest publicly accessible.

20120513

clothing labels hell

One of the things that have been annoying me for years — not Google, but rather vaguely related to the information technologies — are the clothing labels.
They are mostly made of highly skin-irritating fabric, and they tend to outlive any piece of clothing they are super-securely attached to.

Why???

20120510

Buggers Must Die

It is somewhat pathetic to criticise Google, a company which always strives to do what's best for us simple people — for free!
But.
I think they've just crossed the line with the “New Gmail Look”.
BTW, I hope you understand the irony of the first line: I really don't think I must feel obliged for their “free” email service — however good and accessible it is; quite the contrary, I feel I'm contributing to Google an irreplaceable and precious source of real-time information — the stuff the most world's (and certainly Google's) money comes from.
Personal opinions and tastes aside, the New Gmail Look is effectively incompatible with Mozilla Firefox — I know nothing of its compatibility with the new “good” Internet Explorer, unfortunately — and is clearly (cleverly) targeted for Google Chrome. To be clear, Gmail is functional in FF, but its CPU consumption there makes the combination unusable.
This is not something new or unexpected, both FF and Gmail have undergone development with this problem known — there are discussions, bug reports and blog posts like this one all over the internets — and I've been hoping a solution will be found before the New Look would become the only look, but alas, ah-ah, nope. At this moment, I have my peaceful internet existence violated and I feel forced to use the Chrome: Safari is also okay wrt Gmail, but it hasn't got nearly as much plugins as Chrome or especially Firefox have, so not much choice for a single-browser setup.

I don't think it's a fact to be taken lightly. I see something much worse than Microsoft coming, and I personally will now always try not to use Google products as the first option. Luckily, most alternatives to Google products actually outperform the latter.

Erm, yes, this blog will also be moved to a different provider in the nearest future.

p.s. Unrelated, kudos to Apple for making the choice of English flavour finally available system-wide. Alleluia!

20120509

google trance-laid he-brew

It's always fun to see automatic translation, but watching google trying to translate Hebrew is twice as fun: modern-day residents of the holy land don't use diacritics (= no vowel signs), the language does not have capital letters or much punctuation, and on top of that, Israeli names — both first and family ones — are very often just common words. Now you can imagine the level of ambiguity linguistic tools have to deal with here. And I have not even mentioned the abundance of foreign words which sometimes make writing hard to comprehend by even a native human reader.

20120403

pow, guard and rdebug - staying in the web app dev env heaven: for ruby 1.9 only

The new version (for ruby 2.1) is here.

If you, like me, use pow and guard (with spork of Rails 3 standard setup) for the perfect web app development environment, you might have stumbled upon a problem of debugging the server with rdebug -c which tends to connect to a wrong process even when working on just one project (and that's because spork itself starts the remote debug server by default).

So, firstly, you will probably want to limit your server instances run by pow to 1 by
echo export POW_WORKERS=1 >> ~/.powconfig

Then, to actually enable remote debugging you should place the following in your ./config/environments/development.rb:


And finally, to set the port of your choice for the project,
echo export RUBY_DEBUG_PORT=10007 >> ./.powenv

Now, you are welcome to
touch tmp/restart.txt
and (after a bunch of your CPU's cycles)
rdebug -c -p 10007

You're back in heaven, have a happy stay!