This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CASA is a Rails app used by CASA (Court Appointed Special Advocate) chapters to track volunteer work with foster youth. The domain shape: a CasaCase (a youth) is assigned to a Volunteer, who logs CaseContacts; Supervisors oversee volunteers; CasaAdmins run the chapter. The app is multi-tenant — every record belongs to a CasaOrg, and cross-org data leaks are the most important class of bug to avoid.
| Task | Command |
|---|---|
| One-time setup | bin/setup |
| Run app (web + JS/CSS watchers) | bin/dev then visit http://localhost:3000 |
| Run full RSpec suite | bin/rails spec |
| Run a single spec file | bundle exec rspec spec/path/to/file_spec.rb |
| Run a single example | bundle exec rspec spec/path/to/file_spec.rb:LINE |
| Run JS tests | npm run test |
| Run all linters w/ autofix | bin/lint (standardrb + erb_lint + standardjs + factory_bot:lint) |
| Update env after pulling main | bin/update (migrate, bundle, npm, after_party) |
| Run post-deploy tasks | bundle exec rake after_party:run |
| Local mailer previews | http://localhost:3000/rails/mailers |
Seed login credentials (password 12345678 for all):
volunteer1@example.com,supervisor1@example.com,casa_admin1@example.comat/users/sign_inallcasaadmin@example.comat/all_casa_admins/sign_in
Rails 7.2 on Ruby (.ruby-version), PostgreSQL, Hotwire (Turbo + Stimulus), Bootstrap 5, ESBuild, Devise + Devise-Invitable, Pundit, Draper, ViewComponent, Delayed Job, Flipper, Paranoia (soft deletes), Strong Migrations. Linting: Standard.rb + standard-rails, StandardJS, erb-lint. Testing: RSpec + Capybara (system tests via Selenium/Chrome), Jest. There is no UI sign-up — users are admin-invited only (ADR 0002).
There are two separate Devise models: User (with subclasses Volunteer, Supervisor, CasaAdmin — these are real STI subclasses, each with its own model file) and AllCasaAdmin (a separate top-level model and table for super-admins who span orgs). Devise is configured for both in config/routes.rb, and there is a parallel app/controllers/all_casa_admins/ namespace + app/models/all_casa_admins/ for the all-casa flows. When touching auth, check both sides.
- Models include
ByOrganizationScope(app/models/concerns/by_organization_scope.rb) which gives.by_organization(casa_org). - The
Organizationalcontroller concern (app/controllers/concerns/organizational.rb) providescurrent_organizationfrom the signed-in user. - Pundit policy scopes filter by org:
scope.by_organization(user.casa_org).
Every controller index action should call policy_scope; every show/update/destroy should call authorize. Controllers typically use after_action :verify_authorized (with explicit except:). When adding a new model with org-scoped data, all three places must be wired up or queries will leak across orgs.
Policies live in app/policies/. They define both predicate methods (update?, destroy?) and permitted_attributes (role-based field allowlists for strong params). When adding a model attribute that admins/supervisors/volunteers should be able to set, update permitted_attributes in the policy — not just the controller.
- Controllers stay thin. Standard pattern:
set_recordbefore_action,authorize, save/render with:unprocessable_contenton failure. - Service objects (
app/services/) —ServiceName.new(args).perform. Used for CSV exports, SMS reminders, complex multi-record operations. - Decorators (
app/decorators/, Draper) — presentation logic. Access view helpers viah.helper_method. Called asmodel.decorate. Date formatting, conditional display strings, etc. belong here, NOT in models or ERB. - ViewComponents (
app/components/) — reusable UI primitives (badges, modals, sidebar, dropdown menus, form bits). - Parameter objects (
app/values/) — builder-pattern wrappers for non-trivial strong-params logic, e.g.VolunteerParameters.new(params).with_password(pw).without_active. - Concerns (
app/{models,controllers}/concerns/) — shared behavior viaextend ActiveSupport::Concern. Notable model concerns:ByOrganizationScope,Roles,Api, and aCasaCase/directory of concern modules used by theCasaCasemodel.
Hotwire-first: Turbo for navigation/forms, Stimulus controllers in app/javascript/controllers/. There is an in-progress migration from inline JS / jQuery to Stimulus — new code should be Stimulus, but legacy jQuery is not flagged. JS is bundled via ESBuild (bin/asset_bundling_scripts/build_js.js); SCSS via the sass CLI. Both have watchers in Procfile.dev. Email views require inline CSS for client compatibility (ADR 0007).
- Soft deletes via Paranoia:
destroymarks deleted, doesn't hard-delete. Be deliberate when reasoning about uniqueness or "where is this record." - Strong Migrations is enabled and will block unsafe DDL (column removal without
safety_assured, non-concurrent index adds on large tables, type changes). Reversibility is required. - Soft-deletion-aware uniqueness:
validate_uniqueness_of(...).scoped_to(:casa_org_id)is the typical pattern — uniqueness is per-org. - Enums use prefix syntax:
enum :status, {active: 0, inactive: 1}, prefix: :status. - Post-deployment tasks run via After Party (
bundle exec rake after_party:run), invoked automatically bybin/updateand on Heroku release.
- System tests are preferred over controller tests (ADR 0006 —
app/controllers/concerns/users/andspec/controllers/exist but are minimal). New UI behavior should land inspec/system/. - Factories use traits for variants:
create(:casa_case, :active).bin/lintrunsfactory_bot:lintto catch invalid factories. - Use
buildfor unit tests;createonly when persistence is required.letfor lazy,let!when the record must exist before the example. - shoulda-matchers handles association/validation tests.
- No
sleepin tests — rely on Capybara waiting. - Flaky tests are disabled with
xit+ a tracking issue, never deleted.
This project uses Standard.rb (not vanilla RuboCop). Don't fight it on spacing/quotes/trailing commas. There is also a .standard_todo.yml of grandfathered violations — leave older files alone unless touching them substantively. Older migration files (2020–2024) are intentionally excluded from linting.
This branch introduces a Tailwind CSS v4 design system, documented in design.md (repo root) — the source of truth for the new aesthetic. Read it before UI work and keep it updated. New pages use Tailwind-only layouts and coexist with the legacy Bootstrap UI (migrate page-by-page). On every page, show user names without honorific prefixes (first + last only) — display_person for new UI, formatted_name at existing .display_name sites — never mutate the stored display_name. Workflow: on casadesign, commit and push to the casadesign branch at every checkpoint (do not wait for approval). Buttons come from the button_classes(:primary|:secondary|:danger|:danger_outline|:success) helper (one shared 40px height token, never hand-written class strings; before finishing UI work, grep the touched views for clickable elements with a hand-rolled button shape (inline-flex + rounded-lg + px-/py- + bg-/border-) and convert them to button_classes (the tertiary ghost is the only documented exception): this drift, a bespoke string at a different height next to the 40px token, is a recurring bug). Section headings and subtitles are sentence case with no trailing colon (colons only on inline key:value fact labels; audit grep '<h[123][^>]*>[^<]*:</h'); sentence case also covers app-shipped content (seed defaults and constants like ContactTypeGroup::DEFAULT_CONTACT_TYPE_GROUPS, shown as the multiselect chips), not just view copy, so before finishing scan the touched views AND app-shipped names/defaults for Title Case or ALL-CAPS and fix them (proper nouns/acronyms CASA, IEP excepted; never force-case free-form org data); person names are dark identifying text (font-medium text-slate-800), not brand nav links, and avoid sending the user out of the current flow via a name without a clear path back. Multiselects (TomSelect) load blank with a placeholder, never pre-selected with every option (the dropdown's Select/Unselect all covers that), and their chevron is a .ts-wrapper::after base64-SVG caret with z-index: 2 (TomSelect's opaque .ts-control paints over a plain ::after, so without the z-index the caret is hidden behind it; a content glyph escape and a raw data URI both broke in the build). Verify chevrons at the pixel level (screenshot + darkest-pixel), not by computed style. Native-dialog modals use the Dialog:: ViewComponent suite (Dialog::GroupComponent + Dialog::HeaderComponent/BodyComponent/FooterComponent), not the legacy Bootstrap Modal::*. Page headers use one primary CTA plus a More overflow menu (native <details> + the dropdown controller): keep core actions visible and overflow only occasional ones, never bury a core action (also breaks non-JS specs, which can't open <details>). On mobile, collapse the remaining visible secondaries into More too (render twice with hidden sm:contents / sm:hidden) so only the primary CTA and More share the top line. Leading icons on menu/list items top-align to the first line (items-start), never centered against wrapped text. When you build and verify a component or pattern, document it in design.md immediately (do not wait for approval) and reuse it from there: design.md is the source of truth, so match it and never re-derive or let it drift. Before finishing a new or changed component, check it against design.md and its sibling components and pixel-verify the match (e.g. the multiselect chevron must match the single-select chevron, measured via screenshot + darkest-pixel, never eyeballed or trusted from computed style): rendering is not the same as matching. When a partial is still rendered by legacy Bootstrap pages (e.g. shared/_manage_volunteers, shared/_court_order_list), never restyle it in place: add a casa_app-specific Tailwind twin (casa_cases/_volunteer_assignment, casa_cases/_court_orders) that preserves every JS hook (ids, classes, data-actions, field names, DOM adjacencies) and leave the shared partial for the Bootstrap pages.
.github/instructions/ruby.instructions.mdand.github/instructions/copilot-review.instructions.md— the project's own review checklist; the source of truth for "what is a bug-shaped change here."doc/architecture-decisions/— ADRs (especially 0002 no-UI-signup, 0003 two-user-tables, 0006 few-controller-tests, 0007 inline-email-CSS).doc/productsense.md— product philosophy.db/schema.rb— paste into dbdiagram.io for an ERD.