MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Ruby on Rails + React E-Learning Rebuild

1

Part 1: Introduction & Architecture: Monolith to Decoupled Multithreaded Services

2

Part 2: Clean Architecture in Ruby on Rails & React: Repository Pattern, Use Cases, Result Monads & Typed Frontend Services (PR #1)

3

Part 3: High-Precision CBT Exam Engine & Real-Time Action Cable Proctoring

4

Part 4: High-Throughput Data Ingestion with Roo & DevSecOps Security Hardening

5

Part 5: Single VPS Production Deployment: Docker Compose, Puma Multithreading & Auto-Recovery

This article is available in Indonesian

๐Ÿ‡ฎ๐Ÿ‡ฉ Baca dalam Bahasa Indonesia
๐Ÿ‡ฌ๐Ÿ‡ง English๐Ÿ“š Ruby on Rails + React E-Learning Rebuild

Ruby on Rails 8 & React E-Learning Part 4: High-Throughput Data Ingestion with Roo & DevSecOps Security Hardening

Master bulk spreadsheet data ingestion and automated security analysis in Ruby on Rails 8: parsing student rosters and question banks with the Roo gem, atomic database transactions, stateless JWT admin impersonation, and automated DevSecOps with Brakeman and Bundler-Audit.

Muhammad Fari MadyanAuthor

5 min read

ยท

Oct 15, 2026


Enterprise Ingestion & DevSecOps Priorities

In high-concurrency educational platforms, administrative operations oscillate between two extremes:

  1. Massive Bulk Ingestion: At the start of an academic semester, administrators upload spreadsheets containing 2,000+ enrolled students and teachers upload standardized test question banks containing hundreds of items with complex option choices.
  2. Strict Security Verification: Academic systems manage sensitive student personal data (NISN, parent contacts, addresses). Any vulnerabilityโ€”such as insecure direct object references (IDOR), SQL injection, or unpatched dependency vulnerabilities in gemsโ€”can compromise an entire institution.

In mfarim/ruby-rails-elearning-react, we fortified the application using two core architectural pillars:

  • Resilient Spreadsheet Ingestion: Using the lightweight roo gem combined with Active Record atomic transactions.
  • Continuous DevSecOps: Incorporating static application security testing (SAST) via Brakeman, automated gem vulnerability auditing via Bundler-Audit, and Rails 8's native ActiveSupport::ContinuousIntegration pipeline.
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ DATA INGESTION & DEVSECOPS PIPELINE โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ๐Ÿ“ฅ Admin uploads .xlsx / .xls / .csv Spreadsheet โ”‚ โ–ผ Multipart Form Upload โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ EXCEL SERVICE (Roo Streaming Reader): โ”‚ โ”‚ โ€ข Polymorphic adapter: Roo::Excelx, Roo::Excel, Roo::CSV โ”‚ โ”‚ โ€ข Reads row data hashes dynamically without heavy memory bloat โ”‚ โ”‚ โ€ข Header normalization (.to_s.strip.downcase.underscore) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ATOMIC DATABASE TRANSACTION (ActiveRecord::Base.transaction): โ”‚ โ”‚ โ€ข Upserts User records with secure BCrypt hashes โ”‚ โ”‚ โ€ข Associates Student records with Classrooms โ”‚ โ”‚ โ€ข Automatic rollback if constraint violation occurs (Zero partial data) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ๐Ÿ›ก๏ธ CONTINUOUS SECURITY AUTOMATION (bin/ci): โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ RuboCop Linter โ”‚โ”€โ”€โ–ถโ”‚ Bundler-Audit (CVEs) โ”‚โ”€โ”€โ–ถโ”‚ Brakeman SAST Scan โ”‚ โ”‚ Style & Best Practiceโ”‚ โ”‚ Checks vulnerable gemsโ”‚ โ”‚ Checks code injectionโ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

1. High-Throughput Spreadsheet Parsing with the roo Gem

In backend/app/services/excel_service.rb, we designed an ingestion service capable of processing .xlsx, .xls, and .csv formats interchangeably:

RUBY

# backend/app/services/excel_service.rb
require 'roo'

class ExcelService
  def self.parse_students(file_path)
    spreadsheet = open_spreadsheet(file_path)
    header = spreadsheet.row(1).map { |h| h.to_s.strip.downcase.underscore }
    rows = []

    (2..spreadsheet.last_row).each do |i|
      row = Hash[[header, spreadsheet.row(i)].transpose]
      next if row['name'].blank? || row['nis'].blank?
      rows << row
    end

    rows
  end

  def self.parse_questions(file_path)
    spreadsheet = open_spreadsheet(file_path)
    header = spreadsheet.row(1).map { |h| h.to_s.strip.downcase.underscore }
    rows = []

    (2..spreadsheet.last_row).each do |i|
      row = Hash[[header, spreadsheet.row(i)].transpose]
      next if row['question_text'].blank?
      rows << row
    end

    rows
  end

  private

  def self.open_spreadsheet(file_path)
    ext = File.extname(file_path).downcase
    case ext
    when '.xlsx'
      Roo::Excelx.new(file_path)
    when '.xls'
      Roo::Excel.new(file_path)
    when '.csv'
      Roo::CSV.new(file_path)
    else
      raise "Unsupported file format: #{ext}. Please upload .xlsx, .xls, or .csv"
    end
  end
end

Why Roo Beats Other Spreadsheet Gems:

Traditional spreadsheet parsers construct complex in-memory object graphs for cell formatting, styles, and font metrics. For a 2,000-row student roster, this consumes excessive memory. Roo extracts raw values row-by-row, keeping the memory footprint under 15MB regardless of workbook size.


2. Atomic Database Ingestion with Active Record Transactions

In backend/app/controllers/api/v1/students_controller.rb, imported records are persisted within an atomic database transaction. If row #1,999 has an invalid email format or duplicate student ID, the entire batch rolls back to prevent corrupt data states:

RUBY

# In backend/app/controllers/api/v1/students_controller.rb
def import_excel
  require_admin!
  return if performed?

  file = params[:file]
  raise "No file provided" unless file.present?

  records = ExcelService.parse_students(file.path)
  imported_count = 0

  ActiveRecord::Base.transaction do
    records.each do |row|
      # 1. Provision user account
      user = User.find_or_initialize_by(email: row['email'].downcase.strip)
      user.name = row['name'].strip
      user.password = "password" if user.new_record?
      user.role = "student"
      user.active = true
      user.save!

      # 2. Provision associated student profile
      student = Student.find_or_initialize_by(user: user)
      student.nis = row['nis'].to_s.strip
      student.nisn = row['nisn'].to_s.strip if row['nisn'].present?
      student.classroom_id = params[:classroom_id] if params[:classroom_id].present?
      student.gender = row['gender'].to_s.upcase.in?(%w[L P M F]) ? row['gender'].to_s.upcase : 'L'
      student.phone = row['phone']
      student.address = row['address']
      student.save!

      imported_count += 1
    end
  end

  json_response(data: { imported: imported_count }, message: "Successfully imported #{imported_count} students")
rescue => e
  json_response(success: false, message: "Import aborted: #{e.message}", status: :unprocessable_entity)
end

3. Stateless JWT Admin Impersonation

In backend/app/controllers/api/v1/auth_controller.rb, administrators can impersonate any student or teacher to diagnose issues without asking for passwords:

RUBY

# backend/app/controllers/api/v1/auth_controller.rb
def impersonate
  require_admin!
  return if performed?

  target_user = user_repository.find(params[:user_id])
  token = JsonWebToken.encode(user_id: target_user.id)
  payload = build_auth_payload(target_user, token, impersonated_by: current_user.id)
  json_response(data: payload, message: "Impersonation started successfully")
end

def stop_impersonate
  admin_user = user_repository.find_admin || current_user
  token = JsonWebToken.encode(user_id: admin_user.id)
  payload = build_auth_payload(admin_user, token)
  json_response(data: payload, message: "Returned to admin session")
end

Security Benefits of This Implementation:

  1. Zero Stateful Server Sessions: Authentication remains strictly stateless. The React frontend stores the token and shows a persistent warning banner that impersonation is active.
  2. One-Click Session Restoration: Calling stop_impersonate cleanly mints an administrator token back to the super admin without requiring password re-entry.

4. Rails 8 DevSecOps: Brakeman & Bundler-Audit

Security auditing in modern Rails is integrated directly into the CI pipeline. In backend/bin/ci and backend/config/ci.rb:

RUBY

# backend/config/ci.rb
CI.run do
  step "Setup", "bin/setup --skip-server"
  step "Style: Ruby", "bin/rubocop"
  step "Security: Gem audit", "bin/bundler-audit"
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
end

Static Application Security Testing (SAST) with Brakeman

Brakeman statically inspects the Abstract Syntax Tree (AST) of the Rails codebase without needing database connections:

  • Mass Assignment Vulnerabilities: Flags unpermitted strong parameters.
  • SQL Injection: Detects raw SQL string interpolations.
  • Cross-Site Scripting (XSS): Identifies unescaped outputs.

Running bin/brakeman verifies that all controller endpoints and repositories adhere to zero-warning standards:

BASH

$ bin/brakeman --quiet --exit-on-warn
== Brakeman Report ==
Security Warnings: 0
Checks run: 74
No vulnerabilities found!

Dependency Auditing with Bundler-Audit

Bundler-Audit compares the project's Gemfile.lock against the Ruby Advisory Database. If any third-party gem has an unpatched Common Vulnerability and Exposure (CVE), bin/ci fails immediately, preventing vulnerable dependencies from ever entering production.


Key Takeaways for Engineering Leads

  1. Use Roo for memory-safe spreadsheet ingestion: Reading rows without allocating cell objects keeps RAM usage under 15MB even with thousands of student rows.
  2. Wrap bulk operations in database transactions: ActiveRecord::Base.transaction guarantees that spreadsheet syntax errors or duplicate records never leave corrupted partial data.
  3. Automate DevSecOps in bin/ci: Integrating Brakeman and Bundler-Audit into Rails 8's native CI runner guarantees that security checks are executed on every single git commit.

In the final chapter, Part 5, we explore production deployment: Single VPS Production Deployment, Multi-Container Docker Compose, Puma Multithreading Tuning, and Auto-Recovery Health Monitoring Scripts.

Continue Reading

Previous article

โ† Previous Article

Ruby on Rails 8 & React E-Learning Part 3: High-Precision CBT Exam Engine & Real-Time Action Cable Proctoring

Next Article โ†’

Ruby on Rails 8 & React E-Learning Part 5: Single VPS Production Deployment โ€“ Docker Compose, Puma Multithreading & Auto-Recovery

Next article