docs/developer/tutorial/model.mdx
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.
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
bin/rails g spree:model Brand name:string:index description:rich_text logo:attachment
This creates two files:
app/models/spree/brand.rb — the modeldb/migrate/XXXXXXXXXXXXXX_create_spree_brands.rb — the migrationNow apply the migration:
<CodeGroup>spree migrate
bin/rails db: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.
Open 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.spree console
bin/rails 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.
<Tip> 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. </Tip>The model is complete. Now let's give admins a UI to manage brands: Admin Dashboard.