The Complete Guide to Cookie Storage in Rails 7: Security, Performance, and Best Practices

Cookies are fundamental to web applications, but choosing the right storage method can make or break your app’s security and performance. Rails 7 offers multiple cookie storage mechanisms, each with distinct security properties and use cases. Let’s explore when to use each approach and why it matters.

The Cookie Storage Spectrum

Rails provides four main cookie storage methods, each offering different levels of security:

# 1. Plain cookies - readable and modifiable by client
cookies[:theme] = 'dark'

# 2. Signed cookies - readable but tamper-proof
cookies.signed[:discount_code] = 'SAVE10'

# 3. Encrypted cookies - hidden and tamper-proof
cookies.encrypted[:user_preferences] = { notifications: true }

# 4. Session storage - server-side with encrypted session cookie
session[:current_user_id] = user.id

1. Plain Cookies: When Transparency is Acceptable

Use for: Non-sensitive data where client-side reading/modification is acceptable or even desired.

# Setting a plain cookie
cookies[:theme] = 'dark'
cookies[:language] = 'en'
cookies[:consent_given] = 'true'

# With expiration
cookies[:temporary_banner_dismissed] = {
  value: 'true',
  expires: 1.day.from_now
}

Security implications:

  • ✅ Fast and simple
  • ❌ Completely readable in browser dev tools
  • ❌ User can modify values freely
  • ❌ No protection against tampering

Best for:

  • UI preferences (theme, language)
  • Non-critical flags (banner dismissal)
  • Data you want JavaScript to access easily

2. Signed Cookies: Tamper-Proof but Visible

Signed cookies prevent modification while remaining readable. Rails uses HMAC-SHA1 with your secret_key_base to create a cryptographic signature.

# Setting signed cookies
cookies.signed[:discount_code] = 'SAVE10'
cookies.signed[:referral_source] = 'google_ads'

# Reading signed cookies
discount = cookies.signed[:discount_code]  # Returns 'SAVE10' or nil if tampered

How it works:

# Rails internally does:
# 1. Create signature: HMAC-SHA1(secret_key_base, 'SAVE10')
# 2. Store: Base64.encode64('SAVE10--signature')
# 3. On read: verify signature matches content

Security implications:

  • ✅ Tamper-proof – modification invalidates the cookie
  • ✅ Prevents privilege escalation attacks
  • ⚠️ Content still visible (Base64 encoded)
  • ❌ Not suitable for truly sensitive data

Real-world example from our codebase:

# lib/session/cookie_discount_accessor.rb
def discount_code
  # Prevents users from changing 'SAVE10' to 'SAVE50' in browser
  @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
end

def set_discount_code(code)
  @cookies.signed[:discount] = {
    value: code.name,
    expires: code.expiration || 30.days.from_now
  }
end

Best for:

  • Discount codes
  • Referral tracking
  • Non-sensitive IDs that shouldn’t be modified
  • Data integrity without confidentiality requirements

3. Encrypted Cookies: Maximum Security

Encrypted cookies are both signed and encrypted, making them unreadable and tamper-proof.

# Setting encrypted cookies
cookies.encrypted[:credit_card_last4] = '4242'
cookies.encrypted[:user_preferences] = {
  notifications: true,
  marketing_emails: false
}

# Reading encrypted cookies
preferences = cookies.encrypted[:user_preferences]

Security implications:

  • ✅ Content completely hidden from client
  • ✅ Tamper-proof
  • ✅ Suitable for sensitive data
  • ⚠️ Slightly higher CPU overhead
  • ⚠️ Size limitations (4KB total per domain)

Best for:

  • Personal information
  • Financial data
  • Complex user preferences
  • Any data you’d store in a database but need client-side

4. Session Storage: Server-Side Security

Rails sessions are encrypted cookies by default, but the data is conceptually server-side.

# Session storage
session[:current_user_id] = user.id
session[:shopping_cart] = cart.to_h
session[:two_factor_verified] = true

# Configuration in config/application.rb
config.session_store :cookie_store, key: '_myapp_session'

Security implications:

  • ✅ Encrypted by default
  • ✅ Automatic expiration handling
  • ✅ CSRF protection integration
  • ⚠️ 4KB size limit
  • ⚠️ Lost on cookie deletion

Best for:

  • User authentication state
  • Shopping carts
  • Multi-step form data
  • Security-sensitive flags

Security Best Practices

1. Choose the Right Storage Method

# ❌ Don't store sensitive data in plain cookies
cookies[:ssn] = '123-45-6789'  # Visible to everyone!

# ✅ Use appropriate security level
cookies.encrypted[:ssn] = '123-45-6789'  # Hidden and protected
session[:user_id] = user.id              # Server-side, encrypted

2. Set Proper Cookie Attributes

# Secure cookies for HTTPS
cookies[:theme] = {
  value: 'dark',
  secure: Rails.env.production?,  # HTTPS only
  httponly: true,                 # No JavaScript access
  samesite: :strict              # CSRF protection
}

3. Handle Cookie Tampering Gracefully

def current_discount_code
  code_name = cookies.signed[:discount]
  return nil unless code_name

  DiscountCode.find_by(name: code_name)&.tap do |code|
    # Remove if expired or invalid
    cookies.delete(:discount) unless code.usable?
  end
end

4. Use Expiration Strategically

# Short-lived sensitive data
cookies.signed[:password_reset_token] = {
  value: token,
  expires: 15.minutes.from_now,
  secure: true,
  httponly: true
}

# Long-lived preferences
cookies.encrypted[:user_preferences] = {
  value: preferences.to_json,
  expires: 1.year.from_now
}

Advanced Patterns

1. Cookie Accessor Classes

Create dedicated classes for complex cookie management:

class Session::CookieDiscountAccessor
  def initialize(cookies)
    @cookies = cookies
  end

  def discount_code
    @cookies.signed[:discount] && DiscountCode.find_by(name: @cookies.signed[:discount])
  end

  def set_discount_code(code)
    @cookies.signed[:discount] = {
      value: code.name,
      expires: code.expiration || 30.days.from_now
    }
  end

  def remove_discount_code
    @cookies.delete(:discount)
  end
end

2. Validation and Cleanup

class Session::CheckAndRemoveDiscountCode
  def initialize(cookies:)
    @accessor = Session::CookieDiscountAccessor.new(cookies)
  end

  def run
    # Remove referral conflicts
    @accessor.referral_code && @accessor.remove_discount_code && return
      
    # Remove expired codes
    discount_code = @accessor.discount_code
    @accessor.remove_discount_code if discount_code && !discount_code.usable?
  end
end

3. Error Handling for Corrupted Cookies

def safe_read_encrypted_cookie(key)
  cookies.encrypted[key]
rescue ActiveSupport::MessageVerifier::InvalidSignature,
       ActiveSupport::MessageEncryptor::InvalidMessage
  # Cookie was corrupted or created with different secret
  cookies.delete(key)
  nil
end

Performance Considerations

Cookie Size Limits

  • Total limit: 4KB per domain
  • Individual limit: ~4KB per cookie
  • Count limit: ~50 cookies per domain

CPU Overhead

# Benchmark different storage methods
require 'benchmark'

Benchmark.bm do |x|
  x.report("plain")     { 1000.times { cookies[:test] = 'value' } }
  x.report("signed")    { 1000.times { cookies.signed[:test] = 'value' } }
  x.report("encrypted") { 1000.times { cookies.encrypted[:test] = 'value' } }
end

# Results (approximate):
#                user     system      total        real
# plain      0.001000   0.000000   0.001000 (  0.001000)
# signed     0.010000   0.000000   0.010000 (  0.009000)
# encrypted  0.050000   0.000000   0.050000 (  0.048000)

Configuration and Security Headers

Session Configuration

# config/application.rb
config.session_store :cookie_store,
  key: '_myapp_session',
  secure: Rails.env.production?,
  httponly: true,
  expire_after: 14.days,
  same_site: :lax

Security Headers

# config/application.rb
config.force_ssl = true  # HTTPS in production

# Use Secure Headers gem
SecureHeaders::Configuration.default do |config|
  config.cookies = {
    secure: true,
    httponly: true,
    samesite: {
      lax: true
    }
  }
end

Testing Cookie Security

# spec/lib/session/coupon_code_spec.rb
RSpec.describe Session::CouponCode do
  describe 'cookie tampering protection' do
    it 'handles corrupted signed cookies gracefully' do
      # Simulate tampered cookie
      cookies.signed[:discount] = 'SAVE10'
      cookies[:discount] = 'tampered_value'  # Direct manipulation

      accessor = Session::CookieDiscountAccessor.new(cookies)
      expect(accessor.discount_code).to be_nil
    end
  end
end

Migration Strategies

Upgrading Cookie Security

def upgrade_cookie_security
  # Read from old plain cookie
  if (old_value = cookies[:legacy_data])
    # Migrate to encrypted
    cookies.encrypted[:legacy_data] = old_value
    cookies.delete(:legacy_data)
  end
end

Handling Secret Key Rotation

# config/credentials.yml.enc
secret_key_base: new_secret
legacy_secret_key_base: old_secret

# In application
def read_with_fallback(key)
  cookies.encrypted[key] || begin
    # Try with old secret
    old_verifier = ActiveSupport::MessageEncryptor.new(
      Rails.application.credentials.legacy_secret_key_base
    )
    old_verifier.decrypt_and_verify(cookies[key])
  rescue
    nil
  end
end

Quick Decision Matrix

Data TypeSensitivityClient Access NeededRecommended Storage
Theme preferencesLowYesPlain cookies
Discount codesMediumNoSigned cookies
User settingsMediumNoEncrypted cookies
AuthenticationHighNoSession
Credit card dataHighNoDatabase + session ID
Shopping cartMediumNoSession or encrypted
CSRF tokensHighLimitedSession (built-in)

Common Pitfalls to Avoid

  1. Don’t mix storage types for the same data
   # ❌ Inconsistent
   cookies[:user_id] = user.id        # Sometimes
   cookies.signed[:user_id] = user.id # Other times

   # ✅ Consistent
   session[:user_id] = user.id        # Always
  1. Don’t store large objects in cookies
   # ❌ Will hit 4KB limit
   cookies.encrypted[:full_user] = user.to_json

   # ✅ Store reference
   session[:user_id] = user.id
  1. Don’t forget expiration
   # ❌ Never expires
   cookies.signed[:temp_token] = token

   # ✅ Proper expiration
   cookies.signed[:temp_token] = {
     value: token,
     expires: 1.hour.from_now
   }

Conclusion

Cookie storage in Rails 7 offers a rich toolkit for different security and performance needs. The key is matching the storage method to your data’s sensitivity and access patterns:

  • Plain cookies for non-sensitive, client-accessible data
  • Signed cookies when you need tamper protection but not confidentiality
  • Encrypted cookies for sensitive data that must remain client-side
  • Session storage for server-side state with automatic encryption

Remember: the best cookie strategy combines appropriate storage methods with proper security headers, validation, and graceful error handling. When in doubt, err on the side of more security rather than less.

The Rails cookie system is designed to make secure defaults easy—take advantage of it to build applications that are both performant and secure.


Rails 7+ API error handling that scales ⚖️

A solid API error strategy gives you:

  • Consistent JSON error shapes
  • Correct HTTP status codes
  • Separation of concerns (domain vs transport)
  • Observability without leaking internals

Below is a practical, production-ready approach that covers controller hooks, controllers, models/libs, background jobs, and more—illustrated with a real scenario from Session::CouponCode.

Core principles

  • Keep transport (HTTP, JSON) in controllers; keep domain logic in models/libs.
  • Map known, expected failures to specific HTTP statuses.
  • Log unexpected failures; return a generic message to clients.
  • Centralize API error rendering in a base controller.

1) A single error boundary for all API controllers

Create a base Error::ApiError and rescue it (plus a safe catch‑all) in your ApiController.

# lib/error/api_error.rb
module Error
  class ApiError < StandardError
    attr_reader :status, :details
    def initialize(message, status = :unprocessable_entity, details: nil)
      super(message)
      @status  = status
      @details = details
    end
  end
end
# app/controllers/api_controller.rb
class ApiController < ActionController::Base
  include LocaleConcern
  skip_forgery_protection

  impersonates :user,
               ......

  # Specific handlers first
  rescue_from Error::ApiError,                          with: :handle_api_error
  rescue_from ActionController::ParameterMissing,       with: :handle_bad_request
  rescue_from ActiveRecord::RecordNotFound,             with: :handle_not_found
  rescue_from ActiveRecord::RecordInvalid,              with: :handle_unprocessable
  rescue_from ActiveRecord::RecordNotUnique,            with: :handle_conflict

  # Catch‑all last
  rescue_from StandardError,                            with: :handle_standard_error

  private

  def handle_api_error(e)
    render json: { success: false, error: e.message, details: e.details }, status: e.status
  end

  def handle_bad_request(e)
    render json: { success: false, error: e.message }, status: :bad_request
  end

  def handle_not_found(_e)
    render json: { success: false, error: 'Not found' }, status: :not_found
  end

  def handle_unprocessable(e)
    render json: { success: false, error: e.record.errors.full_messages }, status: :unprocessable_entity
  end

  def handle_conflict(_e)
    render json: { success: false, error: 'Conflict' }, status: :conflict
  end

  def handle_standard_error(e)
    Rollbar.error(e, path: request.fullpath, client_id: try(:current_client)&.id)
    render json: { success: false, error: 'Something went wrong' }, status: :internal_server_error
  end
end
  • Order matters. Specific rescue_from before StandardError.
  • This pattern avoids duplicating rescue_from across controllers and keeps HTML controllers unaffected.

2) Errors in before actions

Because before_action runs inside controllers, the same rescue_from handlers apply.

Two patterns:

  • Render in the hook for simple guard clauses:
before_action :require_current_client

def require_current_client
  return if current_client
  render json: { success: false, error: 'require_login' }, status: :unauthorized
end
  • Raise a domain/auth error and let rescue_from handle JSON:
# lib/error/unauthorized_error.rb
module Error
  class UnauthorizedError < Error::ApiError
    def initialize(message = 'require_login') = super(message, :unauthorized)
  end
end

before_action :require_current_client

def require_current_client
  raise Error::UnauthorizedError unless current_client
end

Prefer raising if you want consistent global handling and logging.

3) Errors inside controllers

Use explicit renders for happy-path control flow; raise for domain failures:

def create
  form = CreateThingForm.new(params.require(:thing).permit(:name))
  result = CreateThing.new(form: form).call

  if result.success?
    render json: { success: true, thing: result.thing }, status: :created
  else
    # Known domain failure → raise an ApiError to map to 422
    raise Error::ApiError.new(result.message, :unprocessable_entity, details: result.details)
  end
end

Common controller exceptions (auto-mapped above):

  • ActionController::ParameterMissing → 400
  • ActiveRecord::RecordNotFound → 404
  • ActiveRecord::RecordInvalid → 422
  • ActiveRecord::RecordNotUnique → 409

4) Errors in models, services, and libs

Do not call render here. Either:

  • Return a result object (Success/Failure), or
  • Raise a domain‑specific exception that the controller maps to an HTTP response.

Example from our scenario, Session::CouponCode:

# lib/error/session/coupon_code_error.rb
module Error
  module Session
    class CouponCodeError < Error::ApiError; end
  end
end
# lib/session/coupon_code.rb
class Session::CouponCode
  def discount_dollars
    # ...
    case
    when coupon_code.gift_card?
      # ...
    when coupon_code.discount_code?
      # ...
    when coupon_code.multiorder_discount_code?
      # ...
    else
      raise Error::Session::CouponCodeError, 'Unrecognized discount code'
    end
  end
end

Then, in ApiController, the specific handler (or the Error::ApiError handler) renders JSON with a 422.

This preserves separation: models/libs raise; controllers decide HTTP.

5) Other important surfaces

  • ActiveJob / Sidekiq
  • Prefer retry_on, discard_on, and job‑level rescue with logging.
  • Return no HTTP here; jobs are async.
class MyJob < ApplicationJob
  retry_on Net::OpenTimeout, wait: 10.seconds, attempts: 3
  discard_on Error::ApiError
  rescue_from(StandardError) { |e| Rollbar.error(e) }
end
  • Mailers
  • Use rescue_from to avoid bubble‑ups crashing deliveries:
class ApplicationMailer < ActionMailer::Base
  rescue_from Postmark::InactiveRecipientError, Postmark::InvalidEmailRequestError do
    # no-op / log
  end
end
  • Routing / 404
  • For APIs, keep 404 mapping at the controller boundary with rescue_from ActiveRecord::RecordNotFound.
  • For HTML, config.exceptions_app = routes + ErrorsController.
  • Middleware / Rack
  • For truly global concerns, use middleware. This is rarely necessary for controller-scoped API errors in Rails.
  • Validation vs. Exceptions
  • Use validations (ActiveModel/ActiveRecord) for expected user errors.
  • Raise exceptions for exceptional conditions (invariants violated, external systems fail unexpectedly).

6) Observability

  • Always log unexpected errors in the catch‑all (StandardError).
  • Add minimal context: client_id, request.fullpath, feature flags.
  • Avoid leaking stack traces or internal messages to clients. Send generic messages on 500s.

7) Testing

  • Unit test domain services to ensure they raise Error::ApiError (or return Failure).
  • Controller/request specs: assert status codes and JSON shapes for both happy path and error path.
  • Ensure before_action guards either render or raise as intended.

Applying this to our scenario

  • /lib/session/coupon_code.rb raises Error::Session::CouponCodeError on unknown/invalid discount values.
  • /app/controllers/api_controller.rb rescues that error and returns JSON:
  • { success: false, error: e.message } with a 422 (or via Error::ApiError base).

This converts prior 500s into clean API responses and keeps error handling centralized.

When to generalize vs. specialize

  • Keep a catch‑all rescue_from StandardError in ApiController to prevent 500s from leaking internals.
  • Still add specific handlers (or subclass Error::ApiError) for known cases to control the correct status code and message.
  • Do not replace everything with only StandardError—you’ll lose semantics and proper HTTP codes.

  • Key takeaways
  • Centralize API‐wide error handling in ApiController using specific handlers + a safe catch‑all.
  • Raise domain errors in models/libs; render JSON only in controllers.
  • Map common Rails exceptions to correct HTTP statuses; log unexpected errors.
  • Prefer Error::ApiError as a base for consistent message/status handling across the API.

🔮 The Future of Ruby: Is It Still Relevant in 2025 and Beyond?

Ruby, the language that brought joy back into programming, is now over two decades old. It revolutionized web development through Rails and championed a developer-first philosophy. But in the era of AI, server-less, and systems programming, is Ruby still relevant? With Python dominating AI, Go owning the backend space, and Elixir praised for concurrency — where does Ruby stand?

Let’s explore Ruby’s current state, the challenges it faces, and what the future might hold.


🧱 What Ruby Still Does Exceptionally Well

1. Web Development with Rails

Ruby on Rails remains one of the fastest and most pleasant ways to build web applications. It’s productive, expressive, and mature.

  • Companies like GitHub, Shopify, Basecamp, and Hey.com still use Rails at scale.
  • Rails 8 introduced modern features like Turbo, Hotwire, and Kamal (for zero-downtime deploys).
  • It’s still a top pick for startups wanting to build MVPs quickly.

2. Developer Happiness

The principle of “developer happiness” is deeply embedded in Ruby’s philosophy:

  • Intuitive syntax
  • Expressive and readable code
  • A community that values elegance over boilerplate

Ruby continues to be one of the best languages for teaching programming, prototyping ideas, or building software that feels joyful to write.


⚠️ Challenges Facing Ruby Today

1. Performance Limitations

Ruby’s performance has improved dramatically with YJIT, MJIT, and better memory handling. But it still lags behind languages like Go or Rust in raw speed, especially in CPU-bound or concurrent environments.

2. Concurrency and Parallelism

  • Ruby has a Global Interpreter Lock (GIL) in MRI, which limits real parallelism.
  • While Fibers and async gems (async, polyphony, concurrent-ruby) help, it’s not as seamless as Go’s goroutines or Elixir’s lightweight processes.

3. Ecosystem Narrowness

Ruby’s ecosystem is tightly tied to Rails.

  • Unlike Python, which powers AI, data science, and automation…
  • Or JavaScript, which rules the browser and serverless space…

Ruby hasn’t made significant inroads outside web development.

4. Enterprise Perception

Many large enterprises shy away from Ruby, viewing it as either:

  • A “legacy startup language“, or
  • Too dynamic and flexible for highly-regulated or enterprise-scale environments.

🛠️ How Can Ruby Improve?

💡 1. Concurrency and Async Programming

  • Embrace the shift toward non-blocking IO, async/await patterns.
  • Invest in the ecosystem around async, falcon, and evented web servers.

💡 2. AI/ML Integration

  • Ruby doesn’t need to compete with Python in AI, but it can bridge to Python using gems like pycall, pybind11, or ruby-dlib.
  • Better interop with other platforms like JRuby, TruffleRuby, or even WebAssembly can unlock new domains.

💡 3. Broaden Ecosystem Use

  • Encourage usage outside web: CLI tools, static site generation, scripting, DevOps, etc.
  • Frameworks like Hanami, Roda, Dry-rb, and Trailblazer are promising.

💡 4. Stronger Developer Outreach

  • More documentation, YouTube tutorials, free courses, and evangelism.
  • Encourage open source contribution in tools beyond Rails.

📉 Will Rails Usage Decline?

Not disappear, but become more specialized.

Rails is no longer the hottest framework — but it’s still one of the most productive and complete options for web development.

  • Startups love it for speed of development.
  • Mid-sized businesses rely on it for stability and maintainability.
  • But serverless-first, JavaScript-heavy, or cloud-native stacks may bypass it in favor of Next.js, Go, or Elixir/Phoenix.

The challenge is staying competitive in the face of frameworks that promise better real-time capabilities and lightweight microservices.

🌟 Why Ruby Still Matters

Despite all that, Ruby still offers:

  • 🧘‍♂️ Developer productivity
  • 🧩 Readable, expressive syntax
  • 🚀 Fast prototyping
  • ❤️ A helpful, mature community
  • 🧪 First-class TDD culture

It’s a joy to write in Ruby. For many developers, that alone is enough.


🔚 Final Thoughts: The Joyful Underdog

Ruby is no longer the main character in the programming language race. But that’s okay.

In a world chasing performance benchmarks, Ruby quietly reminds us: “Programming can still be beautiful.

The future of Ruby lies in:

  • Focusing on what it does best (developer experience, productivity)
  • Expanding into new areas (concurrency, scripting, interop)
  • And adapting — not by competing with Go or Python, but by embracing its unique strengths.

Go with Ruby! 🚀

🏃‍♂️ Solving LeetCode Problems the TDD Way (Test-First Ruby): Minimum Size Subarray Sum

Welcome to my new series where I combine the power of Ruby with the discipline of Test-Driven Development (TDD) to tackle popular algorithm problems from LeetCode! 🧑‍💻💎 Whether you’re a Ruby enthusiast looking to sharpen your problem-solving skills, or a developer curious about how TDD can transform the way you approach coding challenges, you’re in the right place.

🎲 Episode 7: Minimum Size Subarray Sum

###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#
# Input: target = 7, nums = [2,3,1,2,4,3]
# Output: 2
# Explanation: The subarray [4,3] has the minimal length under the problem constraint.
# Example 2:
#
# Input: target = 4, nums = [1,4,4]
# Output: 1
# Example 3:
#
# Input: target = 11, nums = [1,1,1,1,1,1,1,1]
# Output: 0
#
#
# Constraints:
#
# 1 <= target <= 109
# 1 <= nums.length <= 105
# 1 <= nums[i] <= 104
#
###########################################################

🔧 Setting up the TDD environment

mkdir minimum-size-subarray-sum
touch minimum-size-subarray-sum/subarray_sum_min_size.rb
touch minimum-size-subarray-sum/test_subarray_sum_min_size.rb

Github Repo: https://github.com/abhilashak/leetcode/tree/main/minimum_size_subarray_sum

❌ Red: Writing the failing test

Test File:

# ❌ Fail
# frozen_string_literal: true

#######################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
#######################################################
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'

class TestSubArraySumMinSize < Minitest::Test
  def set_up; end

  def test_array_of_length_one
    assert_equal 0, SubArray.new([2], 3).min_size
    assert_equal 1, SubArray.new([2], 2).min_size
    assert_equal 0, SubArray.new([3], 4).min_size
  end
end

Source Code:

# frozen_string_literal: true

# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose @sum is greater than or equal to target. If there is no such subarray, return 0 instead.
# ............
#
###########################################################
class SubArray
   def min_size
   end
end
✗  ruby test_subarray_sum_min_size.rb
Run options: --seed 5914

# Running:

E

Finished in 0.000386s, 2590.6736 runs/s, 0.0000 assertions/s.

  1) Error:
TestSubArraySumMinSize#test_array_of_length_one:
ArgumentError: wrong number of arguments (given 2, expected 0)
    test_subarray_sum_min_size.rb:16:in 'BasicObject#initialize'
    test_subarray_sum_min_size.rb:16:in 'Class#new'
    test_subarray_sum_min_size.rb:16:in 'TestSubArraySumMinSize#test_array_of_length_one'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
➜  minimum-size-subarray-sum git:(main) ✗

✅ Green: Making it pass

# Pass ✅ 
# frozen_string_literal: true

###########################################################
# #209
# Given an array of positive integers nums and a positive integer target, return the minimal length of a subarray
# whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.
#
# Example 1:
#........
#
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
  end

  def min_size
    0 if @nums.length == 1 && @nums.first < @target
  end
end
✗ ruby minimum-size-subarray-sum/test_subarray_sum_min_size.rb
Run options: --seed 52896

# Running:
.

Finished in 0.000354s, 2824.8588 runs/s, 2824.8588 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

…………………………………………………. …………………………………………………………..

# frozen_string_literal: true
# .........
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'

class TestSubArraySumMinSize < Minitest::Test
  def set_up; end

  def test_array_of_length_one
    assert_equal 0, SubArray.new([2], 3).min_size
    assert_equal 1, SubArray.new([2], 2).min_size
    assert_equal 0, SubArray.new([3], 2).min_size
  end

  def test_array_of_length_two
    assert_equal 0, SubArray.new([2, 2], 5).min_size
    assert_equal 0, SubArray.new([1, 2], 10).min_size
    assert_equal 2, SubArray.new([2, 2], 4).min_size
    assert_equal 2, SubArray.new([3, 5], 8).min_size
  end
end
# Solution for upto 2 Array Input Length ✅ 

# frozen_string_literal: true
###########################################################
# .............
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
  end

  def min_size
    if @nums.length == 1
      return (@nums.first == @target ? 1 : 0)
    end

    @nums.sum == @target ? 2 : 0
  end
end

…………………………………………………. …………………………………………………………..

# frozen_string_literal: true

#######################################################
# ..........
#######################################################
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'

class TestSubArraySumMinSize < Minitest::Test
  def set_up; end

  def test_array_of_length_one
    assert_equal 0, SubArray.new([2], 3).min_size
    assert_equal 1, SubArray.new([2], 2).min_size
    assert_equal 0, SubArray.new([3], 4).min_size
  end

  def test_array_of_length_two
    assert_equal 0, SubArray.new([2, 2], 5).min_size
    assert_equal 0, SubArray.new([1, 2], 10).min_size
    assert_equal 2, SubArray.new([2, 2], 4).min_size
    assert_equal 2, SubArray.new([3, 5], 8).min_size
  end

  def test_array_of_length_three
    assert_equal 0, SubArray.new([2, 3, 4], 10).min_size
    assert_equal 1, SubArray.new([12, 3, 9], 10).min_size
    assert_equal 2, SubArray.new([2, 3, 4], 7).min_size
    assert_equal 1, SubArray.new([2, 3, 4], 4).min_size
  end

  def test_array_of_length_five
    assert_equal 0, SubArray.new([2, 3, 4, 1, 9], 20).min_size
    assert_equal 2, SubArray.new([2, 3, 9, 1, 0], 10).min_size
    assert_equal 4, SubArray.new([2, 3, 4, 6, 4], 17).min_size
    assert_equal 5, SubArray.new([2, 3, 4, 12, 10], 31).min_size
  end
end
# Solution for upto 5 Array Input Length ✅ 
# frozen_string_literal: true

# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# ...............
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
    @min_length = 0 # default 0 -> solution not found
    @left_pos = 0
    @right_pos = 0
    @sum = nil
  end

  def min_size
    while @right_pos < @nums.length
      # first position where left and right positions are at starting point
      @sum = if @left_pos.zero? && @right_pos.zero?
               @nums[@right_pos]
             else
               # add elements inside the window
               @nums[@left_pos..@right_pos].sum
             end

      if solution_found?
        update_min_length

        return 1 if @min_length == 1 # best scenario found, stop here
      else
        @right_pos += 1 # increase window size by 1
      end
    end

    @min_length
  end

  private

  def update_min_length
    new_length = @right_pos - @left_pos + 1

    if min_length_empty? || min_or_equal_length?(new_length)
      @min_length = new_length
      @left_pos += 1
    end
  end

  def solution_found?
    @sum >= @target
  end

  def min_length_empty?
    @min_length.zero?
  end

  # if new length of subarray found is less than already found min length
  # or new length found is equal to previous min length (should decrease window size
  # by increasing left pos to find the less length subarray)
  def min_or_equal_length?(new_length)
    new_length <= @min_length
  end
end

…………………………………………………. …………………………………………………………..

# Test Cases with Original LeetCode examples, Edge Cases, Additional test cases
# frozen_string_literal: true

#######################################################
# ..........
#######################################################
require 'minitest/autorun'
require_relative 'subarray_sum_min_size'

class TestSubArraySumMinSize < Minitest::Test
  def set_up; end

  def test_array_of_length_one
    assert_equal 0, SubArray.new([2], 3).min_size
    assert_equal 1, SubArray.new([2], 2).min_size
    assert_equal 0, SubArray.new([3], 4).min_size
  end

  def test_array_of_length_two
    assert_equal 0, SubArray.new([2, 2], 5).min_size
    assert_equal 0, SubArray.new([1, 2], 10).min_size
    assert_equal 2, SubArray.new([2, 2], 4).min_size
    assert_equal 2, SubArray.new([3, 5], 8).min_size
  end

  def test_array_of_length_three
    assert_equal 0, SubArray.new([2, 3, 4], 10).min_size
    assert_equal 1, SubArray.new([12, 3, 9], 10).min_size
    assert_equal 2, SubArray.new([2, 3, 4], 7).min_size
    assert_equal 1, SubArray.new([2, 3, 4], 4).min_size
  end

  def test_array_of_length_five
    assert_equal 0, SubArray.new([2, 3, 4, 1, 9], 20).min_size
    assert_equal 2, SubArray.new([2, 3, 9, 1, 0], 10).min_size
    assert_equal 4, SubArray.new([2, 3, 4, 6, 4], 17).min_size
    assert_equal 5, SubArray.new([2, 3, 4, 12, 10], 31).min_size
  end

  # Original LeetCode examples
  def test_leetcode_example1
    # Input: target = 7, nums = [2,3,1,2,4,3]
    # Output: 2 (subarray [4,3])
    assert_equal 2, SubArray.new([2, 3, 1, 2, 4, 3], 7).min_size
  end

  def test_leetcode_example2
    # Input: target = 4, nums = [1,4,4]
    # Output: 1 (subarray [4])
    assert_equal 1, SubArray.new([1, 4, 4], 4).min_size
  end

  def test_leetcode_example3
    # Input: target = 11, nums = [1,1,1,1,1,1,1,1]
    # Output: 0 (no subarray sums to >= 11)
    assert_equal 0, SubArray.new([1, 1, 1, 1, 1, 1, 1, 1], 11).min_size
  end

  # Edge cases
  def test_empty_array
    assert_equal 0, SubArray.new([], 5).min_size
  end

  def test_target_zero
    assert_equal 1, SubArray.new([1, 2, 3], 0).min_size
  end

  def test_target_equals_single_element
    assert_equal 1, SubArray.new([5, 2, 3], 5).min_size
  end

  def test_target_equals_array_sum
    assert_equal 3, SubArray.new([1, 2, 3], 6).min_size
  end

  def test_target_greater_than_array_sum
    assert_equal 0, SubArray.new([1, 2, 3], 10).min_size
  end

  # Additional test cases
  def test_consecutive_ones
    assert_equal 3, SubArray.new([1, 1, 1, 1, 1], 3).min_size
    assert_equal 5, SubArray.new([1, 1, 1, 1, 1], 5).min_size
    assert_equal 0, SubArray.new([1, 1, 1, 1, 1], 6).min_size
  end

  def test_large_numbers
    assert_equal 1, SubArray.new([100, 1, 1, 1], 50).min_size
    assert_equal 2, SubArray.new([50, 50, 1, 1], 100).min_size
  end

  def test_window_shrinking
    # Test that the algorithm properly shrinks the window
    # [1, 4, 4] with target 4 should return 1, not 2
    assert_equal 1, SubArray.new([1, 4, 4], 4).min_size
  end

  def test_multiple_valid_subarrays
    # [2, 3, 1, 2, 4, 3] with target 7
    # Valid subarrays: [2,3,1,2] (sum=8), [4,3] (sum=7), [3,1,2,4] (sum=10)
    # Should return 2 (shortest: [4,3])
    assert_equal 2, SubArray.new([2, 3, 1, 2, 4, 3], 7).min_size
  end

  def test_all_elements_equal
    assert_equal 2, SubArray.new([3, 3, 3, 3], 6).min_size
    assert_equal 3, SubArray.new([3, 3, 3, 3], 9).min_size
    assert_equal 0, SubArray.new([3, 3, 3, 3], 13).min_size
  end
end
# Solution 1 ✅ 
# frozen_string_literal: true

# disable rubocop GuardClause for better readability in the code
# rubocop:disable Style/GuardClause
###########################################################
# #209
#  .............
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
    @min_length = 0 # default 0 -> solution not found
    @left_pos = 0
    @right_pos = 0
    @sum = nil
  end

  def min_size
    while @right_pos < @nums.length
      @sum = calculate_sum

      if solution_found?
        update_min_length

        return 1 if @min_length == 1 # best scenario found, stop here
      else
        @right_pos += 1 # increase window size by 1
      end
    end

    @min_length
  end

  private

  def calculate_sum
    # first position where left and right positions are at starting point
    return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?

    # add elements inside the window
    @nums[@left_pos..@right_pos].sum
  end

  def update_min_length
    new_length = @right_pos - @left_pos + 1

    if min_length_empty? || min_or_equal_length?(new_length)
      @min_length = new_length
      @left_pos += 1
    end
  end

  def solution_found?
    @sum >= @target
  end

  def min_length_empty?
    @min_length.zero?
  end

  # if new length of subarray found is less than already found min length
  # or new length found is equal to previous min length (should decrease window size
  # by increasing left pos to find the less length subarray)
  def min_or_equal_length?(new_length)
    new_length <= @min_length
  end
end
# Solution 2 ✅ 
# frozen_string_literal: true

# disable rubocop GuardClause for better readability in the code
###########################################################
# #209
# .............
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
    @min_length = 0 # default 0 -> solution not found
    @left_pos = 0
    @right_pos = 0
    @sum = nil
  end

  def min_size
    while @right_pos < @nums.length
      @sum = calculate_sum

      if solution_found?
        update_min_length

        return 1 if @min_length == 1 # best scenario found, stop here
      else
        @right_pos += 1 # increase window size by 1
      end
    end

    @min_length
  end

  private

  def calculate_sum
    # first position where left and right positions are at starting point
    return @nums[@right_pos] if @left_pos.zero? && @right_pos.zero?

    # add elements inside the window
    @nums[@left_pos..@right_pos].sum
  end

  def update_min_length
    new_length = @right_pos - @left_pos + 1

    @min_length = new_length if min_length_empty? || min_length_greater?(new_length)
    @left_pos += 1
  end

  def solution_found?
    @sum >= @target
  end

  def min_length_empty?
    @min_length.zero?
  end

  # if new length of subarray found is less than already found min length
  # or new length found is equal to previous min length (should decrease window size
  # by increasing left pos to find the less length subarray)
  def min_length_greater?(new_length)
    @min_length > new_length
  end
end

🧮 Algorithm Complexity Analysis

Time Complexity: O(n²)

Our current algorithm has quadratic time complexity due to the calculate_sum method:

def calculate_sum(nums, left_pos, right_pos)
  # This line causes O(n) complexity in each iteration
  nums[left_pos..right_pos].sum
end

Why O(n²)?

  • Outer loop: while right_pos < nums.length → O(n)
  • Inner operation: nums[left_pos..right_pos].sum → O(n)
  • Total: O(n) × O(n) = O(n²)

Solution: We should change this logic of repeated addition of numbers that are already added before. We can add the next Number (Right position) and substract the Left Number that is out of the window.

Space Complexity: O(1)

  • Only uses a constant number of variables regardless of input size
  • No additional data structures that grow with input

🚀 Optimized Version (O(n) Time):

Here’s how to make it linear time complexity:

Let’s Try to Optimize our solution with the Solution given above:

# frozen_string_literal: true

# disable rubocop GuardClause for better readability in the code
###########################################################
# ..................
###########################################################
class SubArray
  def initialize(nums, target)
    @nums = nums
    @target = target
    @min_length = 0 # default 0 -> solution not found
    @left_pos = 0
    @right_pos = 0
    @sum = 0
  end

  def min_size
    while @right_pos < @nums.length
      # Add the new element at right_pos to the current sum
      @sum += @nums[@right_pos]

      update_min_length if solution_found?

      @right_pos += 1 # always move right pointer
    end

    @min_length
  end

  private

  def update_min_length
    new_length = @right_pos - @left_pos + 1

    @min_length = new_length if min_length_empty? || min_length_greater?(new_length)

    # Shrink the window from the left as much as possible while maintaining sum >= target
    while @left_pos < @right_pos && (@sum - @nums[@left_pos]) >= @target
      @sum -= @nums[@left_pos]
      @left_pos += 1
      new_length = @right_pos - @left_pos + 1
      @min_length = new_length if min_length_greater?(new_length)
    end
  end

  def solution_found?
    @sum >= @target
  end

  def min_length_empty?
    @min_length.zero?
  end

  # if new length of subarray found is less than already found min length
  # or new length found is equal to previous min length (should decrease window size
  # by increasing left pos to find the less length subarray)
  def min_length_greater?(new_length)
    @min_length > new_length
  end
end

📊 Complexity Comparison:

VersionTime ComplexitySpace ComplexityWhy
Your CurrentO(n²)O(1)Recalculates sum each time
OptimizedO(n)O(1)Maintains running sum

Key Optimization:

Instead of recalculating the sum each time:

# Your approach (O(n) each time)
nums[left_pos..right_pos].sum

# Optimized approach (O(1) each time)
current_sum += num        # Add new element
current_sum -= nums[left] # Remove old element

Our algorithm works correctly but can be optimized from O(n²) to O(n) time complexity!

LeetCode Submission (simplified version of 0(n)):

# @param {Integer} target
# @param {Integer[]} nums
# @return {Integer}
def min_sub_array_len(target, nums)
  return 0 if nums.empty?

  min_length = Float::INFINITY
  left = 0
  sum = 0

  nums.each_with_index do |num, right|
    sum += num

    # Shrink window from left as much as possible while maintaining sum >= target
    while sum >= target && left <= right
      min_length = [min_length, right - left + 1].min
      sum -= nums[left]
      left += 1
    end
  end

  min_length == Float::INFINITY ? 0 : min_length
end

The Problem: https://leetcode.com/problems/minimum-size-subarray-sum/description/

The Solution: https://leetcode.com/problems/minimum-size-subarray-sum/description/?submissionId=1712728937

https://leetcode.com/problems/minimum-size-subarray-sum/submissions/1712728937/


Happy Algo Coding! 🚀

Introduction to Software Development Methodologies 📊: Part 1

Software development is not just about writing code; it’s about building high-quality, maintainable, and scalable systems that deliver value to users. To achieve this consistently, teams follow structured approaches known as software development methodologies. These methodologies provide a roadmap for planning, designing, developing, testing, and delivering software.

In this three-part blog series, we’ll explore key methodologies and best practices in software development, using Ruby and Ruby on Rails examples wherever appropriate.

🌐 What Are Software Development Methodologies?

Software development methodologies are structured processes or frameworks that guide the planning and execution of software projects. They help teams manage complexity, collaborate effectively, reduce risk, and deliver projects on time.

Common Goals of Any Methodology:

  • Define clear project scope and goals
  • Break down work into manageable tasks
  • Encourage communication among team members
  • Track progress and measure success
  • Deliver working software iteratively or incrementally

💼 Why Methodologies Matter

Without a methodology, software projects often suffer from unclear requirements, missed deadlines, buggy releases, or scope creep. A good methodology:

  • Increases team productivity
  • Ensures better quality and maintainability
  • Reduces time-to-market
  • Improves customer satisfaction

In Ruby and Rails projects, where rapid development is a key feature, following a methodology keeps things under control and makes collaboration more effective.

📖 A Brief Overview of Popular Software Development Methodologies

We’ll explore these in detail in Part 2, but here are the major ones:

1. Waterfall

A linear approach where each phase (Requirements → Design → Implementation → Testing → Deployment) happens one after another.

2. Agile

An iterative and incremental model that encourages adaptive planning, early delivery, and continuous improvement.

3. Scrum

A popular Agile framework that structures work in sprints and emphasizes team roles, ceremonies, and artifacts.

4. Kanban

A flow-based Agile method that uses visual boards to manage work and improve process efficiency.

5. Extreme Programming (XP)

Focuses on engineering excellence, including practices like pair programming, TDD, and continuous integration. Ruby and Rails communities highly embrace XP.

6. DevOps

Combines software development and IT operations to shorten the development lifecycle and deliver high-quality software continuously.

✨ Ruby On Rails: A Natural Fit for Agile and XP

Ruby and Rails were built with developer happiness and productivity in mind. They naturally complement Agile and XP methodologies.

Example:

  • Ruby on Rails encourages convention over configuration, which speeds up development.
  • It supports Test-Driven Development (TDD) out of the box via tools like RSpec and Minitest.
  • Features like scaffolding align well with the iterative nature of Agile.

🔹 Coming Up in Part 2

In the next part of this series, I’ll do a deep dive into each of the above methodologies, with pros, cons, and real-world use cases. I’ll also show examples of how Rails developers apply these in practice.


Stay tuned! 🚀

Rails 8 + ActiveAdmin: The Propshaft vs Sprockets Dilemma 😵‍💫

ActiveAdmin traditionally requires Sprockets, but there are actually several alternatives and modern approaches. Let me break down your options:

ActiveAdmin Asset Pipeline Options:

Current Status (2025):

  • ActiveAdmin 3.x still primarily designed for Sprockets
  • It has SCSS files with imports, mixins, and variables that need preprocessing
  • The gem’s CSS compilation expects Sprockets’ asset compilation pipeline

Alternative Approaches:

1. Keep Sprockets (Current Choice)

Pros:

  • ✅ Fully supported, zero configuration issues
  • ✅ All ActiveAdmin features work out-of-the-box
  • ✅ Stable and well-tested

Cons:

  • ❌ Older, slower asset pipeline
  • ❌ More complex than Propshaft
  • ❌ Goes against Rails 8 defaults

2. Hybrid: Propshaft + Manual CSS for ActiveAdmin

We could try keeping Propshaft for our main app and handling ActiveAdmin CSS manually:

ActiveAdmin traditionally requires Sprockets, but the landscape is evolving. Here are your realistic options in 2025:

Current Situation:

ActiveAdmin 3.x: Requires Sprockets (stable)
ActiveAdmin 4.0 beta: Propshaft support is experimental/problematic
Rails 8 default: 🆕 Propshaft (faster, simpler)

https://github.com/activeadmin/activeadmin/releases

Our Options:

Option 1: Stay with Sprockets(Recommended for now)

Pros:

  • ✅ Zero configuration issues
  • ✅ All ActiveAdmin features work perfectly
  • ✅ Battle-tested and stable

Cons:

  • ❌ Slower than Propshaft
  • ❌ More complex asset pipeline
  • ❌ Goes against Rails 8 defaults

Option 2: ActiveAdmin Assets Gem (Simplest alternative)

The activeadmin_assets gem was mentioned as a simple solution! Let me add this option:

# Add to Gemfile
gem 'activeadmin_assets'

Pros:

  • Zero Node.js dependency
  • ✅ Works with Propshaft
  • ✅ Pre-compiled ActiveAdmin assets
  • ✅ Simple one-line installation

Cons:

  • ❌ Can’t customize ActiveAdmin styles
  • ❌ Community-maintained (not official)

Option 3: ActiveAdmin 4.0 Beta + Propshaft ⚠️ (Experimental)

Based on the GitHub discussions, this is possible but complex:

# Rails 8 setup with Propshaft + ActiveAdmin v4 beta
rails new myapp -a propshaft
bundle add activeadmin -v=4.0.0beta15
bundle add cssbundling-rails -v "1.4.1"
bundle add tailwindcss-ruby -v "~> 3.4.17"
yarn add tailwindcss@^3.4.17  # ActiveAdmin uses TailwindCSS v3
rails generate active_admin:install
rails css:install:tailwind
yarn add @activeadmin/activeadmin@4.0.0-beta15
npm pkg set scripts.build:css="tailwindcss -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify -c tailwind-active_admin.config.js"
./bin/dev  # Instead of rails server

Pros:

  • ✅ Modern asset pipeline (Propshaft)
  • ✅ ActiveAdmin v4 features

Cons:

  • Complex setup with many steps
  • Requires Node.js/Yarn (defeats the purpose)
  • Beta software (unstable)
  • TailwindCSS v3/v4 compatibility issues

Option 4: 🔄 Alternative Admin Gems

If you want to avoid the ActiveAdmin complexity entirely:

RailsAdmin:

gem 'rails_admin'
  • ✅ No asset pipeline issues
  • ✅ Works with Propshaft out of the box
  • ❌ Less customizable than ActiveAdmin

Trestle:

gem 'trestle'
  • ✅ Modern Bootstrap-based admin
  • ✅ Better Rails 8 compatibility
  • ❌ Smaller community

Adminterface (Deprecated):

  • 🚫 No longer maintained (as shown in search results)

🏆 My Recommendation:

For our production app: Stick with Sprockets + ActiveAdmin 3.x

Why?

  1. Stability – Battle-tested combination
  2. Zero configuration issues
  3. Full feature set available
  4. Easy maintenance

The performance difference between Propshaft and Sprockets won’t significantly impact your admin interface, which is typically used by a small number of internal users.

🔮 Future Outlook:

  • ActiveAdmin v4 will eventually have better Propshaft support
  • Rails community is working on solutions
  • Node.js dependency trend for complex CSS is becoming standard anyway

Bottom Line: Don’t let the asset pipeline take over the application preferences. Our current Sprockets setup is perfectly fine for an admin interface! 🎯

The Reality Check:

We completely replaced Propshaft with Sprockets for the entire application.

Current State:

  • Propshaft: Completely removed
  • Sprockets: Handles ALL assets (main app + admin)
  • Importmaps: Still working (unaffected)
  • Hotwire/Turbo: Still working (unaffected)

🏗️ How ActiveAdmin Works:

ActiveAdmin is NOT a Rails Engine. It integrates directly into your app:

  1. Routes: ActiveAdmin.routes(self) adds /admin routes to your app
  2. Assets: ActiveAdmin’s CSS/JS becomes part of your asset pipeline
  3. Controllers: ActiveAdmin generates controllers in your app’s namespace

It’s tightly coupled to your application, not isolated.

🤷‍♂️ So What Are Our Real Options?

Option 1: Accept Current Setup

What we have:

  • Modern Rails 8 with Importmaps + Hotwire ✅
  • Sprockets instead of Propshaft ❌ (but works fine)
  • Full-featured admin interface ✅

Option 2: Use activeadmin_assets Gem

# Replace current ActiveAdmin setup
gem 'activeadmin_assets'  # Pre-compiled assets
gem 'propshaft'           # Back to Rails 8 default
  • Get Propshaft back
  • Keep Rails 8 defaults
  • Can’t customize admin styles

Option 3: Build Custom Admin

Keep your Rails 8 purity but build admin from scratch:

  • Pure Rails 8
  • Weeks of development time

🔍 What activeadmin_assets Actually Does:

The activeadmin_assets gem doesn’t replace ActiveAdmin – it provides pre-compiled static assets for ActiveAdmin to work with Propshaft.

But here’s the critical issue for you:

🚨 activeadmin_assets = NO Customization

If you use activeadmin_assets, you get:

  • ✅ ActiveAdmin functionality
  • ✅ Propshaft compatibility
  • ZERO ability to customize styles/interface

The assets are pre-compiled and frozen – you can’t modify colors, layouts, or styling.

Since We Want Customization:

We’re actually in the BEST setup possible right now!

Our Current Setup Gives You:

  • Full ActiveAdmin customization (styles, layouts, themes)
  • Modern Rails 8 (Importmaps, Hotwire, Turbo)
  • All ActiveAdmin features (filters, actions, forms)
  • ❌ Sprockets instead of Propshaft (minor trade-off)

Customization Examples We Can Do:

  • Custom CSS themes and colors
  • Layout modifications
  • Custom JavaScript behaviors
  • Brand styling and logos
  • Advanced form customizations

🎯 Reality Check:

There’s no solution that gives us:

  1. Full ActiveAdmin customization AND
  2. Propshaft AND
  3. Zero complexity

We have to pick 2 out of 3.

💡 My Honest Last Decision:

Keep our current setup! We’re already using:

  • Modern Rails 8 patterns (95% of your codebase)
  • Importmaps and Hotwire (exactly as intended)
  • Sprockets only affects asset serving (invisible to users)

The Propshaft vs Sprockets difference won’t affect your product’s success, but losing admin customization will limit our UX.


Guide: Integrating React ⚛️ into a Rails 8 Application – Part 3 | Start developing react

Let’s move on to quick development of more react components now. Before that let’s check what we have now and understand it very clear.

📄 File 1:

Our app/javascript/components/App.jsx file:

import React from 'react';

function App() {
  return (
    <div>
      <h1>React is working fine!</h1>
      <p>Welcome to Rails + React App</p>
    </div>
  );
}

export default App;

Let’s examine this React component step by step:

Line 1: Import React

import React from 'react';
  • import – ES6 module syntax to bring in external code
  • React – The main React library
  • from 'react' – Importing from the npm package named “react”
  • Why needed? Even though we use --jsx=automatic, we still import React for any hooks or React features we might use.

Function Component: Line 3-9

A React function component is a simple JavaScript function that serves as a building block for user interfaces in React applications. These components are designed to be reusable and self-contained, encapsulating a specific part of the UI and its associated logic.

function App() {
  return (
    <div>
      <h1>React is working fine!</h1>
      <p>Welcome to Rails + React App</p>
    </div>
  );
}

🔍 Breaking this down:

Line 3: Component Declaration

function App() {
  • function App() – This is a React Function Component
  • Component naming – Must start with capital letter (App, not app)
  • What it is – A JavaScript function that returns JSX (user interface)

Line 4-8: JSX Return

return (
  <div>
    <h1>React is working fine!</h1>
    <p>Welcome to Rails + React App</p>
  </div>
);
  • return – Every React component must return something
  • JSX – Looks like HTML, but it’s actually JavaScript
  • <div> – Must have one parent element (React Fragment rule)
  • <h1> & <p> – Regular HTML elements, but processed by React

Line 11: Export

export default App;
  • export default – ES6 syntax to make this component available to other files
  • App – The component name we’re exporting
  • Why needed? So application.js can import and use this component

📄 File 2:

Our app/javascript/application.js file:

// Entry point for the build script in your package.json
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './components/App';

document.addEventListener('DOMContentLoaded', () => {
  const container = document.getElementById('react-root');

  if(container) {
    const root = createRoot(container);
    root.render(<App />);
  }
});

This is the entry point that connects React to your Rails app:

    Imports: Line 2-4

    import React from 'react';
    import { createRoot } from 'react-dom/client';
    import App from './components/App';
    

    🔍 Breaking down each import:

    Line 2:

    import React from 'react';
    
    • Same as before – importing the React library

    Line 3:

    import { createRoot } from 'react-dom/client';
    
    • { createRoot }Named import (notice the curly braces)
    • react-dom/client – ReactDOM library for browser/DOM manipulation
    • createRoot – New React 18+ API for rendering components to DOM

    Line 4:

    import App from './components/App';
    
    • AppDefault import (no curly braces)
    • ./components/App – Relative path to our App component
    • Note: We don’t need .jsx extension, esbuild figures it out

    DOM Integration: Line 6-12

    document.addEventListener('DOMContentLoaded', () => {
      const container = document.getElementById('react-root');
    
      if(container) {
        const root = createRoot(container);
        root.render(<App />);
      }
    });
    

    🔍 Step by step breakdown:

    Line 6:

    document.addEventListener('DOMContentLoaded', () => {
    
    • document.addEventListener – Standard browser API
    • 'DOMContentLoaded' – Wait until HTML is fully loaded
    • () => { – Arrow function (ES6 syntax)
    • Why needed? Ensures the HTML exists before React tries to find elements

    Line 7:

    const container = document.getElementById('react-root');
    
    • const container – Create a variable to hold the DOM element
    • document.getElementById('react-root') – Find HTML element with id="react-root"
    • Where is it? In your Rails view file: app/views/home/index.html.erb

    Line 9:

    if(container) {
    
    • Safety check – Only proceed if the element exists
    • Prevents errors – If someone visits a page without react-root element

    Line 10-11:

    const root = createRoot(container);
    root.render(<App />);
    
    • createRoot(container) – Create a React “root” at the DOM element
    • root.render(<App />) – Render our App component inside the container
    • <App /> – JSX syntax for using our component (self-closing tag)

    🎯 Key React Concepts You Just Learned:

    1. Components

    • Functions that return JSX
    • Must start with capital letter
    • Reusable pieces of UI

    2. JSX

    • Looks like HTML, actually JavaScript
    • Must return single parent element
    • Processed by esbuild into regular JavaScript

    3. Import/Export

    • Default exports: export default Appimport App from './App'
    • Named exports: export { createRoot }import { createRoot } from 'package'

    4. React DOM

    • createRoot() – Modern way to mount React apps (React 18+)
    • render() – Display components in the browser

    5. Rails Integration

    • Rails serves the HTML page
    • React takes over the #react-root element
    • esbuild bundles everything together

    🚀 This pattern is the foundation of every React app! We create components, import them, and render them to the DOM.


    📚 Step-by-Step React Learning with Todo List

    Now let’s build a Todo List app step by step. I’ll explain each React concept thoroughly as we go. Here’s our learning roadmap:

    Step 1: Understanding JSX and Basic Component Structure

    First, let’s update our App.jsx to create the basic structure of our Todo app:

    import React from 'react';
    
    function App() {
      return (
        <div className="todo-app">
          <h1>My Todo List</h1>
          <p>Let's learn React by building a todo app!</p>
    
          {/* This is a JSX comment */}
          <div className="todo-container">
            <h2>Add a new todo</h2>
            <input type="text" placeholder="Enter a todo..." />
            <button>Add Todo</button>
    
            <h2>My Todos</h2>
            <ul>
              <li>Learn React basics</li>
              <li>Build a todo app</li>
              <li>Master React hooks</li>
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;
    

    🎯 Key Concepts Explained:

    JSX (JavaScript XML):

    • JSX lets you write HTML-like syntax directly in JavaScript
    • It’s a syntax extension for JavaScript, not actual HTML
    • JSX gets compiled to JavaScript function calls
    • You can use {} to embed JavaScript expressions inside JSX

    Important JSX Rules:

    • Use className instead of class (because class is a reserved word in JavaScript)
    • You can use single quotes for className values in JSX. Both work perfectly fine:
    // Both of these are valid:
    <div className='todo-app'>    // Single quotes ✅
    <div className="todo-app">    // Double quotes ✅
    

    Quote Usage in JSX/JavaScript:

    Single quotes vs Double quotes:

    • JavaScript treats them identically
    • It’s mostly a matter of personal/team preference
    • The key is to be consistent throughout your project

    Common conventions:

    // Option 1: Single quotes for JSX attributes
    <div className='todo-app'>
      <input type='text' placeholder='Enter todo...' />
    </div>
    
    // Option 2: Double quotes for JSX attributes  
    <div className="todo-app">
      <input type="text" placeholder="Enter todo..." />
    </div>
    
    // Option 3: Mixed (but stay consistent within each context)
    const message = 'Hello World';  // Single for JS strings
    <div className="todo-app">      // Double for JSX attributes
    

    When you MUST use specific quotes:

    // When the string contains the same quote type
    <div className="It's a great day">        // Double quotes needed
    <div className='He said "Hello"'>        // Single quotes needed
    
    // Or use escape characters
    <div className='It\'s a great day'>       // Escaping single quote
    <div className="He said \"Hello\"">      // Escaping double quote
    

    💡 Tip: Many teams use tools like Prettier or ESLint to automatically format and enforce consistent quote usage across the entire project.

    • All tags must be closed (self-closing tags need / at the end)
    • JSX comments use {/* */} syntax
    • Return a single parent element (or use React Fragment <>...</>)

    Try updating our App.jsx with this code and see it in your browser!


    Step 2: Introduction to State with useState

    Now let’s add state to make our app interactive. State is data that can change over time.

    import React, { useState } from 'react';
    
    function App() {
      // useState Hook - creates state variable and setter function
      const [todos, setTodos] = useState([
        { id: 1, text: 'Learn React basics', completed: false },
        { id: 2, text: 'Build a todo app', completed: false },
        { id: 3, text: 'Master React hooks', completed: true }
      ]);
    
      const [inputValue, setInputValue] = useState('');
    
      return (
        <div className="todo-app">
          <h1>My Todo List</h1>
    
          <div className="todo-container">
            <h2>Add a new todo</h2>
            <input 
              type="text" 
              placeholder="Enter a todo..." 
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
            />
            <button>Add Todo</button>
    
            <h2>My Todos ({todos.length})</h2>
            <ul>
              {todos.map(todo => (
                <li key={todo.id}>
                  {todo.text} {todo.completed ? '✅' : '⏳'}
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;
    

    🎯 Key Concepts Explained:

    useState Hook:

    • useState is a React Hook that lets you add state to functional components
    • It returns an array with two elements: [currentValue, setterFunction]
    • const [todos, setTodos] = useState([]) creates a state variable todos and a function setTodos to update it
    • The initial value is passed as an argument to useState

    Controlled Components:

    • The input field is now “controlled” by React state
    • value={inputValue} makes the input show what’s in state
    • onChange={(e) => setInputValue(e.target.value)} updates state when user types

    Array.map() for Rendering Lists:

    • todos.map() transforms each todo into a JSX element
    • Each list item needs a unique key prop for React’s optimization
    • {todo.text} embeds the todo text using JSX expressions

    Try this code and notice how the input field now responds to typing!


    Step 3: Event Handling and Adding Todos

    Let’s make the “Add Todo” button work:

    import React, { useState } from 'react';
    
    function App() {
      const [todos, setTodos] = useState([
        { id: 1, text: 'Learn React basics', completed: false },
        { id: 2, text: 'Build a todo app', completed: false },
        { id: 3, text: 'Master React hooks', completed: true }
      ]);
    
      const [inputValue, setInputValue] = useState('');
    
      // Function to add a new todo
      const addTodo = () => {
        if (inputValue.trim() !== '') {
          const newTodo = {
            id: Date.now(), // Simple ID generation
            text: inputValue,
            completed: false
          };
    
          setTodos([...todos, newTodo]); // Spread operator to add new todo
          setInputValue(''); // Clear the input field
        }
      };
    
      // Function to handle Enter key press
      const handleKeyPress = (e) => {
        if (e.key === 'Enter') {
          addTodo();
        }
      };
    
      return (
        <div className="todo-app">
          <h1>My Todo List</h1>
    
          <div className="todo-container">
            <h2>Add a new todo</h2>
            <input 
              type="text" 
              placeholder="Enter a todo..." 
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
            />
            <button onClick={addTodo}>Add Todo</button>
    
            <h2>My Todos ({todos.length})</h2>
            <ul>
              {todos.map(todo => (
                <li key={todo.id}>
                  {todo.text} {todo.completed ? '✅' : '⏳'}
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;
    

    🎯 Key Concepts Explained:

    Event Handlers:

    • onClick={addTodo} – function runs when button is clicked
    • onKeyPress={handleKeyPress} – function runs when key is pressed
    • Event handlers receive an event object (e) with information about the event

    State Updates:

    • setTodos([...todos, newTodo]) – creates a new array with all existing todos plus the new one
    • Important: Always create new arrays/objects instead of mutating existing ones
    • React compares old and new state to determine if re-render is needed

    Spread Operator (...):

    • ...todos spreads out all elements of the todos array
    • This is the React way to add items to an array in state

    Try adding new todos now!

    🎯 Function Syntax Options in JavaScript:

    const addTodo = () => {} is a function syntax – specifically an arrow function. Let me explain why it’s declared as const and the different ways to write functions in JavaScript.

    1. Function Declaration (Traditional)
    function addTodo() {
      // function body
    }
    
    2. Function Expression with Arrow Function
    const addTodo = () => {
      // function body
    };
    
    3. Function Expression (Traditional)
    const addTodo = function() {
      // function body
    };
    

    🤔 Why use const for functions?

    Arrow functions are expressions, not declarations:

    // This is a DECLARATION - creates a function named addTodo
    function addTodo() { }
    
    // This is an EXPRESSION - creates a function and assigns it to a variable
    const addTodo = () => { };
    

    Why const specifically?

    // ❌ Could be reassigned accidentally
    let addTodo = () => { };
    addTodo = "oops"; // Function is now gone!
    
    // ❌ Could be reassigned accidentally  
    var addTodo = () => { };
    addTodo = null; // Function is now gone!
    
    // ✅ Cannot be reassigned - prevents bugs
    const addTodo = () => { };
    addTodo = "something"; // ERROR: Assignment to constant variable
    

    📚 Key Differences:

    Function Declaration vs Arrow Function:

    // Function Declaration
    function addTodo() {
      console.log("Adding todo");
    }
    
    // Arrow Function (assigned to const)
    const addTodo = () => {
      console.log("Adding todo");
    };
    

    Hoisting Behavior:

    // ✅ This works - function declarations are "hoisted"
    sayHello(); // "Hello!"
    
    function sayHello() {
      console.log("Hello!");
    }
    
    // ❌ This doesn't work - arrow functions are not hoisted
    sayGoodbye(); // Error: Cannot access 'sayGoodbye' before initialization
    
    const sayGoodbye = () => {
      console.log("Goodbye!");
    };
    

    this Binding:

    // Function declaration has its own 'this'
    function regularFunction() {
      console.log(this); // 'this' can change based on how it's called
    }
    
    // Arrow function inherits 'this' from surrounding scope
    const arrowFunction = () => {
      console.log(this); // 'this' is inherited from parent scope
    };
    

    🚀 In React Context:

    In React functional components, we typically use arrow functions with const because:

    1. Prevents accidental reassignment – our function won’t get overwritten
    2. Consistent with modern JavaScript – ES6+ standard
    3. Cleaner syntax – less verbose than traditional function expressions
    4. Better for event handlersthis behavior is more predictable

    All these are equivalent in React:

    // Option 1: Arrow function with const (most common)
    const addTodo = () => {
      if (inputValue.trim() !== '') {
        // ... logic
      }
    };
    
    // Option 2: Traditional function declaration
    function addTodo() {
      if (inputValue.trim() !== '') {
        // ... logic  
      }
    }
    
    // Option 3: Function expression with const
    const addTodo = function() {
      if (inputValue.trim() !== '') {
        // ... logic
      }
    };
    

    💡 Why React developers prefer arrow functions:

    1. Shorter syntax for simple functions
    2. Consistent variable declaration (everything uses const)
    3. No hoisting confusion – functions are defined before they’re used
    4. Better with modern tooling – ESLint, Prettier handle them well

    So yes, const addTodo = () => {} is definitely a function! It’s just a modern way to write functions that prevents accidental reassignment and has cleaner syntax.

    🎯 What is Hoisting?

    Hoisting is a fundamental JavaScript concept that can be confusing at first. Let me explain it clearly with examples.

    Hoisting is JavaScript’s behavior of moving declarations to the top of their scope during the compilation phase, before the code is executed.

    Think of it like JavaScript “hoists” (lifts up) your variable and function declarations to the top of their scope.

    📚 How Hoisting Works:

    Function Declarations are Hoisted:

    // This works even though we call the function before declaring it!
    sayHello(); // Outputs: "Hello!"
    
    function sayHello() {
      console.log("Hello!");
    }
    

    Behind the scenes, JavaScript treats it like this:

    // JavaScript internally reorganizes it like this:
    function sayHello() {
      console.log("Hello!");
    }
    
    sayHello(); // Now it makes sense!
    

    Variable Declarations (with var) are Hoisted:

    console.log(myVar); // Outputs: undefined (not an error!)
    var myVar = "Hello";
    console.log(myVar); // Outputs: "Hello"
    

    Behind the scenes:

    // JavaScript internally treats it like this:
    var myVar; // Declaration is hoisted to the top
    console.log(myVar); // undefined (declared but not assigned)
    myVar = "Hello"; // Assignment stays in place
    console.log(myVar); // "Hello"
    

    🚫 What is NOT Hoisted:

    let and const variables:

    // ❌ This throws an error!
    console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
    let myLet = "Hello";
    
    // ❌ This also throws an error!
    console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
    const myConst = "Hello";
    

    Arrow Functions (Function Expressions):

    // ❌ This throws an error!
    sayGoodbye(); // ReferenceError: Cannot access 'sayGoodbye' before initialization
    
    const sayGoodbye = () => {
      console.log("Goodbye!");
    };
    

    Function Expressions:

    // ❌ This throws an error!
    myFunc(); // TypeError: myFunc is not a function
    
    var myFunc = function() {
      console.log("Hello!");
    };
    

    🎭 Visual Examples:

    Example 1: Function Declaration Hoisting

    // What you write:
    console.log("1. Start");
    myFunction(); // This works!
    console.log("3. End");
    
    function myFunction() {
      console.log("2. Inside function");
    }
    
    // Output:
    // 1. Start
    // 2. Inside function  
    // 3. End
    

    Example 2: Variable Hoisting with var

    // What you write:
    console.log("Value is:", myVar); // undefined
    var myVar = 42;
    console.log("Value is:", myVar); // 42
    
    // What JavaScript internally does:
    var myVar; // Hoisted to top
    console.log("Value is:", myVar); // undefined
    myVar = 42; // Assignment stays here
    console.log("Value is:", myVar); // 42
    

    Example 3: Why let and const are different

    // Temporal Dead Zone
    console.log("Before declaration");
    
    // ❌ This area is the "Temporal Dead Zone" for myLet
    console.log(myLet); // ReferenceError!
    
    let myLet = "Hello"; // Declaration and initialization
    console.log(myLet); // "Hello"
    

    🎯 Practical Implications:

    Why Function Declarations work everywhere:

    // This is valid JavaScript
    function main() {
      helper(); // Can call helper before it's defined
    
      function helper() {
        console.log("Helper function");
      }
    }
    

    Why Arrow Functions must be defined first:

    // ❌ This doesn't work
    const main = () => {
      helper(); // Error! helper is not defined yet
    
      const helper = () => {
        console.log("Helper function");
      };
    };
    
    // ✅ This works
    const main = () => {
      const helper = () => {
        console.log("Helper function");
      };
    
      helper(); // Now it works!
    };
    

    💡 Best Practices:

    1. Use const and let instead of var to avoid hoisting confusion
    2. Define functions before using them (even though function declarations are hoisted)
    3. Use arrow functions for consistency and to avoid hoisting surprises
    4. Initialize variables when you declare them

    🔗 Back to Our React Example:

    // This is why we use const for arrow functions
    const App = () => {
      // ✅ Good: Function defined before use
      const addTodo = () => {
        // function logic
      };
    
      // ❌ Bad: Would cause error if we tried to call it here
      // deleteTodo(); // Error!
    
      const deleteTodo = () => {
        // function logic
      };
    
      return (
        <div>
          <button onClick={addTodo}>Add</button>
          <button onClick={deleteTodo}>Delete</button>
        </div>
      );
    };
    

    Hoisting is JavaScript’s way of making function declarations available throughout their scope, but it doesn’t apply to modern variable declarations (let, const) or arrow functions. That’s why we define our functions with const and make sure to declare them before we use them!


    Step 4: Toggling Todo Completion

    Let’s add the ability to mark todos as complete/incomplete:

    import React, { useState } from 'react';
    
    function App() {
      const [todos, setTodos] = useState([
        { id: 1, text: 'Learn React basics', completed: false },
        { id: 2, text: 'Build a todo app', completed: false },
        { id: 3, text: 'Master React hooks', completed: true }
      ]);
    
      const [inputValue, setInputValue] = useState('');
    
      const addTodo = () => {
        if (inputValue.trim() !== '') {
          const newTodo = {
            id: Date.now(),
            text: inputValue,
            completed: false
          };
    
          setTodos([...todos, newTodo]);
          setInputValue('');
        }
      };
    
      // Function to toggle todo completion
      const toggleTodo = (id) => {
        setTodos(todos.map(todo => 
          todo.id === id 
            ? { ...todo, completed: !todo.completed } // Toggle completed status
            : todo // Keep todo unchanged
        ));
      };
    
      const handleKeyPress = (e) => {
        if (e.key === 'Enter') {
          addTodo();
        }
      };
    
      return (
        <div className="todo-app">
          <h1>My Todo List</h1>
    
          <div className="todo-container">
            <h2>Add a new todo</h2>
            <input 
              type="text" 
              placeholder="Enter a todo..." 
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              onKeyPress={handleKeyPress}
            />
            <button onClick={addTodo}>Add Todo</button>
    
            <h2>My Todos ({todos.length})</h2>
            <ul>
              {todos.map(todo => (
                <li key={todo.id}>
                  <span 
                    onClick={() => toggleTodo(todo.id)}
                    style={{ 
                      textDecoration: todo.completed ? 'line-through' : 'none',
                      cursor: 'pointer',
                      color: todo.completed ? '#888' : '#000'
                    }}
                  >
                    {todo.text}
                  </span>
                  {todo.completed ? ' ✅' : ' ⏳'}
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;
    

    🎯 Key Concepts Explained:

    Array.map() for Updates:

    • todos.map() creates a new array where each todo is either updated or kept the same
    • todo.id === id ? { ...todo, completed: !todo.completed } : todo
    • This pattern is very common in React for updating items in arrays

    Conditional Rendering:

    • todo.completed ? '✅' : '⏳' – ternary operator for conditional display
    • textDecoration: todo.completed ? 'line-through' : 'none' – conditional styling

    Arrow Functions in JSX:

    • onClick={() => toggleTodo(todo.id)} – arrow function to pass arguments to event handlers
    • Without the arrow function, toggleTodo(todo.id) would execute immediately

    Now you can click on todos to toggle their completion status!


    🤔 What we’ve learned so far:

    1. ✅ JSX syntax and rules
    2. ✅ useState hook for state management
    3. ✅ Event handling (onClick, onChange, onKeyPress)
    4. ✅ Controlled components
    5. ✅ Array mapping for rendering lists
    6. ✅ Conditional rendering
    7. ✅ State updates with spread operator

    Next Steps: In the following steps, we’ll cover:

    • Deleting todos
    • Component composition (breaking into smaller components)
    • Props passing
    • Filtering todos
    • More advanced state management

    Let’s see in Part 4. Happy React Development! 🚀

    📦 Sprockets vs 🧵 Propshaft in Ruby on Rails 7/8 – What’s the Difference?

    When working with asset pipelines in Ruby on Rails 7 and 8, you might encounter Sprockets and Propshaft—two asset handling libraries. While both aim to serve static assets like JavaScript, CSS, images, and fonts, they do so in different ways.

    This post will walk you through what each does, how they differ, and when you might want to use one over the other.


    📦 What is Sprockets?

    Sprockets is the original Rails asset pipeline system, introduced way back in Rails 3.1. It allows developers to:

    • Concatenate and minify JavaScript and CSS
    • Preprocess assets using things like SCSS, CoffeeScript, ERB, etc.
    • Fingerprint assets for cache busting
    • Compile assets at deploy time

    It works well for traditional Rails applications where the frontend and backend are tightly coupled.

    Pros:

    • Mature and stable
    • Rich preprocessing pipeline (SCSS, CoffeeScript, ERB, etc.)
    • Supports advanced directives like //= require_tree .

    Cons:

    • Complex internal logic
    • Slower compilation times
    • Relies on a manifest file that can get messy
    • Tightly coupled with older Rails asset practices

    🧵 What is Propshaft?

    Propshaft is the newer asset pipeline introduced by the Rails team as an alternative to Sprockets. It focuses on simplicity and modern best practices. Propshaft was added as an optional asset pipeline starting in Rails 7 and is included by default in some new apps.

    Design Philosophy:
    Propshaft aims to work like a static file server with fingerprinting and logical path mapping, rather than a full asset compiler.

    Key Features:

    • Uses logical paths (e.g., /assets/application.css)
    • No preprocessing pipeline by default (but supports it via extensions like Tailwind or Sass)
    • Supports digesting (fingerprinting) of assets
    • Leaner and faster than Sprockets
    • Easier to integrate with modern JavaScript bundlers (like importmaps, esbuild, or webpack)

    Pros:

    • Lightweight and fast
    • Easier to debug
    • Works great with importmaps and Hotwire
    • Modern, forward-looking approach

    Cons:

    • No advanced preprocessing by default
    • Limited plugin ecosystem (still maturing)
    • Doesn’t support old Sprockets directives

    🔍 Key Differences at a Glance

    FeatureSprocketsPropshaft
    Introduced InRails 3.1Rails 7
    Default in RailsRails 6 and earlierOptional from Rails 7+
    Preprocessing SupportYes (SCSS, ERB, CoffeeScript, etc.)No (only raw assets by default)
    SpeedSlowerFaster
    Configuration ComplexityHigherMinimal
    Plugin EcosystemLarge and matureNew and growing
    Use With Importmaps/HotwireCan work, but heavierIdeal
    DebuggingHarder due to complexityEasier

    🧰 When Should You Use Sprockets?

    Choose Sprockets if:

    • You are upgrading a legacy Rails app
    • Your project already relies on Sprockets
    • You use heavy asset preprocessing
    • You need compatibility with gems that depend on Sprockets

    ⚡ When Should You Use Propshaft?

    Choose Propshaft if:

    • You are starting a new Rails 7/8 project
    • You use Importmaps or Hotwire/Turbo
    • You prefer faster and simpler asset handling
    • You don’t need complex preprocessing

    Propshaft pairs particularly well with modern frontend workflows like Tailwind CSS (via build tools) or StimulusJS (with importmaps).

    🛠️ Switching from Sprockets to Propshaft

    If you’re migrating, here are basic steps:

    1. Remove sprockets-rails gem from your Gemfile: # Gemfile # gem "sprockets-rails"
    2. Add propshaft: gem "propshaft"
    3. Update config/application.rb: config.assets.resolver = Propshaft::Resolver.new( paths: [Rails.root.join("app/assets")] )
    4. Remove app/assets/config/manifest.js (used by Sprockets)
    5. Move all assets to the correct logical paths under app/assets
    6. Use digested URLs as needed (asset_path("application.css") etc.)

    🧪 Real Example in Rails 8

    Here’s how your application.html.erb might look using Propshaft:

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
    
    

    And your app/assets/builds/application.css could be compiled via Tailwind or SCSS using a toolchain.


    🧠 Final Thoughts

    Sprockets has served Rails well for over a decade, but Propshaft is the new lightweight future. If you’re starting fresh, Propshaft is a strong choice, especially when used alongside Hotwire, Importmaps, or modern JS bundlers.

    However, don’t feel pressured to switch if your current Sprockets setup works fine—Rails continues to support both.


    ✨ TL;DR

    • Sprockets = older, feature-rich, best for legacy apps
    • Propshaft = newer, minimal, better for modern workflows

    Choose based on your app’s needs and complexity. Cheers! 🚀

    Design Studio – Rails 8 e-commerce application: Integrate active admin

    Now let’s create an admin interface for our e-commerce Application.

    We have a well-structured e-commerce Rails application with:

    Models: User, Product, ProductVariant, Order, OrderItem
    Authentication: Custom session-based auth with user roles (customer/admin)
    Authorization: Already has admin role checking

    Admin Interface Recommendations

    Here are the best options for Rails admin interfaces, ranked by suitability for our project:

    1. ActiveAdmin (Recommended ⭐)
      Best fit for e-commerce with complex associations
      Excellent filtering, search, and batch operations
      Great customization options and ecosystem
      Handles your Product → ProductVariant → OrderItem relationships well
    2. Administrate (Modern Alternative)
      Clean, Rails-way approach by Thoughtbot
      Good for custom UIs, less configuration
      More work to set up initially
    3. Rails Admin (What you asked about)
      Quick setup but limited customization
      Less actively maintained
      Good for simple admin needs
    4. Avo (Modern Premium)
      Beautiful modern UI
      Some features require paid version

      https://avohq.io/rails-admin
      https://docs.avohq.io/3.0/

    Choose ActiveAdmin for our e-commerce application. Let’s integrate it with our existing authentication system

    Add in Gemfile:

    gem "activeadmin"
    gem "sassc-rails" # Required for ActiveAdmin
    gem "image_processing", "~> 1.2" # For variant processing if not already present
    

    Bundle Install and run the Active Admin Generator:

    $ bundle install
    $ rails generate active_admin:install --skip-users
    definition of Rules was here
    create app/assets/javascripts/active_admin.js
    create app/assets/stylesheets/active_admin.scss
    create db/migrate/20250710083516_create_active_admin_comments.rb
    

    Migration File created by Active Admin:

    class CreateActiveAdminComments < ActiveRecord::Migration[8.0]
      def self.up
        create_table :active_admin_comments do |t|
          t.string :namespace
          t.text   :body
          t.references :resource, polymorphic: true
          t.references :author, polymorphic: true
          t.timestamps
        end
        add_index :active_admin_comments, [ :namespace ]
      end
    
      def self.down
        drop_table :active_admin_comments
      end
    end
    

    Run database migration:

    $ rails db:migrate
    

    in app/initializers/active_admin.rb

    # This setting changes the method which Active Admin calls
      # within the application controller.
      config.authentication_method = :authenticate_admin_user!
    ....
    # This setting changes the method which Active Admin calls
      # (within the application controller) to return the currently logged in user.
      config.current_user_method = :current_admin_user
    ....
     # Default:
      config.logout_link_path = :destroy_session_path
    

    in app/controllers/application_controller.rb

    private
    
      def authenticate_admin_user!
        require_authentication
        ensure_admin
      end
    
      def current_admin_user
        Current.user if Current.user&.admin?
      end
    

    Run the active admin user, product generator:

    rails generate active_admin:resource User
    rails generate active_admin:resource Product
    rails generate active_admin:resource ProductVariant
    rails generate active_admin:resource Order
    rails generate active_admin:resource OrderItem
    

    Let’s update all the active admin resources with fields, filters, attributes, panels etc.

    Let’s add accepts_nested_attributes_for :variants, allow_destroy: true in Product Model.

    accepts_nested_attributes_for is a Rails feature that allows a parent model to accept and process attributes for its associated child models through nested parameters. Here’s what it does:

    What it enables:

    1. Nested Forms: You can create/update a Product and its ProductVariants in a single form submission
    2. Mass Assignment: Allows passing nested attributes through strong parameters
    3. CRUD Operations: Create, update, and delete associated records through the parent

    In our Product model

    class Product < ApplicationRecord
      has_many :variants, dependent: :destroy, class_name: "ProductVariant"
      accepts_nested_attributes_for :variants, allow_destroy: true
    end
    

    What this allows:

    Before: You’d need separate forms/requests for Product and ProductVariant

    # Create product first
    product = Product.create(name: "T-Shirt", brand: "Nike")
    
    # Then create variants separately
    product.variants.create(size: "M", color: "Red", sku: "NIKE-001-M-RED")
    product.variants.create(size: "L", color: "Blue", sku: "NIKE-001-L-BLUE")
    

    After: You can do it all in one go:

    Product.create(
      name: "T-Shirt",
      brand: "Nike",
      variants_attributes: [
        { size: "M", color: "Red", sku: "NIKE-001-M-RED" },
        { size: "L", color: "Blue", sku: "NIKE-001-L-BLUE" }
      ]
    )
    

    In ActiveAdmin context:

    This enables the nested form I created in app/admin/products.rb:

    f.inputs "Product Variants" do
      f.has_many :variants, allow_destroy: true, new_record: true do |v|
        v.input :size
        v.input :color
        v.input :sku
        # ... other variant fields
      end
    end
    
    The allow_destroy: true option:
    • Allows deleting associated records by passing _destroy: ‘1’
    • Essential for updating existing products with variants
    Strong Parameters:

    In the controller, you need to permit the nested attributes:

    permit_params :name, :brand, :category, 
                  variants_attributes: [:id, :size, :color, :sku, :mrp, :discount_percent, :_destroy]
    

    Propshaft vs Sprockets

    We have an error after integrating ActiveAdmin:

     Rendered layout layouts/application.html.erb (Duration: 13.9ms | GC: 0.5ms)
    Completed 500 Internal Server Error in 118ms (ActiveRecord: 28.7ms (1 query, 0 cached) | GC: 27.0ms)
    
    ActionView::Template::Error (undefined method 'load_path' for an instance of Sprockets::Environment)
    Caused by: NoMethodError (undefined method 'load_path' for an instance of Sprockets::Environment)
    
    Information for: ActionView::Template::Error (undefined method 'load_path' for an instance of Sprockets::Environment):
        14:     <link rel="icon" href="/icon.svg" type="image/svg+xml">
        15:     <link rel="apple-touch-icon" href="/icon.png">
    

    This error occurs because Rails 8 uses Propshaft by default, but ActiveAdmin requires Sprockets. There’s a conflict between the two asset pipelines. Let’s fix this by switching to Sprockets completely:

    Add in Gemfile:

    # Use Sprockets for asset pipeline (required for ActiveAdmin)
    gem "sprockets-rails"
    
    $ bundle install
    
    # application.rb
    
    # Use Sprockets for asset pipeline
    config.assets.enabled = true
    
    

    Let’s create an admin user for testing our ActiveAdmin:

    # seed.rb
    # Create admin user
    admin_user = User.find_or_create_by(email: "admin@designstudio.com") do |user|
      user.password = "password123"
      user.role = "admin"
    end
    
    puts "Admin user created: #{admin_user.email}" if admin_user.persisted?
    
    # run seed
    ✗ rails db:seed
    

    ✅ We have Successfully Integrated ActiveAdmin with Nested Attributes!

    I’ve successfully converted your Rails application to use nested attributes (variants_attributes) across the entire stack and integrated ActiveAdmin as your admin interface. Here’s what I accomplished:

    🔧 What I Fixed & Updated:

    1. Asset Pipeline Issue:

    • Switched from Propshaft to Sprockets (required for ActiveAdmin)
    • Updated Gemfile and application configuration
    • Fixed the load_path error you encountered

    2. Model Changes:

    • Added accepts_nested_attributes_for :variants, allow_destroy: true to Product model

    3. Controller Updates:

    • Changed from singular product_variant to plural variants_attributes
    • Simplified create/update actions to use Rails nested attributes handling
    • Updated strong parameters to permit nested variant attributes

    4. View Improvements:

    • Updated product form to handle multiple variants with JavaScript
    • Added “Add Another Variant” functionality
    • Implemented proper nested form fields for all variant attributes

    5. ActiveAdmin Configuration:

    • Set up authentication using your existing user system
    • Created comprehensive admin resources for all models
    • Added nested variant management in product forms
    • Built an informative dashboard with key metrics

    🎯 Admin Features You Now Have:

    1. Dashboard – Key metrics, recent activity, revenue overview
    2. User Management – Role management, order history
    3. Product Management – Full CRUD with image uploads and nested variants
    4. Order Management – Status updates, batch actions
    5. Inventory Management – ProductVariant and OrderItem tracking

    🔑 Admin Access:

    • URL: http://localhost:3000/admin
    • Email: admin@designstudio.com
    • Password: password123

    📊 Comparison vs Rails Admin:

    ActiveAdmin Advantages:

    • ✅ Better for complex e-commerce relationships
    • ✅ Excellent filtering and search capabilities
    • ✅ Powerful batch operations
    • ✅ Highly customizable forms and views
    • ✅ Active development and community

    Rails Admin:

    • ❌ Less actively maintained
    • ❌ Limited customization options
    • ❌ Not as suitable for complex nested relationships

    🚀 What’s Working Now:

    1. Nested Variants: Create/edit products with multiple variants in one form
    2. Image Management: Upload multiple product images
    3. Order Processing: Admin can update order statuses with batch actions
    4. User Management: Promote users to admin, view order history
    5. Dashboard Analytics: Revenue tracking, popular products, recent activity

    Visit /admin to explore your new admin interface, Cheers!

    Check docs: https://activeadmin.info/0-installation.html

    ⚠️ Tailwind UI Issue after moving to Sprockets

    The issue is with <%= stylesheet_link_tag "/tailwind/application.css", "data-turbo-track": "reload" %> where we are trying to load Tailwind CSS from an incorrect path. The tailwindcss-rails gem serves Tailwind CSS differently. Let me fix this:

    ls -la app/assets/tailwind/
    
    curl -I http://localhost:3000/tailwind/application.css
    
    rails routes | grep -i tailwind
    

    The issue is that the /tailwind/application.css path is returning 404. The tailwindcss-rails gem works differently. Let’s check how it’s configured and fix this:

    find . -name "*.css" -path "*/public/*" | head -10
    
    rails assets:precompile
    

    Now let’s test if the Tailwind CSS is properly accessible:

    curl -I http://localhost:3000/assets/tailwind/application.css
    

    The issue is that with the tailwindcss-rails gem, Tailwind CSS gets compiled into the main application.css file rather than a separate file. Let’s check the current asset manifest and fix the layout:

    rails assets:precompile
    
    bundle exec tailwindcss build
    

    The Tailwind CSS is already built and located in app/assets/builds/tailwind.css. Let’s fix the configuration:

    rails assets:precompile
    
    curl -I http://localhost:3000/assets/tailwind.css
    

    The issue has been fixed. The Tailwind CSS is now properly accessible. Here’s what I did to resolve the problem:

    1. Identified the issue: The Tailwind CSS was being incorrectly referenced as /tailwind/application.css in the layout file, but it wasn’t being served properly through the asset pipeline.
    2. Found the correct location: Discovered that the tailwindcss-rails gem builds the CSS file to app/assets/builds/tailwind.css.
    3. Fixed the asset pipeline configuration:
    • Updated app/assets/config/manifest.js to include //= link_tree ../builds which includes the built Tailwind CSS
    • Removed the incorrect tailwind/application.css reference
    1. Updated the layout file:
    • Changed from <%= stylesheet_link_tag "/tailwind/application.css" %>
    • To the correct <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
    1. Verified the fix:
    • Successfully precompiled assets
    • Confirmed that http://localhost:3000/assets/tailwind.css returns a 200 OK status

    Install Rails Icons 🍁

    For better font-awesome assets, lets add rails-icons gem:

    # Rails Icons - Modern icon library support
    gem "rails_icons"
    
    $ rails generate rails_icons:install --help
    $ rails generate rails_icons:install --libraries=heroicons
    
    $ npm install @fortawesome/fontawesome-free
    

    How to migrate from the CDN to Rails Icons

    For a production Rails application, it’s generally better to use a gem rather than a CDN for Font Awesome. Here’s why:

    CDN Issues:

    • External dependency (can fail if CDN is down)
    • Privacy concerns (external requests)
    • No version control
    • Requires internet connection
    • Not cacheable with your assets

    Gem Benefits:

    • Self-hosted (no external dependencies)
    • Better performance (served with your assets)
    • Version control
    • Works offline
    • Follows Rails conventions
    • Better for production

    Based on my research, adding a gem is definitely the better approach for a production Rails application. Here are the best options:

    Option 1: Modern approach with npm package (Recommended)

    Since we’re using Rails 8 with modern tooling, I’d recommend using the npm package:

    yarn add @fortawesome/fontawesome-free
    
    npm --version
    
    which node
    

    However, given your current setup, I’d recommend Option 2 which is more Rails-native:

    Option 2: Rails Icons gem (Recommended for our setup)

    This is a modern gem that supports Font Awesome and other icon libraries with a clean Rails API:

    bundle install
    
    rails generate rails_icons:install --help
    
    rails generate rails_icons:install --libraries=heroicons
    

    Let’s remove the CDN and set up Rails Icons properly. Since we’re already using Font Awesome extensively, let’s add Font Awesome as a custom library:

    rails generate rails_icons:initializer --custom=fontawesome
    
    npm install @fortawesome/fontawesome-free
    
    find node_modules/@fortawesome/fontawesome-free -name "*.svg" -type f | head -5
    
    cp -r node_modules/@fortawesome/fontawesome-free/svgs/* app/assets/svg/icons/fontawesome/
    

    Rails Icons is now properly set up instead of the CDN. Here’s what we’ve accomplished:

    What’s Changed:

    1. Removed the Font Awesome CDN link (no more external dependency)
    2. Added rails_icons gem with Heroicons as the default library
    3. Added Font Awesome as a custom library with all variants (solid, regular, brands)
    4. Self-hosted all icons through Rails asset pipeline

    What is Ransack?

    Ransack is a popular Ruby gem that provides powerful search and filtering capabilities for Rails applications. It’s commonly used with ActiveAdmin (which your app uses) to create searchable, filterable, and sortable data tables.

    Ransackable Attributes and Associations

    ransackable_attributes

    This method defines which model attributes can be searched, filtered, or sorted through Ransack. It’s a security feature that prevents unauthorized access to sensitive data.

    def self.ransackable_attributes(auth_object = nil)
      %w[id email role created_at updated_at password_reset_token password_reset_sent_at]
    end
    

    What it does:

    • Allows searching/filtering by id, email, role, created_at, updated_at, etc.
    • Prevents searching by sensitive fields like password_digest
    • The auth_object parameter can be used for role-based access control

    ransackable_associations

    This method defines which model associations can be used in Ransack queries.

    def self.ransackable_associations(auth_object = nil)
      %w[orders sessions]
    end
    

    What it does:

    • Allows searching/filtering by related models (e.g., “users who have orders”)
    • Enables joins and complex queries across associations

    Why This Matters for Security

    Without these methods, Rails 7+ will raise warnings or errors because Ransack needs explicit permission to search certain fields. This prevents:

    1. Information disclosure – Searching sensitive fields like passwords
    2. Performance issues – Searching on unindexed or inappropriate fields
    3. Unauthorized access – Accessing data through unexpected query parameters

    Example Usage in ActiveAdmin

    In your ActiveAdmin dashboard, this enables features like:

    # In app/admin/users.rb
    ActiveAdmin.register User do
      # Users can now search by email, role, etc.
      # Users can filter by orders, sessions
      # Users can sort by created_at, updated_at
    end
    

    In Our User Model

    Looking at your User model:

    def self.ransackable_attributes(auth_object = nil)
      %w[id email role created_at updated_at password_reset_token password_reset_sent_at]
    end
    
    def self.ransackable_associations(auth_object = nil)
      %w[orders sessions]
    end
    

    This means:

    • ✅ Admins can search users by email, role, creation date
    • ✅ Admins can filter users by their orders or sessions
    • ✅ Admins can sort users by any of these attributes
    • ❌ Admins cannot search by password_digest (secure)
    • ❌ Admins cannot search by other sensitive fields

    Benefits

    1. Security: Explicit control over what can be searched
    2. Performance: Only searchable fields need indexes
    3. Usability: Provides powerful admin interface features
    4. Compliance: Helps meet data protection requirements

    Migration Guide:

    Old CDN way:

    <i class="fas fa-user"></i>          <!-- solid -->
    <i class="far fa-star"></i>          <!-- regular -->
    <i class="fab fa-facebook-f"></i>    <!-- brands -->
    

    New Rails Icons way:

    <%= icon "user", library: "fontawesome", variant: "solid", class: "size-6" %>
    <%= icon "star", library: "fontawesome", variant: "regular", class: "size-6" %>
    <%= icon "facebook-f", library: "fontawesome", variant: "brands", class: "size-6" %>
    

    Or use modern Heroicons (recommended for new icons):

    <%= icon "user", class: "size-6" %>
    <%= icon "star", class: "size-6" %>
    <%= icon "heart", class: "size-6" %>
    

    Benefits:

    • No external dependencies (works offline)
    • Better performance (served with your assets)
    • Version control over icons
    • Multiple libraries (Font Awesome + Heroicons + more)
    • Consistent API across all icon libraries
    • Better for production and privacy

    Our application should work exactly the same now, but with better performance and no external dependencies! You can gradually migrate icons from <i class="fas fa-x"> to <%= icon "x", library: "fontawesome", variant: "solid" %> as needed.

    Asset Issue Again

    ✅ Final Fix Applied:

    Added //= link_tree ../../javascript .js to the manifest, which tells Sprockets to include all JavaScript files from the app/javascript directory tree.

    ✅ What’s Working Now:

    1. All CSS files properly compiled:
    • tailwind.css ✅
    • application.css ✅
    • active_admin.css ✅
    1. All JavaScript files properly compiled:
    • application.js ✅
    • controllers/application.js ✅
    • controllers/decorative_background_controller.js ✅
    • controllers/hello_controller.js ✅
    • controllers/index.js ✅
    1. Asset Pipeline fully functional:
    • Sprockets serving all assets correctly
    • Proper cache headers and ETags
    • All asset links declared in manifest

    🎯 Current Status:

    Our Rails application now has:

    • ✅ Modern asset pipeline with Sprockets + Importmap
    • ✅ Tailwind CSS for styling (self-hosted)
    • ✅ ActiveAdmin for administration
    • ✅ Rails Icons instead of Font Awesome CDN
    • ✅ Stimulus controllers for JavaScript functionality
    • ✅ All assets properly precompiled and served

    Check Github: Active admin changes

    📋 Clean Commit History Summary:

    1. ✅ Phase 1: feat: Add Rails Icons gem with Font Awesome and Heroicons support #94
    • Replaced Font Awesome CDN with self-hosted icons
    • Added Rails Icons with unified API for multiple icon libraries
    1. ✅ Phase 2: task: Migrate from Propshaft to Sprockets asset pipeline #96
    • Switched from Propshaft to Sprockets for ActiveAdmin compatibility
    • Fixed asset compilation and linking issues
    1. ✅ Phase 3: feat: Integrate ActiveAdmin for comprehensive admin interface #94
    • Complete ActiveAdmin setup with authentication
    • Full admin resources for all e-commerce models
    1. ✅ Phase 4: fix: Resolve ActiveAdmin PostgreSQL and Ransack security issues #94
    • Fixed PostgreSQL GROUP BY errors in dashboard
    • Added Ransack security configuration for all models

    🚀 Our ActiveAdmin is now fully functional!

    You should now be able to:

    • ✅ Access the admin dashboard at localhost:3000/admin
    • ✅ View analytics and statistics without GROUP BY errors
    • ✅ Search and filter all resources safely with Ransack
    • ✅ Manage Users, Products, Variants, Orders, and Order Items
    • ✅ Use nested attributes for product variants
    • ✅ Perform batch operations and advanced filtering

    Test it out: Visit localhost:3000/admin and log in with your admin credentials to see the beautiful, fully-functional admin interface! 🎯

    to be continued 🚀…

    Rails 8 App: Create an Academic software app using SQL without using ActiveRecord- Part 3

    In this episode we move on from creating the tables with constraints, foreign keys, proper indexes, enums, reversing each migrations, seeded data etc. Now let’s check our seeded data with some ui tool that help us smell and modify our db data.

    Setup a UI tool for analysing SQL and data

    W’re using PostgreSQL. Here are the best SQL GUI tools for pure SQL data analysis and manipulation on macOS:

    🏆 Top Recommendations for SQL Analysis

    1. TablePlus ⭐️ (Highly Recommended)

    • Best for: Fast SQL queries, data filtering, before/after comparisons
    • Strengths:
      Lightning-fast query execution
      Excellent data filtering UI with SQL query builder
      Beautiful native macOS interface
      Export to CSV/JSON/SQL
      Query history and favorites
      Cost: Free tier (2 tabs), Pro $89
      Perfect for: Your use case of checking seeded data

    2. Postico (macOS Native)

    • Best for: PostgreSQL-specific features and analysis
    • Strengths:
      Built specifically for PostgreSQL
      Excellent for large dataset analysis
      Advanced filtering and sorting
      Beautiful data visualization
      Cost: $49 (one-time)
      PostgreSQL-optimized: Uses all PostgreSQL features

    3. pgAdmin (Free, Official)

    • Best for: Advanced PostgreSQL administration and complex queries
    • Strengths:
      Official PostgreSQL tool
      Advanced query planner visualization
      Excellent for performance analysis
      Complete database management
      Cost: Free
      Learning curve: Steeper but very powerful

    4. DBeaver (Free, Cross-platform)

    • Best for: Advanced SQL analysis and scripting
    • Strengths:
      Powerful SQL editor with autocomplete
      Data export in multiple formats
      Query execution plans
      Visual query builder
      Cost: Free (Community), Pro $10/month
      Great for: Complex data analysis workflows

    Quick Setup Commands

    For TablePlus (easiest to get started):

    # Install via Homebrew
    brew install --cask tableplus
    

    For pgAdmin:

    # Install via Homebrew
    brew install --cask pgadmin4
    

    For Postico:

    # Install via Homebrew
    brew install --cask postico
    

    🔧 Connection Details You’ll Need

    Your PostgreSQL connection details:

    • Host: localhost (default)
    • Port: 5432 (default)
    • Database: academic_sql_software_development
    • Username: Your macOS username (default)
    • Password: None (default for local development)

    💡 Pro Tips for Data Analysis

    Once connected, you can:

    1. Check seeded data:
       SELECT COUNT(*) FROM users;
       SELECT COUNT(*) FROM orders;
       SELECT COUNT(*) FROM products;
    
    1. Analyze relationships:
       SELECT 
         u.first_name, u.last_name, 
         COUNT(o.id) as order_count
       FROM users u 
       LEFT JOIN orders o ON u.id = o.user_id 
       GROUP BY u.id, u.first_name, u.last_name
       ORDER BY order_count DESC;
    
    1. Filter and export specific datasets for before/after comparisons

    My Recommendation: Start with TablePlus – it’s the most intuitive for our workflow of checking and filtering seeded data, with excellent performance for the data volumes we’re working with (10k users, 5k orders, etc.).

    Let’s Go with TablePlus 🥳

    🚀 Step 1: Install TablePlus

    brew install --cask tableplus
    

    📊 Step 2: Check Our Database Schema

     We have a great setup for learning SQL with realistic relationships. Let’s create a progressive SQL learning path using our actual data.

    🔗 Step 3: Connect to Your Database

    TablePlus Connection Details:

    • Host: localhost
    • Port: 5432
    • Database: academic_sql_software_development
    • User: (your macOS username)
    • Password: (leave blank)

    📚 SQL Learning Path: Basic to Advanced

    Change Font size, colour, theme etc:

    Level 1: Basic SELECT Queries

    -- 1. View all users
    SELECT * FROM users LIMIT 10;
    
    -- 2. Count total records
    SELECT COUNT(*) FROM users;
    SELECT COUNT(*) FROM orders;
    SELECT COUNT(*) FROM products;
    
    -- 3. Filter data
    SELECT first_name, last_name, email 
    FROM users 
    WHERE gender = 'female' 
    LIMIT 10;
    
    -- 4. Sort data
    SELECT first_name, last_name, date_of_birth 
    FROM users 
    ORDER BY date_of_birth DESC 
    LIMIT 10;
    
    -- 5. Filter with conditions
    SELECT title, price, category 
    FROM products 
    WHERE price > 50 AND category = 'men' 
    ORDER BY price DESC;
    

    Level 2: Basic Aggregations

    -- 1. Count by category
    SELECT category, COUNT(*) as product_count 
    FROM products 
    GROUP BY category;
    
    -- 2. Average prices by category
    SELECT category, 
           AVG(price) as avg_price,
           MIN(price) as min_price,
           MAX(price) as max_price
    FROM products 
    GROUP BY category;
    
    -- 3. Users by gender
    SELECT gender, COUNT(*) as user_count 
    FROM users 
    WHERE gender IS NOT NULL
    GROUP BY gender;
    
    -- 4. Products with low stock
    SELECT COUNT(*) as low_stock_products 
    FROM products 
    WHERE stock_quantity < 10;
    

    Level 3: Inner Joins

    -- 1. Users with their orders
    SELECT u.first_name, u.last_name, u.email, o.id as order_id, o.created_at
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id
    ORDER BY o.created_at DESC
    LIMIT 20;
    
    -- 2. Orders with product details
    SELECT o.id as order_id, 
           p.title as product_name, 
           p.price, 
           p.category,
           o.created_at
    FROM orders o
    INNER JOIN products p ON o.product_id = p.id
    ORDER BY o.created_at DESC
    LIMIT 20;
    
    -- 3. Complete order information (3-table join)
    SELECT u.first_name, u.last_name,
           p.title as product_name,
           p.price,
           p.category,
           o.created_at as order_date
    FROM orders o
    INNER JOIN users u ON o.user_id = u.id
    INNER JOIN products p ON o.product_id = p.id
    ORDER BY o.created_at DESC
    LIMIT 20;
    

    Level 4: Left Joins (Show Missing Data)

    -- 1. All users and their order count (including users with no orders)
    SELECT u.first_name, u.last_name, u.email,
           COUNT(o.id) as order_count
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    GROUP BY u.id, u.first_name, u.last_name, u.email
    ORDER BY order_count DESC;
    
    -- 2. Users who haven't placed any orders
    SELECT u.first_name, u.last_name, u.email, u.created_at
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE o.id IS NULL
    ORDER BY u.created_at DESC;
    
    -- 3. Products that have never been ordered
    SELECT p.title, p.price, p.category, p.stock_quantity
    FROM products p
    LEFT JOIN orders o ON p.id = o.product_id
    WHERE o.id IS NULL
    ORDER BY p.price DESC;
    

    Level 5: Advanced Aggregations & Grouping

    -- 1. Top customers by order count
    SELECT u.first_name, u.last_name,
           COUNT(o.id) as total_orders,
           SUM(p.price) as total_spent
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id
    INNER JOIN products p ON o.product_id = p.id
    GROUP BY u.id, u.first_name, u.last_name
    HAVING COUNT(o.id) > 1
    ORDER BY total_spent DESC
    LIMIT 10;
    
    -- 2. Most popular products
    SELECT p.title, p.category, p.price,
           COUNT(o.id) as times_ordered,
           SUM(p.price) as total_revenue
    FROM products p
    INNER JOIN orders o ON p.id = o.product_id
    GROUP BY p.id, p.title, p.category, p.price
    ORDER BY times_ordered DESC
    LIMIT 10;
    
    -- 3. Monthly order analysis
    SELECT DATE_TRUNC('month', o.created_at) as month,
           COUNT(o.id) as order_count,
           COUNT(DISTINCT o.user_id) as unique_customers,
           SUM(p.price) as total_revenue
    FROM orders o
    INNER JOIN products p ON o.product_id = p.id
    GROUP BY DATE_TRUNC('month', o.created_at)
    ORDER BY month;
    

    Level 6: Student Enrollment Analysis (Complex Joins)

    -- 1. Students with their course and school info
    SELECT u.first_name, u.last_name,
           c.title as course_name,
           s.title as school_name,
           st.enrolment_date
    FROM students st
    INNER JOIN users u ON st.user_id = u.id
    INNER JOIN courses c ON st.course_id = c.id
    INNER JOIN schools s ON st.school_id = s.id
    ORDER BY st.enrolment_date DESC
    LIMIT 20;
    
    -- 2. Course popularity by school
    SELECT s.title as school_name,
           c.title as course_name,
           COUNT(st.id) as student_count
    FROM students st
    INNER JOIN courses c ON st.course_id = c.id
    INNER JOIN schools s ON st.school_id = s.id
    GROUP BY s.id, s.title, c.id, c.title
    ORDER BY student_count DESC;
    
    -- 3. Schools with enrollment stats
    SELECT s.title as school_name,
           COUNT(st.id) as total_students,
           COUNT(DISTINCT st.course_id) as courses_offered,
           MIN(st.enrolment_date) as first_enrollment,
           MAX(st.enrolment_date) as latest_enrollment
    FROM schools s
    LEFT JOIN students st ON s.id = st.school_id
    GROUP BY s.id, s.title
    ORDER BY total_students DESC;
    

    Level 7: Advanced Concepts

    -- 1. Subqueries: Users who spent more than average
    WITH user_spending AS (
      SELECT u.id, u.first_name, u.last_name,
             SUM(p.price) as total_spent
      FROM users u
      INNER JOIN orders o ON u.id = o.user_id
      INNER JOIN products p ON o.product_id = p.id
      GROUP BY u.id, u.first_name, u.last_name
    )
    SELECT first_name, last_name, total_spent
    FROM user_spending
    WHERE total_spent > (SELECT AVG(total_spent) FROM user_spending)
    ORDER BY total_spent DESC;
    
    -- 2. Window functions: Ranking customers
    SELECT u.first_name, u.last_name,
           COUNT(o.id) as order_count,
           SUM(p.price) as total_spent,
           RANK() OVER (ORDER BY SUM(p.price) DESC) as spending_rank
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id
    INNER JOIN products p ON o.product_id = p.id
    GROUP BY u.id, u.first_name, u.last_name
    ORDER BY spending_rank
    LIMIT 20;
    
    -- 3. Case statements for categorization
    SELECT u.first_name, u.last_name,
           COUNT(o.id) as order_count,
           CASE 
             WHEN COUNT(o.id) >= 5 THEN 'VIP Customer'
             WHEN COUNT(o.id) >= 2 THEN 'Regular Customer'
             ELSE 'New Customer'
           END as customer_type
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    GROUP BY u.id, u.first_name, u.last_name
    ORDER BY order_count DESC;
    

    Level 8: Self-Joins & Advanced Analysis

    -- 1. Find users enrolled in the same course (pseudo self-join)
    SELECT DISTINCT 
           u1.first_name || ' ' || u1.last_name as student1,
           u2.first_name || ' ' || u2.last_name as student2,
           c.title as course_name
    FROM students s1
    INNER JOIN students s2 ON s1.course_id = s2.course_id AND s1.user_id < s2.user_id
    INNER JOIN users u1 ON s1.user_id = u1.id
    INNER JOIN users u2 ON s2.user_id = u2.id
    INNER JOIN courses c ON s1.course_id = c.id
    ORDER BY c.title, student1
    LIMIT 20;
    
    -- 2. Complex business question: Multi-role users
    SELECT u.first_name, u.last_name, u.email,
           COUNT(DISTINCT o.id) as orders_placed,
           COUNT(DISTINCT st.id) as courses_enrolled,
           CASE 
             WHEN COUNT(DISTINCT o.id) > 0 AND COUNT(DISTINCT st.id) > 0 THEN 'Customer & Student'
             WHEN COUNT(DISTINCT o.id) > 0 THEN 'Customer Only'
             WHEN COUNT(DISTINCT st.id) > 0 THEN 'Student Only'
             ELSE 'No Activity'
           END as user_type
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    LEFT JOIN students st ON u.id = st.user_id
    GROUP BY u.id, u.first_name, u.last_name, u.email
    ORDER BY orders_placed DESC, courses_enrolled DESC;
    

    🎯 Our Learning Strategy:

    1. Start with Level 1-2 in TablePlus to get comfortable
    2. Progress through each level – try to understand each query before moving on
    3. Modify the queries – change filters, add fields, etc.
    4. Create your own variations based on business questions

    to be continued … 🚀