Skip to main content
In this step we’ll create the Spree::Brand model with everything it needs: a name column, a rich text description, and an uploadable logo.

Step 1: Generate the Model

Spree ships a spree:model generator that produces a model and migration following all Spree conventions — Spree:: namespacing, prefixed IDs, presence validations, and API filtering allowlists. It understands Rails attribute types including the virtual ones, so rich text and file attachments are part of the same command:
spree generate model Brand name:string:index description:rich_text logo:attachment
Throughout this tutorial every command is shown in two forms. Spree CLI is for projects created with create-spree-app, where the app runs in Docker and spree <command> routes into the container. Without Spree CLI is for apps running Spree backend directly on your machine.
This creates two files:
  • app/models/spree/brand.rb — the model
  • db/migrate/XXXXXXXXXXXXXX_create_spree_brands.rb — the migration
Now apply the migration:
spree migrate
This creates the spree_brands table with an indexed name column. The description and logo attributes don’t add columns — rich text lives in Action Text’s action_text_rich_texts table, and uploads live in Active Storage’s tables.

Step 2: Understand the Generated Model

Open app/models/spree/brand.rb:
app/models/spree/brand.rb
module Spree
  class Brand < Spree.base_class
    has_prefix_id :brand

    has_rich_text :description
    has_one_attached :logo

    validates :name, presence: true

    self.whitelisted_ransackable_attributes = %w[name]
    self.whitelisted_ransackable_associations = %w[]
    self.whitelisted_ransackable_scopes = %w[]
  end
end
Line by line:
  • Spree.base_class — inherits all Spree model functionality (multi-store scoping helpers, preferences, and more). The class is namespaced under Spree::, so it’s available as Spree::Brand.
  • has_prefix_id :brand — records get Stripe-style public IDs like brand_k5nR8xLq. APIs never expose raw database IDs.
  • has_rich_text :description — formatted content via Action Text.
  • has_one_attached :logo — file uploads via Active Storage, with image processing and direct-to-storage uploads.
  • validates :name, presence: true — generated automatically for required columns.
  • whitelisted_ransackable_attributes — controls which attributes API clients may filter and sort by. Only allowlisted attributes are queryable, so adding name here is what later makes ?q[name_cont]=nike work.

Step 3: Try It in the Console

spree console
brand = Spree::Brand.create!(name: "Wilson")
brand.prefixed_id           # => "brand_k5nR8xLq"

brand.update!(description: "<h1>Hello</h1><p>World</p>")
brand.description.to_s             # => rendered rich text HTML
brand.description.to_plain_text    # => "Hello World"

Spree::Brand.find_by_prefix_id!("brand_k5nR8xLq")  # lookup by public ID
We’ll upload the logo through the admin UI in the next step — the model side is already done.
The generator accepts more attribute types and options — references with auto-resolved class names (user:belongs_to), unique indexes (slug:string:uniq), soft delete (--paranoid), and custom fields support (--metafields). Run it with --help to see everything.

Next Step

The model is complete. Now let’s give admins a UI to manage brands: Admin Dashboard.