Setup 🛠 Rails 8 App – Part 16: Implementing Authentication, Users, Orders, and Order Items

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:

  1. Add Rails’ built‑in authentication via has_secure_password.
  2. Create a users table with roles for customers and admins.
  3. Build an orders table to capture overall transactions.
  4. Create order_items to 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:

  1. Uncomment the following line in your Gemfile.
    # gem "bcrypt", "~> 3.1.7"
  2. Run bundle install.
  3. In your migration, create a password_digest column:
create_table :users do |t|
  t.string :email,           null: false, index: { unique: true }
  t.string :password_digest, null: false
  # ... other fields ...
end

  1. 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:

  1. Defines predicate and bang methods
  2. Adds query scopes
  3. 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 separate addresses table 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 (no placed_at yet) 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 (no placed_at yet) and finalized purchases.
  • Timestamps and an optional placed_at datetime
✗ 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_id and product_variant_id as FKs
  • quantity, unit_price, and any discount_percent
  • Optional JSONB metadata for 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 nested OrderItemsController under 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
  1. Pros:
    • Fully automated and idempotent—you can run db:seed anytime 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).
  2. 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
  1. 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).
  2. 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 🚀!

Unknown's avatar

Author: Abhilash

Hi, I’m Abhilash! A seasoned web developer with 13+ years of experience specializing in Ruby and Ruby on Rails. Since 2010, I’ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Let’s explore the ever-evolving world of web development together!

One thought on “Setup 🛠 Rails 8 App – Part 16: Implementing Authentication, Users, Orders, and Order Items”

Leave a comment