Simple Preferences to Any Resource for Rails

Minimalistic, abstract 3D-like image of a dashboard in duotone sky blue and cyan, featuring stylized buttons, knobs, and sliders.

Having an option for your users to set their preferences is something that is likely required rather sooner than later. These might range from aesthetic preferences to usability settings.

Some examples for users: keyboard_shortcuts_enabled, language_preference, timezone, text_editor_theme, notification_sound_enabled.

Or for other domain models: contact_sort_order, default_campaign_filters, task_view_default, favorite_projects.

If you’ve ever built, or are part of a team that builds, SaaS products, you know these feature requests are not all known beforehand. Feature requests come in, requirements change and so on.

There are a few gems out there that provide a solution to this, but to me this is the kind of functionality I want to control and and not rely on a third-party (YMMV).

This article outlines a super simple, but extendable way to add one or many preferences to any domain model. There is some syntactic sugar added to make them quack more like Rails does too. 🦆

I want to preface that I use these preferences only for UI purposes. They are never needed to be queried/searched-for in some way.

Let’s dig right in. What is needed:

Just want to use it in your app right now? Run this template: rails app:template LOCATION="" in your Rails app’s root folder.

Let’s create the data model that holds it all:

rails g model Vault resource:belongs_to{polymorphic} scope:string payload:jsonb

I’ve chosen the general name “Vault” here as it fits quite well for the various purposes, but you can of course name it however you want.

Now let’s update the created migration file to add some sane validations, default values and some indexes.

class CreateVaults < ActiveRecord::Migration[7.1]
  def change
    create_table :vaults do |t|
      t.belongs_to :resource, polymorphic: true, null: false
      t.string :scope, null: false
      t.jsonb :payload, null: false, default: {}


    add_index :vaults, :scope
    add_index :vaults, :payload, using: :gin

Next add the store_attribute gem: bundle add store_attribute.

Now open the created Vault model and add these two class methods:

Get the latest Rails Designer updates

# app/models/vault.rb
class Vault < ApplicationRecord
  # …
  def self.vault_scope(scope_name)
    default_scope { where(scope: scope_name) }

  def self.vault_attribute(key, *attributes)
    options = attributes.extract_options!

    store_attribute :payload, key, *attributes, **options
  # …

Up next: a Vaults concern.

# app/models/concern/vaults.rb
module Vaults
  extend ActiveSupport::Concern

  class_methods do
    def vault(association_name, class_name: nil)
      has_one association_name, as: :resource, class_name: class_names.presence || "#{self}::#{association_name.to_s.camelize}", dependent: :destroy

Now the basics are done already! Let’s create the first Vault to store some User preferences. Create the following file:

# app/models/user/preferences.rb
class User::Preferences < Vault
  vault_scope :user_preferences

  vault_attribute :time_zone, :string, default: "UTC"
  vault_attribute :datetime_format, :string, default: "dd-mm-yyyy"
  vault_attribute :notification_sound_enabled, :boolean, default: true

Note: this folder structure is, by convention, through the set up of the class_name in the Vaults concern: "#{self}::#{association_name.to_s.camelize}". Set a custom one by setting the class_name below.

Then in your User model:

# app/models/user.rb
class User < ApplicationRecord
# …
  include Vaults

  vault :preferences
  # or when using a different folder structure:
  # `vault :preferences, class_name: "CustomPreferences"`
# …

Get User’s preferences like this:

user = User.find(1)
=> #<User:0x00000007146fb100>
=> "UTC"
=> true

Need to update a preference?

user.preferences.update notification_sound_enabled: false

And that’s all! 😎

Want to add this to your app without doing any manual work? 🧠 Run this command in your Rails app’s root folder: rails app:template LOCATION="".

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out. Interested in sharing Rails Designer with the Ruby on Rails community? Become an affiliate.

UI components for Ruby on Rails apps

$ 99 69 one-time

Get Access
  • One-time payment

  • Access to the entire library

  • Built for Ruby on Rails

  • Designed with Tailwind CSS and enhanced with Hotwire

  • Includes free updates (to any 1.x version)

  • Last update recently