Let’s now move onto create Authentication for our application.
Modern e‑commerce applications need robust user authentication, clear role‑based access, and an intuitive ordering system. In this post, we’ll walk through how to:
- Add Rails’ built‑in authentication via
has_secure_password. - Create a
userstable with roles for customers and admins. - Build an
orderstable to capture overall transactions. - Create
order_itemsto track each product variant in an order.
Throughout, we’ll leverage PostgreSQL’s JSONB for flexible metadata, and we’ll use Rails 8 conventions for migrations and models.
Automatic Authentication For Rails 8 Apps
bin/rails generate authentication
This creates all the necessary files for users and sessions.
Create Authentication Manually
1. Create users table and user model
✗ rails g migration create_users
# users migration
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
t.string :email, null: false, index: { unique: true }
t.string :password_digest, null: false
t.string :role, null: false, default: "customer"
t.string :first_name
t.string :last_name
t.jsonb :metadata, null: false, default: {}
t.timestamps
end
# You can later set up an enum in the User model:
# enum role: { customer: "customer", admin: "admin" }
end
end
✗ rails g model user
# User model
class User < ApplicationRecord
has_secure_password
enum :role, {
customer: "customer",
admin: "admin"
}
has_many :orders
end
2. Authenticating with has_secure_password
Rails ships with bcrypt support out of the box. To enable it:
- Uncomment the following line in your
Gemfile.# gem "bcrypt", "~> 3.1.7" - Run
bundle install. - In your migration, create a
password_digestcolumn:
create_table :users do |t|
t.string :email, null: false, index: { unique: true }
t.string :password_digest, null: false
# ... other fields ...
end
- In
app/models/user.rb, enable:
class User < ApplicationRecord
has_secure_password
# ...
end
This gives you user.authenticate(plain_text_password) and built‑in validation that a password is present on create.
3. Setting Up Users with Roles
We often need both customers and admins. Let’s create a role column with a default of "customer":
create_table :users do |t|
t.string :role, null: false, default: "customer"
# ...
end
In the User model you can then define an enum:
class User < ApplicationRecord
......
enum :role, {
customer: "customer",
admin: "admin"
}
end
This lets you call current_user.admin? or User.customers for scopes.
user.customer! # sets role to "customer"
user.admin? # => false
Rails built-in enum gives you a quick way to map a column to a fixed set of values, and it:
- Defines predicate and bang methods
- Adds query scopes
- Provides convenient helpers for serialization, validations, etc.
4. Building the Orders Table
Every purchase is represented by an Order. Key fields:
user_id(foreign key)total_price(decimal with scale 2)status(string; e.g.pending,paid,shipped)shipping_address(JSONB): allows storing a full address object with flexible fields (street, city, postcode, country, and even geolocation) without altering your schema. You can index JSONB columns (GIN) to efficiently query nested fields, and you avoid creating a separateaddressestable unless you need relationships or reuse.placed_at(datetime, optional): records the exact moment the order was completed, independent of when the record was created. Making this optional lets you distinguish between draft/in-progress orders (noplaced_atyet) and finalized purchases.- Timestamps
placed_at(datetime, optional): records the exact moment the order was completed, independent of when the record was created. Making this optional lets you distinguish between draft/in-progress orders (noplaced_atyet) and finalized purchases.- Timestamps and an optional
placed_atdatetime
✗ rails g migration create_orders
# orders migration
class CreateOrders < ActiveRecord::Migration[8.0]
def change
create_table :orders do |t|
t.references :user, null: false, foreign_key: true, index: true
t.decimal :total_price, precision: 12, scale: 2, null: false, default: 0.0
t.string :status, null: false, default: "pending", index: true
t.jsonb :shipping_address, null: false, default: {}
t.datetime :placed_at
t.timestamps
end
# Example statuses: pending, paid, shipped, cancelled
end
end
In app/models/order.rb:
✗ rails g model order
class Order < ApplicationRecord
belongs_to :user
has_many :order_items, dependent: :destroy
has_many :product_variants, through: :order_items
STATUSES = %w[pending paid shipped cancelled]
validates :status, inclusion: { in: STATUSES }
end
5. Capturing Each Item: order_items
To connect products to orders, we use an order_items join table. Each row stores:
order_idandproduct_variant_idas FKsquantity,unit_price, and anydiscount_percent- Optional JSONB
metadatafor special instructions
✗ rails g migration create_order_items
# order_items migration
class CreateOrderItems < ActiveRecord::Migration[8.0]
def change
create_table :order_items do |t|
t.references :order, null: false, foreign_key: true, index: true
t.references :product_variant, null: false, foreign_key: true, index: true
t.integer :quantity, null: false, default: 1
t.decimal :unit_price, precision: 10, scale: 2, null: false
t.decimal :discount_percent, precision: 5, scale: 2, default: 0.0
t.jsonb :metadata, null: false, default: {}
t.timestamps
end
# Composite unique index to prevent duplicate variant per order
add_index :order_items, [:order_id, :product_variant_id], unique: true, name: "idx_order_items_on_order_and_variant"
end
Model associations:
✗ rails g model order_item
class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product_variant
validates :quantity, numericality: { greater_than: 0 }
end
6. Next Steps: Controllers & Authorization
- Controllers: Scaffold
UsersController,SessionsController(login/logout),OrdersController, and nestedOrderItemsControllerunder orders or use a service object to build carts. - Authorization: Once role is set, integrate Pundit or CanCanCan to restrict admin actions (creating products, managing variants) and customer actions (viewing own orders).
- Views/Frontend: Tie it all together with forms for signup/login, a product catalog with “Add to Cart”, a checkout flow, and an admin dashboard for product management.
7. Scaffolding Controllers & Views (TailwindCSS Rails 4.2.3)
Generate Controllers & Routes
✗ rails generate controller Users new create index show edit update destroy --skip-routes
create app/controllers/users_controller.rb
invoke tailwindcss
create app/views/users
create app/views/users/new.html.erb
create app/views/users/create.html.erb
create app/views/users/index.html.erb
create app/views/users/show.html.erb
create app/views/users/edit.html.erb
create app/views/users/update.html.erb
create app/views/users/destroy.html.erb
invoke test_unit
create test/controllers/users_controller_test.rb
invoke helper
create app/helpers/users_helper.rb
invoke test_unit
✗ rails generate controller Sessions new create destroy --skip-routes
create app/controllers/sessions_controller.rb
invoke tailwindcss
create app/views/sessions
create app/views/sessions/new.html.erb
create app/views/sessions/create.html.erb
create app/views/sessions/destroy.html.erb
invoke test_unit
create test/controllers/sessions_controller_test.rb
invoke helper
create app/helpers/sessions_helper.rb
invoke test_unit
✗ rails generate controller Orders index show new create edit update destroy --skip-routes
create app/controllers/orders_controller.rb
invoke tailwindcss
create app/views/orders
create app/views/orders/index.html.erb
create app/views/orders/show.html.erb
create app/views/orders/new.html.erb
create app/views/orders/create.html.erb
create app/views/orders/edit.html.erb
create app/views/orders/update.html.erb
create app/views/orders/destroy.html.erb
invoke test_unit
create test/controllers/orders_controller_test.rb
invoke helper
create app/helpers/orders_helper.rb
invoke test_unit
✗ rails generate controller OrderItems create update destroy --skip-routes
create app/controllers/order_items_controller.rb
invoke tailwindcss
create app/views/order_items
create app/views/order_items/create.html.erb
create app/views/order_items/update.html.erb
create app/views/order_items/destroy.html.erb
invoke test_unit
create test/controllers/order_items_controller_test.rb
invoke helper
create app/helpers/order_items_helper.rb
invoke test_unit
In config/routes.rb, nest order_items under orders and add session routes:
Rails.application.routes.draw do
resources :users
n
resources :sessions, only: %i[new create destroy]
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :orders do
resources :order_items, only: %i[create update destroy]
end
root 'products#index'
end
By the end, you’ll have a fully functional e‑commerce back end: secure auth, order tracking, and clear user roles.
How to setup your First User🙍🏻♂️ in the system
The very first user you should set up is:
✅ An admin user — to create/manage products, variants, and handle backend tasks.
Here’s the best approach:
⭐ Best Practice: Seed an Admin User
Instead of manually creating it through the UI (when no one can log in yet), the best and safest approach is to use db/seeds.rb to create an initial admin user.
Why?
- You can reliably recreate it on any environment (local, staging, production).
- You can script strong defaults (like setting a secure admin email/password).
🔒 Tip: Use ENV Variables
For production, never hardcode admin passwords directly in seeds.rb. Instead, do:
admin_password = ENV.fetch("ADMIN_PASSWORD")
and pass it as:
ADMIN_PASSWORD=SomeStrongPassword rails db:seed
This keeps credentials out of your Git history.
🛠 Option 1: Add Seed Data db/seeds.rb
Add a block in db/seeds.rb that checks for (or creates) an admin user:
# db/seeds.rb
email = ENV.fetch("ADMIN_EMAIL") { abort "Set ADMIN_EMAIL" }
password = ENV.fetch("ADMIN_PASSWORD") { abort "Set ADMIN_PASSWORD" }
User.find_or_create_by!(email: admin_email) do |user|
user.password = admin_password
user.password_confirmation = admin_password
user.role = "admin"
user.first_name = "Site"
user.last_name = "Admin"
end
puts "→ Admin user: #{admin_email}"
Then run:
rails db:seed
- Pros:
- Fully automated and idempotent—you can run
db:seedanytime without creating duplicates. - Seed logic lives with your code, so onboarding new team members is smoother.
- You can wire up ENV vars for different credentials in each environment (dev/staging/prod).
- Fully automated and idempotent—you can run
- Cons:
- Seeds can get cluttered over time if you add lots of test data.
- Must remember to re-run seeds after resetting the database.
🛠 Option 2: Custom Rake task or Thor script
Create a dedicated task under lib/tasks/create_admin.rake:
namespace :admin do
desc "Create or update the first admin user"
task create: :environment do
email = ENV.fetch("ADMIN_EMAIL") { abort "Set ADMIN_EMAIL" }
password = ENV.fetch("ADMIN_PASSWORD") { abort "Set ADMIN_PASSWORD" }
user = User.find_or_initialize_by(email: email)
user.password = password
user.password_confirmation = password
user.role = "admin"
user.save!
puts "✅ Admin user #{email} created/updated"
end
end
Run it with:
ADMIN_EMAIL=foo@bar.com ADMIN_PASSWORD=topsecret rails admin:create
- Pros:
- Keeps seed file lean—admin-creation logic lives in a focused task.
- Enforces presence of ENV vars (you won’t accidentally use a default password in prod).
- Cons:
- Slightly more setup than plain seeds, though it’s still easy to run.
I choose for Option 2, because it is namespaced and clear what is the purpose. But in seed there will be lot of seed data together make it difficult to identify a particular task.
🛡 Why is This Better?
✅ No need to expose a sign-up page to create the very first admin.
✅ You avoid manual DB entry or Rails console commands.
✅ You can control/rotate the admin credentials easily.
✅ You can add additional seed users later if needed (for demo or testing).
📝 Summary
✅ Seed an initial admin user
✅ Add a role check (admin? method)
✅ Lock down sensitive parts of the app to admin
✅ Use ENV vars in production for passwords
Enjoy Rails 🚀!
One thought on “Setup 🛠 Rails 8 App – Part 16: Implementing Authentication, Users, Orders, and Order Items”