Rails App Audit
Overview
A top-level health-check for an existing Rails app — inheriting a legacy codebase, onboarding to an unfamiliar project, or doing a pre-engagement review. This skill is the entry point for reviewing an app, the counterpart to [[rails-core]] which is the entry point for writing one.
It orchestrates: it owns the broad health-check items nothing else covers (version pinning, dependency CVEs, exposed secrets, seeds, tech-debt) and hands off the deep dives to the specialist skills ([[rails-database-performance]], [[rails-security]], [[rails-performance]], [[rails-testing]], [[rails-upgrade]]). Do not re-derive what those skills already do — run the cheap detection here, then delegate the fix.
The audit ends in a written, severity-ranked report (see Producing the Report).
How to Run This Audit
- Confirm you are at the app root (
Gemfile,app/,config/present). - Work through every numbered section below — do not stop early.
- For each finding, record three things:
- Severity — 🔴 high (security / data loss / broken in prod), 🟡 medium (tech debt, performance, missing safety net), 🟢 low (polish, style, docs).
- Location —
file:line(the audit must point at real code, not generalities). - Fix — the concrete remediation, and which skill owns the deep fix if it's a delegated area.
- When a section hands off to another skill (shown as "→ [[skill-name]]"), run only the quick detection here and note the handoff; the named skill carries the authoritative checklist.
1. Ruby & Rails Version Pinning
A shared, explicit version is the baseline for reproducible builds. Check the version is pinned and still supported.
cat .ruby-version 2>/dev/null || echo "MISSING .ruby-version"
grep -nE '^\s*ruby\s+["''']' Gemfile
grep -nE '^\s*gem\s+["''']rails["''']' Gemfile
ruby -v; bin/rails -v 2>/dev/null
- No
.ruby-version→ 🟡 add one so every dev/CI uses the same interpreter. - Rails not locked to a specific version (
gem "rails"with no version, or a loose>=) → 🟡 abundle updatecan silently jump majors. Lock it. - EOL Ruby or Rails → 🔴/🟡. Check the running version against the support schedule. An upgrade is its own project — hand off to [[rails-upgrade]] (do not hand-roll the version bump).
2. Gemfile Hygiene
head -1 Gemfile # source must be HTTPS
grep -nE 'cooldown' Gemfile .bundle/config 2>/dev/null # supply-chain cooldown set?
grep -nE 'group\s+:' Gemfile # dev/test gems grouped out of production?
source 'http://...'(not HTTPS) → 🔴.- No supply-chain cooldown → 🟡. Recommend
source "https://rubygems.org", cooldown: 4so freshly-hijacked gem releases can't resolve for 4 days. Rationale and scope rules: [[rails-project-setup]]. - Dev/test gems not grouped → 🟡. Wrap development- and test-only gems (rspec-rails, capybara, debug, factory_bot, brakeman, rubocop) in
group :development, :test do … end, so a production deploy run withBUNDLE_WITHOUT=development:test(orbundle install --without development test) neither installs nor loads them: smaller image, faster boot, smaller attack surface. - Undocumented/obscure gems → 🟢 add a one-line comment for the next maintainer.
Don't flag missing per-gem version pins. With a committed
Gemfile.lock(andbundle install --deployment/BUNDLE_FROZEN=trueon deploy), every version is already frozen; loose constraints in theGemfileare fine.
3. Dependency Vulnerabilities & Static Security Scan
Two free, fast scanners. Run both.
gem install bundler-audit 2>/dev/null; bundle exec bundler-audit check --update 2>/dev/null || bundler-audit check --update
gem install brakeman 2>/dev/null; bundle exec brakeman -q -A 2>/dev/null || brakeman -q -A
bundler-auditflagsGemfile.lockgems with known CVEs and the patched version → 🔴/🟡 by criticality. Record each advisory + the upgrade target.brakemanflags code-level issues (SQLi, mass-assignment, unsafe redirects, XSS). Triage each warning; confidenceHighfirst → 🔴.- For the design of auth/authorization behind these findings, hand off to [[rails-security]].
4. Exposed Secrets
Secrets in the repo (or in git history) are the highest-impact, easiest-to-miss finding.
grep -rinE '(password|secret|api[_-]?key|access[_-]?key|token)\s*[:=]\s*["'''][^"''' ]{6,}' config/ app/ lib/ 2>/dev/null
git log --oneline -- config/master.key config/credentials.yml.enc 2>/dev/null
grep -nE 'config/master.key|config/credentials.*\.key|\.env' .gitignore 2>/dev/null
- Literal credentials in
config/environments/production.rb, initializers, or*.yml→ 🔴. Move to encrypted credentials (bin/rails credentials:edit) or ENV. config/master.keyor any.envNOT gitignored → 🔴. Modern Rails stores secrets inconfig/credentials.yml.enc(encrypted, committable) decrypted bymaster.key(which must stay out of git).- A secret already committed → 🔴. Rotate it first (assume it's compromised), then purge history. See [[rails-security]] for what to rotate and how to scope it.
5. Test Health
A suite that nobody has touched in a year is a liability, not a safety net.
git log -1 --pretty=format:'%ci %s' -- test/ spec/ 2>/dev/null; echo
ls -d test spec 2>/dev/null
grep -c . <(find app -name '*.rb') ; find test spec -name '*_test.rb' -o -name '*_spec.rb' 2>/dev/null | wc -l
- Tests last touched long before app code → 🟡 likely abandoned after an upgrade broke them, or never written. Note the staleness.
- No
test//spec/at all, or near-empty → 🔴 no regression safety net. - For coverage strategy, fixtures vs factories, system tests, and parallelization → hand off to [[rails-testing]]. To quantify, run the suite and a coverage tool (SimpleCov) and report the percentage.
6. Seed Data
Developers must be able to stand up a working local DB without a production dump.
test -s db/seeds.rb && echo "seeds.rb present ($(wc -l < db/seeds.rb) lines)" || echo "MISSING/empty db/seeds.rb"
grep -nE 'find_or_create_by|destroy_all|delete_all' db/seeds.rb 2>/dev/null
- Missing or empty
db/seeds.rb→ 🟡 onboarding requires copying prod data (privacy + size risk). - Seeds that aren't idempotent (plain
create!that dupes on re-run) → 🟢 preferfind_or_create_byso seeds can be loaded, cleared, and re-loaded cleanly.
7. Lint & Style Drift
bundle exec rubocop --format offenses 2>/dev/null | tail -20 || bundle exec standardrb 2>/dev/null | tail -20
ls .rubocop.yml .standard.yml 2>/dev/null
- No linter configured → 🟢 add
rubocop-railsorstandardfor consistent style. - Configured but thousands of offenses → 🟡 style has drifted; consider
rubocop --auto-gen-configto baseline, then fix incrementally. - For the project's actual conventions (method ordering, conditionals, REST routing, naming) → [[rails-style]].
8. Schema & Index Quick Heuristic
A fast smell test before the full schema audit. Foreign keys without indexes are the most common Rails performance bug.
echo "FK-ish columns:"; grep -E '_id' db/schema.rb | grep -v 'add_index\|t\.index' | wc -l
echo "indexes:"; grep -E 'add_index|t\.index' db/schema.rb | wc -l
- If those two numbers are drastically different, indexes have likely been overlooked → 🟡.
- This is only a heuristic. The authoritative index/query/N+1-at-the-DB audit (polymorphic, scope, status, auth, counter-cache, pagination) lives in [[rails-database-performance]] — run it next.
9. Performance & N+1 Quick Scan
grep -rnE '\.(each|map)\b' app/views/ app/controllers/ 2>/dev/null | head -20 # loops that may trigger per-row queries
grep -rnE '\.includes\(|\.preload\(|\.eager_load\(' app/ 2>/dev/null | wc -l # is eager loading used at all?
- Views/controllers iterating an association with no matching
includes/preload→ 🟡 probable N+1. Spot-check the dev log (or addprosopite— near-zero false positives, unlikebullet) to confirm. - For caching, ETags, batching, and the full N+1 treatment → [[rails-performance]].
Production footguns — cheap greps for configurations that cause outages, not just slowness:
grep -nE 'puma_worker_killer|unicorn-worker-killer' Gemfile # memory-threshold worker killers
grep -rnE 'send_data|send_file' app/controllers/ | head # large files served from the web process
grep -rnE 'queue_adapter\s*=\s*:inline|Resque\.inline' config/ | grep -v development # inline jobs outside dev
grep -rnE 'timeout' config/initializers/*elastic* config/initializers/*search* config/initializers/*redis* 2>/dev/null # client timeouts set at all?
- Worker-killer gem present → 🟡 hidden ENV thresholds silently turn the app into a restart loop (processes recycled after a handful of requests); removal has halved response times in practice. Modern instances have the RAM — fix leaks instead. → [[rails-performance]]
send_dataof generated content (CSV/zip/PDF) → 🟡 memory bloat + blocked threads; move generation to a job, serve from storage. → [[rails-jobs]]:inlinejob adapter in test/production config → 🟡 → [[rails-testing]]- No timeout configured on search/cache clients → 🟡 default 30s timeouts let a degraded Elasticsearch/Redis hold every Puma thread and take the app down. → [[rails-performance]]
10. Architecture Smells
Cheap size heuristics surface the files most likely to hide problems.
echo "Fattest models:"; wc -l app/models/**/*.rb 2>/dev/null | sort -rn | head -10
echo "Fattest controllers:"; wc -l app/controllers/**/*.rb 2>/dev/null | sort -rn | head -10
echo "Service-object sprawl:"; ls app/services 2>/dev/null | wc -l
grep -rnE '^\s*def ' app/controllers/ 2>/dev/null | grep -vE 'index|show|new|create|edit|update|destroy' | head # non-REST actions
echo "ApplicationController methods:"; grep -cE '^\s*def ' app/controllers/application_controller.rb 2>/dev/null # dumping-ground smell
- Controllers fat with business logic / many non-REST actions → 🟡 push behaviour into models, extract concerns. See [[rails-controllers]] and [[rails-style]] (REST routing).
ApplicationControlleris a dumping ground (many methods — say 8+ — especially unrelated ones not backing abefore_action, with no grouping) → 🟡. It accretes current-user checks, default redirects, env (production?/staging?) checks,www-canonicalisation, response-header setters, etc., that nobody dares move. Group related methods under comments, push any method used by only 1–2 controllers into a concernincluded just there, and extract each cluster of 2+ related methods into a named concern (e.g.include Akamai). The base controller should read as a manifest ofincludes. → [[rails-controllers]] (Pattern 1).- God models (many hundreds of lines) → 🟡 extract concerns / POROs. See [[rails-models]].
- A large
app/services/tree → 🟡 in 37signals-style Rails this is usually logic that belongs on rich models, not a service layer. Weigh against [[rails-philosophy]] before recommending more of it.
11. Technical-Debt Inventory
grep -rinE 'todo|fixme|hack|xxx|deprecated' app/ lib/ 2>/dev/null | wc -l
grep -rinE 'todo|fixme|hack|xxx' app/ lib/ 2>/dev/null | head -40
grep -rnE '^\s*#\s*[a-z].*\b(def|end|do|=)\b' app/ lib/ 2>/dev/null | head # commented-out code
- Inventory every
TODO/FIXME/HACK→ 🟢/🟡. Decide per item: do it, ticket it, or delete the stale note. - Large blocks of commented-out code → 🟢 delete; git is the history.
Producing the Report
End the audit with a written report, grouped by severity (🔴 → 🟡 → 🟢). Each item:
### 🔴 <short title>
- **Where:** path/to/file.rb:42 (or: Gemfile, db/schema.rb)
- **Finding:** what's wrong, with the evidence (the grep hit / scanner line).
- **Impact:** why it matters (security / perf / maintainability / onboarding).
- **Fix:** the concrete change, and **→ which skill** to use for the deep fix.
Close with a one-paragraph summary: overall health, the top 3 things to fix first, and the recommended order (security & exposed secrets before refactors).
Quick Reference
| # | Check | Command / signal | Deep-dive skill |
|---|---|---|---|
| 1 | Version pinning | .ruby-version, locked gem "rails", EOL? |
[[rails-upgrade]] |
| 2 | Gemfile hygiene | HTTPS source, cooldown, dev/test groups | — |
| 3 | Vulnerabilities | bundler-audit, brakeman |
[[rails-security]] |
| 4 | Exposed secrets | grep config/, master.key gitignored |
[[rails-security]] |
| 5 | Test health | git log -1 -- test/ spec/, coverage |
[[rails-testing]] |
| 6 | Seed data | db/seeds.rb present & idempotent |
— |
| 7 | Lint/style | rubocop / standard offenses |
[[rails-style]] |
| 8 | Schema/index | FK-count vs index-count | [[rails-database-performance]] |
| 9 | N+1/perf | loops without includes |
[[rails-performance]] |
| 10 | Architecture | fat models/controllers, service sprawl | [[rails-philosophy]], [[rails-models]], [[rails-controllers]] |
| 11 | Tech debt | TODO/FIXME, dead code |
— |
Common Mistakes
- Reporting from memory instead of running the commands. Every finding must cite a real grep hit / scanner line /
file:line. No evidence, no finding. - Re-deriving a deep-dive skill inline. Sections 8–10 are detection heuristics only; the authoritative checklist lives in the linked skill. Delegate, don't duplicate.
- Findings without severity or location. A report the user can't triage or act on is noise.
- Burying the lede. Lead with 🔴 security/secrets; refactors and style come last.
- Treating an EOL version as a quick fix. Version upgrades are a project — route to [[rails-upgrade]], don't bump in place.