View performance data on Skylight
A Ruby on Rails application for managing summer camp applications, enrollments, courses, financial aid, recommendations, and payments. The system supports applicants, faculty, and administrators with separate interfaces and workflows.
- Features
- Technology Stack
- Prerequisites
- Installation
- Configuration
- Running the Application
- Running the Test Suite
- Deployment
- Project Structure
- License & Support
- Applicant portal — Registration, applicant details, enrollments, personal statements, course preferences, recommendations, financial aid requests, travel information, and payments
- Camp configuration — Per-year settings: application open/close dates, priority and materials deadlines, offer/reject/waitlist letters, application fees
- Sessions & courses — Camp occurrences (sessions), activities, courses with faculty and capacity; session assignments and course assignments with waitlisting
- Financial aid — Aid requests, amounts, status, and payment deadlines
- Recommendations — Request and upload recommendation letters; email-based workflow
- Payments — Payment flows and receipts (integration with external payment provider)
- Admin — Plain Rails
Admin::MVC at/admin(dashboard, 26 resources in four menu groups, 18 CSV reports, comments on records, CSV exports with a formula-injection guard);/legacy_adminbookmarks redirect to/admin - Faculty interface — Faculty login and student list/student page views
- Maintenance mode — Rack middleware (
lib/middleware/maintenance_mode.rb) servespublic/maintenance.htmlwhiletmp/maintenance.ymlexists on the server
| Layer | Technology |
|---|---|
| Runtime | Ruby 4.0.6 (.ruby-version, .tool-versions, Gemfile), Bundler 4 |
| Framework | Rails 8.1 (config.load_defaults 8.1); no Node.js anywhere |
| Database | MySQL 8 (mysql2 gem), utf8mb4 |
| Auth | Devise (users, admins, faculties); admins sign in at /admin/login |
| Admin | Admin:: namespace (Admin::BaseController, per-resource filters, scopes, Pagy pagination, batch actions, Admin::CsvExport, Admin::Reports SQL reports, Admin::Comment) |
| Server | Puma 8 (systemd notify built in) |
| Assets | Propshaft (digests + serves app/assets/builds, app/assets/images, importmap modules), importmap-rails, Hotwire (Turbo Drive + Stimulus), Tailwind CSS 4 (tailwindcss-rails; tailwind.css + admin.css bundles), Flatpickr |
| Money | money-rails 3 / money 7 (monetize columns, USD) |
| File storage | Active Storage (local disk / Google Cloud Storage in production) |
| Monitoring | Skylight, Sentry |
| Testing / CI | RSpec, FactoryBot, Capybara + Selenium; GitHub Actions (.github/workflows/ci.yml) |
| Deployment | Production: Capistrano 3 + asdf on a UM host. Staging: Hatchbox (DigitalOcean) |
- Ruby 4.0.6 (recommended: asdf or rbenv;
.ruby-versionand.tool-versionspin it) - MySQL 8.x (with OpenSSL available for the
mysql2gem) - Bundler 4.x (ships with Ruby 4.0;
Gemfile.lockrecords the version) - Git
On macOS with Homebrew MySQL you may need to pass include/lib paths when installing the mysql2 gem:
# Homebrew MySQL (example)
gem install mysql2 -v '0.5.6' -- --with-mysql-dir=/opt/homebrew/opt/mysql --with-mysql-lib=/opt/homebrew/opt/mysql/lib --with-mysql-include=/opt/homebrew/opt/mysql/include- Clone the repository
git clone git@github.com:lsa-mis/mmss-mysql.git
cd mmss-mysql- Install Ruby dependencies
bundle install- Create and configure the database (see Configuration)
# Set LOCAL_MYSQL_DATABASE_PASSWORD (see below), then:
bin/rails db:create
bin/rails db:schema:load
# Optionally: bin/rails db:seed- Prepare Rails credentials and config (see Configuration)
No Node.js or Yarn is required: JavaScript is served through import maps
(config/importmap.rb, app/javascript/, vendored packages in
vendor/javascript/) and Tailwind is compiled by the tailwindcss-ruby
standalone binary that ships with the tailwindcss-rails gem.
- Development / Test
Set the MySQL password via environment:Default DB names:export LOCAL_MYSQL_DATABASE_PASSWORD='your_local_mysql_password'
mmss-mysql_development,mmss-mysql_test.
Editconfig/database.ymlif you use a different user or host. - Production
Production uses credentials or environment variables for MySQL:Rails.application.credentials.dig(:mysql, :prod_user)orMYSQL_PROD_USERRails.application.credentials.dig(:mysql, :prod_password)orMYSQL_PROD_PASSWORDRails.application.credentials.dig(:mysql, :prod_servername)orMYSQL_PROD_HOSTRails.application.credentials.dig(:mysql, :prod_sslca)orMYSQL_PROD_SSLCA
Use bin/rails credentials:edit to set (among others):
mysql— production DB user, password, host, sslcaskylight— Skylight authentication (production/staging)- Any other secrets (e.g., payment provider, mailer)
Keep config/master.key secure and do not commit it. In deployment it is linked from the server’s shared config.
- Development / Test
Uses local disk (storage/,tmp/storage). - Production
Configured for Google Cloud Storage (GCS). A GCS keyfile is expected. Bucket and project are set inconfig/storage.yml.
Both deployed environments run behind a TLS-terminating proxy, so config.assume_ssl and
config.force_ssl are on, and ActionDispatch::HostAuthorization only answers the public hostname:
mmss-registration.math.lsa.umich.edu (production) and mmss-registration-staging.lsa.umich.edu
(staging). Any other Host header gets a 403 and a Blocked hosts: log line. To serve additional
names (the cluster's internal node names, a load-balancer IP, a Hatchbox preview host) set
RAILS_ALLOWED_HOSTS=mathmmssapp2.miserver.it.umich.edu,.internal.umich.edu # comma-separated; leading dot = any subdomainin the service environment and restart Puma; no deploy is needed (lib/allowed_hosts.rb). /up
(the Rails health check) is exempt and is served for any host, so monitors can address the node
directly. Staging also still reads the legacy STAGING_ALLOWED_HOSTS variable.
- Skylight — Set
SKYLIGHT_AUTHENTICATIONor use credentials for production/staging. - Sentry — DSN from credentials (
sentry.dsn); options inconfig/initializers/sentry.rb. - Redis — Only referenced by
config/cable.ymlin production (REDIS_URL); the app defines no Action Cable channels, so nothing connects unless one is added.
- Start MySQL (if not running as a service).
- Start the Rails server and the Tailwind watcher
bin/devbin/dev runs Procfile.dev through foreman (installed on first use):
bin/rails server on port 3000 plus the two Tailwind watchers
(tailwindcss:watch for the applicant/faculty bundle, tailwindcss:watch:admin
for admin.css), which rebuild app/assets/builds/*.css whenever
app/assets/tailwind/ or the views change. Without the watchers, run
bin/rails tailwindcss:build once after editing styles and start
bin/rails server on its own.
Default: http://localhost:3000
3. Useful URLs (development)
- Root:
/ - Admin:
/admin(login at/admin/login; seed adminadmin@example.com/passwordpasswordfromdb/seeds.rb) - Faculty:
/faculty,/faculty_login - Letter opener (development and staging):
/letter_opener(on staging, protect with HTTP basic auth env vars or network rules)
- RSpec
Ensure the test database exists and is migrated, and that the Tailwind build exists (request and system specs render the layouts):
bundle exec rspecSystem specs (RAILS_ENV=test bin/rails db:create db:schema:load RAILS_ENV=test bin/rails tailwindcss:build
spec/system) drive headless Chrome through Selenium (SHOW_BROWSER=1for a visible browser). CI runs everything except the system specs on every pull request against a MySQL 8 service (.github/workflows/ci.yml). Conventions and layout: TESTING_STRATEGY.md; production log forensics: LOG_INVESTIGATION_GUIDE.md. - Code style (Standard Ruby)
Configuration lives in
bin/lint # = bundle exec standardrb; CI `lint` job fails on any offense bin/lint --fix # apply safe autocorrections before committing
.standard.yml(standard+standard-rails;db/schema.rb, legacy migrations underdb/migrate,bin,vendorandtmpare ignored). Prefer a targeted# standard:disable Cop/Name -- reasonover a behaviour change when a cop cannot be satisfied by formatting alone.
Work lands on staging (pull requests, CI), is verified on the staging host, and is then promoted
to main, which production deploys from.
- Repo:
git@github.com:lsa-mis/mmss-mysql.git - Branch:
main - Server:
config/deploy/production.rb(e.g.mathmmssapp2.miserver.it.umich.edu), roles: app, db, web; app lives in/home/deployer/apps/mmss-mysql. - Linked files (must exist in shared config on the server):
config/puma.rb,config/nginx.conf,config/master.key,config/lsa-was-base-c096c776ead3.json,mysql/InCommon.CA.crt
Deploy steps:
# 1. once per Ruby bump, on the host as deployer (see below)
asdf install ruby 4.0.6 && asdf reshim ruby
# 2. from your machine, with main checked out and pushed
bundle exec cap production deploy # check_revision, bundle, assets:precompile, db:migrate, puma:restart
bundle exec cap production deploy:upload # only when a linked config file changed (puma.rb, nginx.conf, master.key, GCS key, CA cert)
# operations
bundle exec cap production puma:restart
bundle exec cap production puma:stop
bundle exec cap production maintenance:start # tmp/maintenance.yml from config/maintenance_template.yml
bundle exec cap production maintenance:stop
bundle exec cap production rubygems:updateBefore deploy, deploy:check_revision ensures local HEAD matches origin/main. Migrations run
through capistrano-rails (deploy:migrate, db role) whenever db/migrate changed. Puma's
environment (systemd unit, see config/puma_prod.service) is where RAILS_ALLOWED_HOSTS,
RAILS_LOG_LEVEL and RAILS_MAX_THREADS go.
The host provides Ruby through asdf (capistrano-asdf reads .tool-versions, and
config/deploy.rb points bundle/ruby at /home/deployer/.asdf/shims). Install the
Ruby version pinned in .tool-versions on the host before deploying a release that bumps
it, e.g. for 4.0.6:
asdf install ruby 4.0.6 # needs libyaml-dev, libssl-dev, zlib1g-dev, libffi-dev, libmysqlclient-dev
asdf reshim rubydebug:print_ruby_version runs before bundler:install and prints the Ruby the release
resolved to, so a missing install fails early.
Assets are compiled on the server by capistrano-rails (bin/rails assets:precompile),
which runs tailwindcss:build (both bundles, via the tailwindcss-ruby standalone
binary) and then Propshaft copies the digested files to public/assets (manifest:
public/assets/.manifest.json, which capistrano-rails 1.7 backs up and restores).
No Node.js, Yarn or NODE_OPTIONS are needed on the host. Compiled assets live in
the linked public/assets directory.
maintenance:start uploads config/maintenance_template.yml to tmp/maintenance.yml on the server; while that file exists the MaintenanceMode middleware answers every request routed through Rails (except allowed_paths / allowed_ips) with public/maintenance.html; static files that nginx serves directly from public/ via try_files never reach the middleware (same as with turnout), the response_code (default 503) and a Retry-After header. maintenance:stop removes the file. Edit the template's reason, allowed_ips, etc. before starting.
Hatchbox builds the app from the repository: it reads .ruby-version, runs bundle install and
bin/rails assets:precompile (Tailwind via the bundled standalone binary, then Propshaft), and runs
the release command. Set it up as a separate Hatchbox app on the staging branch with a
deploy webhook, so every merge to staging deploys, and:
RAILS_ENV=stagingso Rails loads config/environments/staging.rb (local Active Storage,letter_opener_web, no GCS keyfile, HTTP basic auth on/letter_opener).- Release command:
bin/rails db:migrate. - Process: the root Procfile (
bundle exec puma -C config/puma.default.rb, binds$PORT). - The environment variables below.
Environment variables
| Variable | Purpose |
|---|---|
RAILS_ENV |
staging |
RAILS_MASTER_KEY |
Decrypts credentials (use staging-specific credentials if you run bin/rails credentials:edit --environment staging) |
SECRET_KEY_BASE |
Hatchbox often sets this; required for sessions |
DATABASE_URL |
MySQL URL from Hatchbox / DigitalOcean (e.g. mysql2://user:pass@host:3306/dbname) — or omit and set STAGING_DATABASE_* in [config/database.yml](config/database.yml) |
STAGING_MAILER_HOST |
Public hostname for mailer URLs (e.g. staging.example.edu) |
STAGING_MAILER_PROTOCOL |
Usually https |
RAILS_ALLOWED_HOSTS |
Extra comma-separated hostnames for ActionDispatch::HostAuthorization (the public hostname is built in; STAGING_ALLOWED_HOSTS still works as a legacy alias) |
LETTER_OPENER_WEB_HTTP_BASIC_USER / LETTER_OPENER_WEB_HTTP_BASIC_PASSWORD |
Optional HTTP basic auth for /letter_opener |
RAILS_SERVE_STATIC_FILES |
Set if the app serves static files without nginx in front |
STAGING_FORCE_SSL |
Defaults to true (HTTPS, secure cookies, assume_ssl); set false only for a plain-HTTP staging box |
RAILS_LOG_TO_STDOUT |
Set so Hatchbox collects the logs |
| Path | Purpose |
|---|---|
app/ |
Models, controllers, views, mailers, helpers; the admin lives in app/{controllers,views,helpers,filters,lib,queries}/admin/ |
lib/allowed_hosts.rb |
Host allow-list for production/staging (RAILS_ALLOWED_HOSTS) |
lib/middleware/ |
MaintenanceMode and the staging LetterOpenerWebBasicAuth Rack middleware |
app/javascript/ |
Import-map entry point (application.js) and Stimulus controllers |
app/assets/tailwind/ |
Tailwind 4 CSS-first config and app styles, built to app/assets/builds/tailwind.css |
config/importmap.rb |
JavaScript import map pins; bin/importmap pin <pkg> vendors into vendor/javascript/ |
config/ |
Application, routes, environments, initializers, deploy |
Procfile / Procfile.dev |
Puma on $PORT for PaaS; local Rails server + Tailwind watcher (bin/dev) |
config/puma.default.rb |
Portable Puma (Hatchbox / DO); production Capistrano still uses linked config/puma.rb |
db/ |
Schema, migrations, seeds |
config/deploy.rb |
Capistrano (production): puma, maintenance, rubygems and debug tasks |
spec/ |
RSpec tests and support |
config/storage.yml |
Active Storage backends (local, GCS) |
- User — Applicant account (Devise).
- ApplicantDetail — Demographics, contact, parent info, etc.
- Enrollment — Per-year application: high school, statement, status, offer status, session assignments, course preferences, recommendations, financial aid, travel.
- CampConfiguration — Camp year and dates (application open/close, priority, materials due, etc.).
- CampOccurrence — A session (date range); has activities and courses.
- Course — Course within a session; course preferences and course assignments (with waitlist).
- SessionAssignment — Enrollment in a session; accept/decline offers.
- FinancialAid, Recommendation, Payment, Travel — Supporting enrollment data.
Proprietary — LSA MIS. For access, deployment, or support, contact the maintaining team.