My opinionated ruby on rails template
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

new stuff and bugfixes

+1103 -124
+21 -6
README.md
··· 1 1 # 🚃 boxcar 2 2 3 - A production-ready, opinionated Rails 7+ application template with 24 integrated modules for authentication, authorization, monitoring, and more. 3 + A production-ready, opinionated Rails 7+ application template with 33 integrated modules for authentication, authorization, monitoring, and more. 4 4 5 5 Inspired by [@nora](https://github.com/24c02)'s [thirdrail](https://github.com/24c02/thirdrail). 6 6 ··· 9 9 | Category | Modules | 10 10 |----------|---------| 11 11 | **Auth & Security** | Custom auth, Pundit, Lockbox encryption, rate limiting | 12 - | **Admin Dashboards** | Blazer, Flipper, Rails Performance, Mission Control | 12 + | **Admin Dashboards** | Blazer, Flipper, Rails Performance, GoodJob, PgHero | 13 13 | **Data Management** | Soft deletes, audit trails, friendly URLs, full-text search | 14 14 | **Observability** | Health checks, analytics, console auditing, StatsD metrics | 15 - | **Infrastructure** | Redis, PostgreSQL multi-db, Solid Queue, Tailwind CSS | 15 + | **Infrastructure** | Redis, PostgreSQL multi-db, GoodJob, Tailwind CSS | 16 16 17 17 ## Quick Start 18 18 ··· 58 58 - **BlindIndex** — Search encrypted fields without decryption 59 59 - **InvisibleCaptcha** — Honeypot spam protection 60 60 - **Strong Migrations** — Prevents dangerous migrations in production 61 + - **CSP & CORS** — Content Security Policy and cross-origin headers 62 + - **Security Scanning** — Bundler-audit and Brakeman in CI 61 63 62 64 ### Admin Dashboards 63 65 ··· 68 70 | Blazer | `/admin/blazer` | admin+ | SQL-based analytics | 69 71 | Flipper | `/admin/flipper` | super_admin+ | Feature flags | 70 72 | Performance | `/admin/performance` | admin+ | Request monitoring | 71 - | Jobs | `/admin/jobs` | admin+ | Background job dashboard | 73 + | Jobs | `/admin/jobs` | admin+ | GoodJob dashboard | 74 + | PgHero | `/admin/pghero` | admin+ | PostgreSQL insights | 72 75 | Console Audits | `/admin/console_audits` | super_admin+ | Rails console access logs | 73 76 74 77 ### Data Features ··· 81 84 82 85 ### Background Jobs 83 86 84 - - **Solid Queue** — Database-backed job processing (no Redis required for jobs) 85 - - **Mission Control** — Web UI for job monitoring and management 87 + - **GoodJob** — PostgreSQL-backed job processing with built-in dashboard 88 + - **Recurring Jobs** — Cron-like scheduling built-in 89 + - **Job Preservation** — Debug failed jobs with full history 86 90 87 91 ### Monitoring & Analytics 88 92 ··· 90 94 - **Ahoy Analytics** — Visit and event tracking with email integration 91 95 - **StatsD Metrics** — Request timing, custom gauges, Datadog-ready 92 96 - **Console1984** — Encrypted audit logs for Rails console access 97 + - **Lograge** — Structured JSON logging with request ID tracing 98 + - **Logstop** — Automatic PII filtering from logs 93 99 94 100 ### Infrastructure 95 101 96 102 - **PostgreSQL** — Multi-database setup (primary, queue, cache, cable) 97 103 - **Redis** — Sessions (db 2), cache (db 1), rate limiting (db 5) 98 104 - **Tailwind CSS** — Pre-configured and ready to customize 105 + - **IdentityCache** — Blob-level caching for ActiveRecord 106 + - **PgHero** — PostgreSQL performance insights 107 + 108 + ### Email 109 + 110 + - **Transactional Templates** — Welcome, password reset, email confirmation 111 + - **Email Previews** — Preview emails in development 112 + - **Premailer** — Automatic CSS inlining for email clients 113 + - **Mailkick** — Unsubscribe management 99 114 100 115 ## Included Concerns 101 116
+5 -2
modules/admin_routes.rb
··· 39 39 # Rails Performance Dashboard (admin or above) 40 40 mount RailsPerformance::Engine, at: 'performance', constraints: AdminConstraint.new(:rails_performance?) if defined?(RailsPerformance) 41 41 42 - # Mission Control Jobs Dashboard (admin or above) 43 - mount MissionControl::Jobs::Engine, at: 'jobs', constraints: AdminConstraint.new(:jobs?) if defined?(MissionControl::Jobs) 42 + # GoodJob Dashboard (admin or above) 43 + mount GoodJob::Engine, at: 'jobs', constraints: AdminConstraint.new(:jobs?) if defined?(GoodJob) 44 44 45 45 # Console Audits (super_admin or above) 46 46 mount Audits1984::Engine, at: 'console_audits', constraints: AdminConstraint.new(:console_audits?) if defined?(Audits1984) 47 + 48 + # PgHero PostgreSQL Dashboard (admin or above) 49 + mount PgHero::Engine, at: 'pghero', constraints: AdminConstraint.new(:pghero?) if defined?(PgHero) 47 50 48 51 resources :users 49 52 end
+1 -1
modules/analytics.rb
··· 13 13 end 14 14 15 15 say ' Creating Ahoy initializer...', :cyan 16 - file 'config/initializers/ahoy.rb', <<~RUBY 16 + file 'config/initializers/ahoy.rb', <<~RUBY, force: true 17 17 # frozen_string_literal: true 18 18 19 19 class Ahoy::Store < Ahoy::DatabaseStore
+6 -13
modules/auth.rb
··· 5 5 6 6 gem 'bcrypt', '~> 3.1' 7 7 8 - say ' Generating User model...', :cyan 9 - generate :model, 'User email:string:uniq password_digest:string role:integer' 8 + # Generate models after bundle install 9 + after_bundle do 10 + say ' Generating User model...', :cyan 11 + generate :model, 'User email:string:uniq password_digest:string role:integer' 10 12 11 - # Add index for email lookup 12 - begin 13 - inject_into_file "db/migrate/#{Dir.entries('db/migrate').grep(/create_users/).first}", 14 - after: "t.string :email\n" do 15 - " t.index :email, unique: true\n" 16 - end 17 - rescue StandardError 18 - nil 13 + say ' Generating Session model...', :cyan 14 + generate :model, 'Session user:references token:string:uniq ip_address:string user_agent:string' 19 15 end 20 16 21 17 # User model with has_secure_password and roles ··· 47 43 end 48 44 end 49 45 RUBY 50 - 51 - say ' Generating Session model...', :cyan 52 - generate :model, 'Session user:references token:string:uniq ip_address:string user_agent:string' 53 46 54 47 file 'app/models/session.rb', <<~RUBY, force: true 55 48 class Session < ApplicationRecord
+1 -1
modules/blazer.rb
··· 21 21 RUBY 22 22 23 23 say ' Creating Blazer config...', :cyan 24 - file 'config/blazer.yml', <<~YAML 24 + file 'config/blazer.yml', <<~YAML, force: true 25 25 # Blazer configuration 26 26 # https://github.com/ankane/blazer 27 27
+227
modules/caching.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up caching strategies...', :green 4 + 5 + gem 'identity_cache' 6 + gem 'cityhash' # Required for identity_cache 7 + 8 + say ' Configuring IdentityCache...', :cyan 9 + initializer 'identity_cache.rb', <<~RUBY 10 + # frozen_string_literal: true 11 + 12 + # IdentityCache - Blob level caching for Active Record 13 + # See: https://github.com/Shopify/identity_cache 14 + 15 + IdentityCache.logger = Rails.logger 16 + IdentityCache.cache_backend = Rails.cache 17 + RUBY 18 + 19 + say ' Creating Cacheable concern...', :cyan 20 + file 'app/models/concerns/cacheable.rb', <<~RUBY 21 + # frozen_string_literal: true 22 + 23 + # Include in models for identity caching 24 + # 25 + # Usage: 26 + # class User < ApplicationRecord 27 + # include Cacheable 28 + # 29 + # cache_index :email 30 + # cache_has_many :posts 31 + # end 32 + # 33 + # User.fetch(1) # Cached fetch by id 34 + # User.fetch_by_email(email) # Cached fetch by index 35 + # 36 + module Cacheable 37 + extend ActiveSupport::Concern 38 + 39 + included do 40 + include IdentityCache 41 + end 42 + 43 + class_methods do 44 + # Expire cache when record is updated 45 + def expire_cache_for(record) 46 + record.expire_cache if record.respond_to?(:expire_cache) 47 + end 48 + end 49 + end 50 + RUBY 51 + 52 + say ' Creating CacheHelper...', :cyan 53 + file 'app/helpers/cache_helper.rb', <<~RUBY 54 + # frozen_string_literal: true 55 + 56 + # Helper methods for view caching 57 + # 58 + # Russian Doll Caching: 59 + # The key insight is that when a nested object changes, 60 + # only its cache (and parent caches) are invalidated. 61 + # 62 + # Example view structure: 63 + # <% cache @project do %> 64 + # <%= render @project.tasks %> 65 + # <% end %> 66 + # 67 + # # _task.html.erb 68 + # <% cache task do %> 69 + # <%= task.name %> 70 + # <%= render task.comments %> 71 + # <% end %> 72 + # 73 + module CacheHelper 74 + # Cache with automatic expiry based on record updated_at 75 + # Also includes a version number for manual cache busting 76 + # 77 + # Usage: 78 + # <% cache_with_version @user, 'v2' do %> 79 + # ... 80 + # <% end %> 81 + # 82 + def cache_with_version(record, version = 'v1', options = {}, &block) 83 + cache([version, record], options, &block) 84 + end 85 + 86 + # Cache a collection with automatic key generation 87 + # Useful for lists that change frequently 88 + # 89 + # Usage: 90 + # <% cache_collection @users do |user| %> 91 + # <%= render user %> 92 + # <% end %> 93 + # 94 + def cache_collection(collection, options = {}, &block) 95 + cache([collection.cache_key_with_version, collection.size], options, &block) 96 + end 97 + 98 + # Time-based cache for content that should refresh periodically 99 + # 100 + # Usage: 101 + # <% cache_for 5.minutes, 'dashboard_stats' do %> 102 + # ... 103 + # <% end %> 104 + # 105 + def cache_for(duration, key, options = {}, &block) 106 + expires_key = (Time.current.to_i / duration.to_i) 107 + cache([key, expires_key], options, &block) 108 + end 109 + end 110 + RUBY 111 + 112 + say ' Creating CacheWarmer job...', :cyan 113 + file 'app/jobs/cache_warmer_job.rb', <<~RUBY 114 + # frozen_string_literal: true 115 + 116 + # Warm caches for frequently accessed data 117 + # 118 + # Schedule this job to run periodically: 119 + # CacheWarmerJob.perform_later 120 + # 121 + # Or warm specific caches: 122 + # CacheWarmerJob.perform_later(caches: ['users', 'settings']) 123 + # 124 + class CacheWarmerJob < ApplicationJob 125 + queue_as :low 126 + 127 + def perform(caches: nil) 128 + warmers = caches || default_warmers 129 + warmers.each { |warmer| send("warm_\#{warmer}") } 130 + end 131 + 132 + private 133 + 134 + def default_warmers 135 + %w[settings] 136 + end 137 + 138 + # Add your cache warming methods here 139 + # 140 + # def warm_users 141 + # User.active.find_each do |user| 142 + # Rails.cache.fetch(user.cache_key_with_version) { user } 143 + # end 144 + # end 145 + 146 + def warm_settings 147 + # Example: warm application settings 148 + # AppConfig.all.each do |config| 149 + # Rails.cache.fetch(['app_config', config.key]) { config.value } 150 + # end 151 + end 152 + end 153 + RUBY 154 + 155 + say ' Documenting caching patterns...', :cyan 156 + file 'docs/caching.md', <<~MARKDOWN 157 + # Caching Strategies 158 + 159 + ## Fragment Caching (Views) 160 + 161 + Basic fragment cache: 162 + ```erb 163 + <% cache @user do %> 164 + <%= @user.name %> 165 + <% end %> 166 + ``` 167 + 168 + ## Russian Doll Caching 169 + 170 + Nested caches that auto-expire: 171 + ```erb 172 + <% cache @project do %> 173 + <h1><%= @project.name %></h1> 174 + <%= render @project.tasks %> 175 + <% end %> 176 + 177 + <!-- _task.html.erb --> 178 + <% cache task do %> 179 + <%= task.name %> 180 + <% end %> 181 + ``` 182 + 183 + **Important:** Use `touch: true` on associations: 184 + ```ruby 185 + class Task < ApplicationRecord 186 + belongs_to :project, touch: true 187 + end 188 + ``` 189 + 190 + ## Model Caching (IdentityCache) 191 + 192 + ```ruby 193 + class User < ApplicationRecord 194 + include Cacheable 195 + 196 + cache_index :email 197 + cache_has_many :posts, embed: true 198 + end 199 + 200 + # Usage 201 + User.fetch(1) # Cached 202 + User.fetch_by_email('a@b.com') # Cached 203 + user.fetch_posts # Cached with user 204 + ``` 205 + 206 + ## Low-Level Caching 207 + 208 + ```ruby 209 + Rails.cache.fetch('expensive_operation', expires_in: 1.hour) do 210 + ExpensiveService.call 211 + end 212 + ``` 213 + 214 + ## Cache Warming 215 + 216 + ```ruby 217 + # Run periodically 218 + CacheWarmerJob.perform_later 219 + 220 + # Or specific caches 221 + CacheWarmerJob.perform_later(caches: ['users']) 222 + ``` 223 + MARKDOWN 224 + 225 + say 'Caching strategies configured!', :green 226 + say ' Use `include Cacheable` in models for identity caching', :cyan 227 + say ' See docs/caching.md for patterns', :cyan
+4 -4
modules/console1984.rb
··· 6 6 gem 'audits1984' 7 7 8 8 after_bundle do 9 - say ' Running Console1984 installer...', :cyan 10 - rails_command 'console1984:install' 9 + say ' Running Console1984 migrations installer...', :cyan 10 + rails_command 'console1984:install:migrations' 11 11 12 - say ' Running Audits1984 installer...', :cyan 13 - rails_command 'audits1984:install' 12 + say ' Running Audits1984 migrations installer...', :cyan 13 + rails_command 'audits1984:install:migrations' 14 14 end 15 15 16 16 say ' Creating Console1984 initializer...', :cyan
+7 -4
modules/credentials.rb
··· 2 2 3 3 say 'Setting up credentials...', :green 4 4 5 - # Initialize credentials for each environment 6 - %w[development staging production].each do |env| 7 - say " Creating #{env} credentials...", :cyan 8 - run "EDITOR='echo' bin/rails credentials:edit --environment #{env}", abort_on_failure: false 5 + # Initialize credentials for each environment (after bundle) 6 + after_bundle do 7 + say ' Creating environment credentials...', :cyan 8 + %w[development staging production].each do |env| 9 + say " #{env}...", :cyan 10 + run "EDITOR='echo' bin/rails credentials:edit --environment #{env}", abort_on_failure: false 11 + end 9 12 end 10 13 11 14 # Create credentials example file
+94
modules/csp.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up security hardening...', :green 4 + 5 + gem 'rack-cors' 6 + # bundler-audit included by Rails 8 7 + 8 + say ' Configuring Content Security Policy...', :cyan 9 + initializer 'content_security_policy.rb', <<~RUBY 10 + # frozen_string_literal: true 11 + 12 + # Define an application-wide content security policy 13 + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 14 + 15 + Rails.application.configure do 16 + config.content_security_policy do |policy| 17 + policy.default_src :self 18 + policy.font_src :self, :data, 'https://fonts.gstatic.com' 19 + policy.img_src :self, :data, :blob 20 + policy.object_src :none 21 + policy.script_src :self 22 + policy.style_src :self, :unsafe_inline, 'https://fonts.googleapis.com' 23 + policy.frame_ancestors :self 24 + policy.base_uri :self 25 + policy.form_action :self 26 + 27 + # Allow connections to same origin and websockets 28 + policy.connect_src :self, :wss 29 + 30 + # Report violations to your error tracking service 31 + # policy.report_uri '/csp-report' 32 + end 33 + 34 + # Generate nonce for inline scripts/styles 35 + # Use <%= csp_meta_tag %> in your layout and 36 + # <%= javascript_tag nonce: true %> for inline scripts 37 + config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 38 + config.content_security_policy_nonce_directives = %w[script-src style-src] 39 + 40 + # Report CSP violations without enforcing (useful for rollout) 41 + # config.content_security_policy_report_only = true 42 + end 43 + RUBY 44 + 45 + say ' Configuring CORS...', :cyan 46 + initializer 'cors.rb', <<~RUBY 47 + # frozen_string_literal: true 48 + 49 + # Configure Cross-Origin Resource Sharing (CORS) 50 + # See: https://github.com/cyu/rack-cors 51 + 52 + Rails.application.config.middleware.insert_before 0, Rack::Cors do 53 + allow do 54 + # Allow requests from your frontend domain 55 + origins Rails.env.development? ? 'localhost:3000' : ENV.fetch('CORS_ORIGINS', '').split(',') 56 + 57 + resource '/api/*', 58 + headers: :any, 59 + methods: %i[get post put patch delete options head], 60 + credentials: true, 61 + max_age: 86_400 62 + 63 + # Health checks should be accessible 64 + resource '/health*', 65 + headers: :any, 66 + methods: [:get] 67 + end 68 + end 69 + RUBY 70 + 71 + say ' Adding security headers...', :cyan 72 + initializer 'secure_headers.rb', <<~RUBY 73 + # frozen_string_literal: true 74 + 75 + # Additional security headers configured via middleware 76 + Rails.application.config.action_dispatch.default_headers = { 77 + 'X-Frame-Options' => 'SAMEORIGIN', 78 + 'X-Content-Type-Options' => 'nosniff', 79 + 'X-XSS-Protection' => '0', # Disabled as per modern best practices 80 + 'X-Permitted-Cross-Domain-Policies' => 'none', 81 + 'Referrer-Policy' => 'strict-origin-when-cross-origin', 82 + 'Permissions-Policy' => 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()' 83 + } 84 + RUBY 85 + 86 + say ' Adding CSP meta tag to layout...', :cyan 87 + inject_into_file 'app/views/layouts/application.html.erb', after: "<%= csrf_meta_tags %>\n" do 88 + " <%= csp_meta_tag %>\n" 89 + end 90 + 91 + say 'Security hardening configured!', :green 92 + say ' CSP headers enabled (review policy for your needs)', :cyan 93 + say ' CORS configured for API routes', :cyan 94 + say ' Run `bundle audit` to check for vulnerabilities', :yellow
+2
modules/database.rb
··· 2 2 3 3 say 'Configuring PostgreSQL with multi-database setup...', :green 4 4 5 + gem 'pg' 6 + 5 7 say ' Creating database.yml...', :cyan 6 8 file 'config/database.yml', <<~YAML, force: true 7 9 # PostgreSQL for all environments (with Row Level Security support)
+11
modules/development_tools.rb
··· 10 10 gem 'actual_db_schema' 11 11 gem 'annotaterb' 12 12 gem 'letter_opener_web' 13 + gem 'rack-mini-profiler', require: false 13 14 end 15 + 16 + # Rack Mini Profiler configuration 17 + initializer 'rack_mini_profiler.rb', <<~RUBY 18 + # frozen_string_literal: true 19 + 20 + if defined?(Rack::MiniProfiler) && Rails.env.development? 21 + require 'rack-mini-profiler' 22 + Rack::MiniProfilerRails.initialize!(Rails.application) 23 + end 24 + RUBY 14 25 15 26 # Bullet configuration for N+1 query detection 16 27 initializer 'bullet.rb', <<~RUBY
+286
modules/email.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up email infrastructure...', :green 4 + 5 + gem 'premailer-rails' # Inline CSS for email clients 6 + 7 + say ' Configuring Premailer...', :cyan 8 + initializer 'premailer.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + Premailer::Rails.config.merge!( 12 + preserve_styles: true, 13 + remove_ids: false, 14 + remove_classes: false 15 + ) 16 + RUBY 17 + 18 + say ' Creating UserMailer...', :cyan 19 + file 'app/mailers/user_mailer.rb', <<~RUBY 20 + # frozen_string_literal: true 21 + 22 + class UserMailer < ApplicationMailer 23 + def welcome(user) 24 + @user = user 25 + @login_url = sign_in_url 26 + 27 + mail( 28 + to: @user.email, 29 + subject: "Welcome to \#{Rails.application.class.module_parent_name}" 30 + ) 31 + end 32 + 33 + def password_reset(user, token) 34 + @user = user 35 + @token = token 36 + @reset_url = edit_password_reset_url(token: @token) 37 + 38 + mail( 39 + to: @user.email, 40 + subject: 'Reset your password' 41 + ) 42 + end 43 + 44 + def email_confirmation(user, token) 45 + @user = user 46 + @token = token 47 + @confirm_url = confirm_email_url(token: @token) 48 + 49 + mail( 50 + to: @user.email, 51 + subject: 'Confirm your email address' 52 + ) 53 + end 54 + end 55 + RUBY 56 + 57 + say ' Creating email layout...', :cyan 58 + file 'app/views/layouts/mailer.html.erb', <<~ERB, force: true 59 + <!DOCTYPE html> 60 + <html> 61 + <head> 62 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 63 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 64 + <style> 65 + /* Base styles */ 66 + body { 67 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 68 + font-size: 16px; 69 + line-height: 1.5; 70 + color: #333333; 71 + background-color: #f5f5f5; 72 + margin: 0; 73 + padding: 0; 74 + } 75 + 76 + /* Email container */ 77 + .email-wrapper { 78 + max-width: 600px; 79 + margin: 0 auto; 80 + padding: 20px; 81 + } 82 + 83 + .email-content { 84 + background-color: #ffffff; 85 + border-radius: 8px; 86 + padding: 40px; 87 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 88 + } 89 + 90 + /* Header */ 91 + .email-header { 92 + text-align: center; 93 + margin-bottom: 30px; 94 + } 95 + 96 + .email-header h1 { 97 + margin: 0; 98 + font-size: 24px; 99 + color: #111111; 100 + } 101 + 102 + /* Body */ 103 + .email-body { 104 + margin-bottom: 30px; 105 + } 106 + 107 + .email-body p { 108 + margin: 0 0 16px 0; 109 + } 110 + 111 + /* Button */ 112 + .button { 113 + display: inline-block; 114 + padding: 12px 24px; 115 + background-color: #2563eb; 116 + color: #ffffff !important; 117 + text-decoration: none; 118 + border-radius: 6px; 119 + font-weight: 600; 120 + } 121 + 122 + .button:hover { 123 + background-color: #1d4ed8; 124 + } 125 + 126 + /* Footer */ 127 + .email-footer { 128 + text-align: center; 129 + font-size: 14px; 130 + color: #666666; 131 + border-top: 1px solid #eeeeee; 132 + padding-top: 20px; 133 + margin-top: 30px; 134 + } 135 + 136 + .email-footer a { 137 + color: #2563eb; 138 + } 139 + </style> 140 + </head> 141 + <body> 142 + <div class="email-wrapper"> 143 + <div class="email-content"> 144 + <%= yield %> 145 + </div> 146 + <div class="email-footer"> 147 + <p>&copy; <%= Date.current.year %> <%= Rails.application.class.module_parent_name %>. All rights reserved.</p> 148 + <p> 149 + <% if @user.respond_to?(:email) %> 150 + <a href="<%= mailkick_unsubscribe_url %>">Unsubscribe</a> 151 + <% end %> 152 + </p> 153 + </div> 154 + </div> 155 + </body> 156 + </html> 157 + ERB 158 + 159 + say ' Creating welcome email template...', :cyan 160 + file 'app/views/user_mailer/welcome.html.erb', <<~ERB 161 + <div class="email-header"> 162 + <h1>Welcome!</h1> 163 + </div> 164 + 165 + <div class="email-body"> 166 + <p>Hi <%= @user.email %>,</p> 167 + 168 + <p>Thanks for signing up! We're excited to have you on board.</p> 169 + 170 + <p>To get started, click the button below to sign in to your account:</p> 171 + 172 + <p style="text-align: center; margin: 30px 0;"> 173 + <a href="<%= @login_url %>" class="button">Sign In</a> 174 + </p> 175 + 176 + <p>If you have any questions, just reply to this email - we're always happy to help.</p> 177 + </div> 178 + ERB 179 + 180 + say ' Creating password reset template...', :cyan 181 + file 'app/views/user_mailer/password_reset.html.erb', <<~ERB 182 + <div class="email-header"> 183 + <h1>Reset Your Password</h1> 184 + </div> 185 + 186 + <div class="email-body"> 187 + <p>Hi <%= @user.email %>,</p> 188 + 189 + <p>We received a request to reset your password. Click the button below to choose a new one:</p> 190 + 191 + <p style="text-align: center; margin: 30px 0;"> 192 + <a href="<%= @reset_url %>" class="button">Reset Password</a> 193 + </p> 194 + 195 + <p>This link will expire in 2 hours.</p> 196 + 197 + <p>If you didn't request this, you can safely ignore this email. Your password won't be changed.</p> 198 + </div> 199 + ERB 200 + 201 + say ' Creating email confirmation template...', :cyan 202 + file 'app/views/user_mailer/email_confirmation.html.erb', <<~ERB 203 + <div class="email-header"> 204 + <h1>Confirm Your Email</h1> 205 + </div> 206 + 207 + <div class="email-body"> 208 + <p>Hi <%= @user.email %>,</p> 209 + 210 + <p>Please confirm your email address by clicking the button below:</p> 211 + 212 + <p style="text-align: center; margin: 30px 0;"> 213 + <a href="<%= @confirm_url %>" class="button">Confirm Email</a> 214 + </p> 215 + 216 + <p>This link will expire in 24 hours.</p> 217 + 218 + <p>If you didn't create an account, you can safely ignore this email.</p> 219 + </div> 220 + ERB 221 + 222 + say ' Creating text email templates...', :cyan 223 + file 'app/views/user_mailer/welcome.text.erb', <<~ERB 224 + Welcome! 225 + 226 + Hi <%= @user.email %>, 227 + 228 + Thanks for signing up! We're excited to have you on board. 229 + 230 + To get started, visit: <%= @login_url %> 231 + 232 + If you have any questions, just reply to this email. 233 + ERB 234 + 235 + file 'app/views/user_mailer/password_reset.text.erb', <<~ERB 236 + Reset Your Password 237 + 238 + Hi <%= @user.email %>, 239 + 240 + We received a request to reset your password. Visit the link below to choose a new one: 241 + 242 + <%= @reset_url %> 243 + 244 + This link will expire in 2 hours. 245 + 246 + If you didn't request this, you can safely ignore this email. 247 + ERB 248 + 249 + file 'app/views/user_mailer/email_confirmation.text.erb', <<~ERB 250 + Confirm Your Email 251 + 252 + Hi <%= @user.email %>, 253 + 254 + Please confirm your email address by visiting: 255 + 256 + <%= @confirm_url %> 257 + 258 + This link will expire in 24 hours. 259 + ERB 260 + 261 + say ' Creating email previews...', :cyan 262 + file 'test/mailers/previews/user_mailer_preview.rb', <<~RUBY 263 + # frozen_string_literal: true 264 + 265 + # Preview emails at http://localhost:3000/rails/mailers/user_mailer 266 + class UserMailerPreview < ActionMailer::Preview 267 + def welcome 268 + user = User.first || OpenStruct.new(email: 'user@example.com') 269 + UserMailer.welcome(user) 270 + end 271 + 272 + def password_reset 273 + user = User.first || OpenStruct.new(email: 'user@example.com') 274 + UserMailer.password_reset(user, 'sample_token_123') 275 + end 276 + 277 + def email_confirmation 278 + user = User.first || OpenStruct.new(email: 'user@example.com') 279 + UserMailer.email_confirmation(user, 'sample_token_123') 280 + end 281 + end 282 + RUBY 283 + 284 + say 'Email infrastructure configured!', :green 285 + say ' Preview emails at /rails/mailers in development', :cyan 286 + say ' Or use LetterOpener at /letter_opener', :cyan
+47 -1
modules/github.rb
··· 4 4 5 5 empty_directory '.github/workflows' 6 6 7 - # Migration index checker - ensures foreign keys are indexed 7 + say ' Adding migration index checker...', :cyan 8 8 file '.github/workflows/check-indexes.yml', <<~YAML 9 9 name: Check Indexes 10 10 on: ··· 22 22 23 23 - name: Check Migration Indexes 24 24 uses: speedshop/ids_must_be_indexed@v1.2.1 25 + YAML 26 + 27 + say ' Adding security scanning...', :cyan 28 + file '.github/workflows/security.yml', <<~YAML 29 + name: Security 30 + 31 + on: 32 + push: 33 + branches: [main] 34 + pull_request: 35 + branches: [main] 36 + schedule: 37 + - cron: '0 6 * * 1' # Weekly on Monday at 6am 38 + 39 + jobs: 40 + bundler-audit: 41 + name: Bundler Audit 42 + runs-on: ubuntu-latest 43 + steps: 44 + - uses: actions/checkout@v4 45 + 46 + - name: Set up Ruby 47 + uses: ruby/setup-ruby@v1 48 + with: 49 + bundler-cache: true 50 + 51 + - name: Run bundler-audit 52 + run: | 53 + gem install bundler-audit 54 + bundle audit check --update 55 + 56 + brakeman: 57 + name: Brakeman 58 + runs-on: ubuntu-latest 59 + steps: 60 + - uses: actions/checkout@v4 61 + 62 + - name: Set up Ruby 63 + uses: ruby/setup-ruby@v1 64 + with: 65 + bundler-cache: true 66 + 67 + - name: Run Brakeman 68 + run: | 69 + gem install brakeman 70 + brakeman -q --no-pager 25 71 YAML 26 72 27 73 say 'GitHub workflows configured!', :green
+113
modules/good_job.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up GoodJob for background processing...', :green 4 + 5 + gem 'good_job' 6 + 7 + say ' Configuring GoodJob...', :cyan 8 + initializer 'good_job.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + Rails.application.configure do 12 + # Use GoodJob as the Active Job adapter 13 + config.active_job.queue_adapter = :good_job 14 + 15 + config.good_job = { 16 + # Execution mode (:async, :external, :inline) 17 + # :async - runs jobs in the web process (good for low-volume) 18 + # :external - runs jobs in a separate process (recommended for production) 19 + # :inline - runs jobs immediately (good for testing) 20 + execution_mode: Rails.env.production? ? :external : :async, 21 + 22 + # Maximum threads per process 23 + max_threads: ENV.fetch('GOOD_JOB_MAX_THREADS', 5).to_i, 24 + 25 + # Poll interval for checking new jobs (in seconds) 26 + poll_interval: ENV.fetch('GOOD_JOB_POLL_INTERVAL', 5).to_i, 27 + 28 + # Queues to process (priority order) 29 + queues: ENV.fetch('GOOD_JOB_QUEUES', '*'), 30 + 31 + # Cron-like recurring jobs 32 + enable_cron: true, 33 + cron: { 34 + # Example: cleanup old sessions daily at 3am 35 + # cleanup_sessions: { 36 + # cron: '0 3 * * *', 37 + # class: 'CleanupSessionsJob', 38 + # description: 'Remove expired sessions' 39 + # }, 40 + 41 + # Example: send weekly digest every Monday at 9am 42 + # weekly_digest: { 43 + # cron: '0 9 * * 1', 44 + # class: 'WeeklyDigestJob', 45 + # description: 'Send weekly email digest' 46 + # } 47 + }, 48 + 49 + # Preserve job records for debugging (default: 14 days) 50 + preserve_job_records: true, 51 + cleanup_preserved_jobs_before_seconds_ago: 14.days.to_i, 52 + 53 + # Dashboard configuration 54 + smaller_number_is_higher_priority: true 55 + } 56 + end 57 + RUBY 58 + 59 + after_bundle do 60 + say ' Installing GoodJob...', :cyan 61 + rails_command 'good_job:install' 62 + 63 + say ' Adding GoodJob to Procfile.dev...', :cyan 64 + append_to_file 'Procfile.dev', "jobs: bundle exec good_job start\n" 65 + end 66 + 67 + say ' Creating example recurring job...', :cyan 68 + file 'app/jobs/cleanup_job.rb', <<~RUBY 69 + # frozen_string_literal: true 70 + 71 + # Example recurring job for cleanup tasks 72 + # Configure schedule in config/initializers/good_job.rb 73 + # 74 + # cron: { 75 + # cleanup: { 76 + # cron: '0 4 * * *', # 4am daily 77 + # class: 'CleanupJob' 78 + # } 79 + # } 80 + # 81 + class CleanupJob < ApplicationJob 82 + queue_as :low 83 + 84 + def perform 85 + cleanup_expired_sessions 86 + cleanup_old_versions 87 + cleanup_orphaned_records 88 + end 89 + 90 + private 91 + 92 + def cleanup_expired_sessions 93 + # Remove sessions older than 30 days 94 + Session.where('created_at < ?', 30.days.ago).delete_all 95 + end 96 + 97 + def cleanup_old_versions 98 + # Remove PaperTrail versions older than 90 days (if using paper_trail) 99 + return unless defined?(PaperTrail) 100 + 101 + PaperTrail::Version.where('created_at < ?', 90.days.ago).delete_all 102 + end 103 + 104 + def cleanup_orphaned_records 105 + # Add your cleanup logic here 106 + end 107 + end 108 + RUBY 109 + 110 + say 'GoodJob configured!', :green 111 + say ' Dashboard at /admin/jobs', :cyan 112 + say ' Run jobs: bundle exec good_job start', :cyan 113 + say ' Add cron jobs in config/initializers/good_job.rb', :cyan
+70
modules/logging.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up structured logging...', :green 4 + 5 + gem 'lograge' 6 + gem 'logstop' 7 + 8 + say ' Creating Lograge initializer...', :cyan 9 + initializer 'lograge.rb', <<~RUBY 10 + # frozen_string_literal: true 11 + 12 + return unless defined?(Lograge) 13 + 14 + Rails.application.configure do 15 + # Enable lograge for structured logging 16 + config.lograge.enabled = true 17 + 18 + # Use JSON format in production for log aggregation 19 + config.lograge.formatter = if Rails.env.production? 20 + Lograge::Formatters::Json.new 21 + else 22 + Lograge::Formatters::KeyValue.new 23 + end 24 + 25 + # Include request_id for tracing across services 26 + config.lograge.custom_options = lambda do |event| 27 + { 28 + request_id: event.payload[:request_id], 29 + user_id: event.payload[:user_id], 30 + ip: event.payload[:ip], 31 + host: event.payload[:host] 32 + }.compact 33 + end 34 + 35 + # Add custom data to the payload 36 + config.lograge.custom_payload do |controller| 37 + { 38 + request_id: controller.request.request_id, 39 + user_id: controller.try(:current_user)&.id, 40 + ip: controller.request.remote_ip, 41 + host: controller.request.host 42 + } 43 + end 44 + 45 + # Keep original Rails logs in development 46 + config.lograge.keep_original_rails_log = Rails.env.development? 47 + end 48 + RUBY 49 + 50 + say ' Creating Logstop initializer...', :cyan 51 + initializer 'logstop.rb', <<~RUBY 52 + # frozen_string_literal: true 53 + 54 + # Logstop filters sensitive data from logs 55 + # By default it filters: email, phone, credit card, SSN, IP addresses 56 + 57 + if defined?(Logstop) 58 + Logstop.guard(Rails.logger) 59 + 60 + # Add custom scrubbers for application-specific sensitive data 61 + # Logstop.scrub(pattern, replacement) 62 + # 63 + # Example: Scrub API keys 64 + # Logstop.scrub(/api_key=\\w+/, 'api_key=[FILTERED]') 65 + end 66 + RUBY 67 + 68 + say 'Structured logging configured!', :green 69 + say ' Logs include request_id for tracing', :cyan 70 + say ' PII is automatically filtered by Logstop', :cyan
+29 -19
modules/metrics.rb
··· 8 8 file 'config/initializers/statsd.rb', <<~RUBY 9 9 # frozen_string_literal: true 10 10 11 - StatsD.backend = if Rails.env.production? 12 - StatsD::Instrument::Backends::UDPBackend.new( 13 - ENV.fetch('STATSD_ADDR', 'localhost:8125'), 14 - :datadog 15 - ) 16 - else 17 - StatsD::Instrument::Backends::LoggerBackend.new(Rails.logger) 18 - end 11 + return unless defined?(StatsD) 19 12 20 - StatsD.prefix = Rails.application.class.module_parent_name.underscore 21 - StatsD.default_tags = [ 22 - "env:\#{Rails.env}", 23 - "app:\#{Rails.application.class.module_parent_name.underscore}" 24 - ] 13 + # Configure StatsD client 14 + # In production, point to your StatsD server (e.g., Datadog agent) 15 + # In development, logs to Rails logger 16 + StatsD.singleton_client = StatsD::Instrument::Client.new( 17 + sink: if Rails.env.production? 18 + StatsD::Instrument::UDPSink.new( 19 + ENV.fetch('STATSD_HOST', 'localhost'), 20 + ENV.fetch('STATSD_PORT', 8125).to_i 21 + ) 22 + else 23 + StatsD::Instrument::LogSink.new(Rails.logger) 24 + end, 25 + prefix: Rails.application.class.module_parent_name.underscore, 26 + default_tags: [ 27 + "env:\#{Rails.env}", 28 + "app:\#{Rails.application.class.module_parent_name.underscore}" 29 + ] 30 + ) 25 31 RUBY 26 32 27 33 say ' Creating Metrics module...', :cyan ··· 109 115 end 110 116 RUBY 111 117 112 - say ' Adding middleware to application...', :cyan 113 - inject_into_file 'config/application.rb', after: "class Application < Rails::Application\n" do 114 - " config.middleware.use RequestMetrics\n" 115 - end 118 + say ' Creating middleware initializer...', :cyan 119 + initializer 'request_metrics.rb', <<~RUBY 120 + # frozen_string_literal: true 121 + 122 + require Rails.root.join('app/middleware/request_metrics') 123 + 124 + Rails.application.config.middleware.use RequestMetrics 125 + RUBY 116 126 117 127 say ' Adding to .env.development...', :cyan 118 - append_to_file '.env.development', "STATSD_ADDR=localhost:8125\n" 128 + append_to_file '.env.development', "STATSD_HOST=localhost\nSTATSD_PORT=8125\n" 119 129 120 130 say 'StatsD metrics configured!', :green 121 131 say ' Use Metrics.increment, .gauge, .measure, .histogram', :cyan 122 132 say ' Request metrics automatically tracked', :cyan 123 - say ' Set STATSD_ADDR in production', :yellow 133 + say ' Set STATSD_HOST/STATSD_PORT in production', :yellow
+28
modules/pghero.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up PgHero...', :green 4 + 5 + gem 'pghero' 6 + 7 + say ' Configuring PgHero...', :cyan 8 + initializer 'pghero.rb', <<~RUBY 9 + # frozen_string_literal: true 10 + 11 + # PgHero - PostgreSQL insights 12 + # Dashboard: /admin/pghero 13 + 14 + ENV['PGHERO_USERNAME'] ||= Rails.application.credentials.dig(:pghero, :username) || 'admin' 15 + ENV['PGHERO_PASSWORD'] ||= Rails.application.credentials.dig(:pghero, :password) || 'admin' 16 + RUBY 17 + 18 + after_bundle do 19 + say ' Generating PgHero config...', :cyan 20 + rails_command 'generate pghero:config' 21 + 22 + say ' Generating PgHero query stats migration...', :cyan 23 + rails_command 'generate pghero:query_stats' 24 + end 25 + 26 + say 'PgHero configured!', :green 27 + say ' Dashboard will be mounted at /admin/pghero', :cyan 28 + say ' Enable pg_stat_statements in PostgreSQL for query insights', :yellow
+4
modules/pundit.rb
··· 116 116 user&.super_admin_or_above? 117 117 end 118 118 119 + def pghero? 120 + user&.admin_or_above? 121 + end 122 + 119 123 def access_admin_endpoints? 120 124 user&.admin_or_above? 121 125 end
+2 -4
modules/rails_performance.rb
··· 4 4 5 5 gem 'rails_performance' 6 6 7 - after_bundle do 8 - say ' Running Rails Performance installer...', :cyan 9 - rails_command 'rails_performance:install' 10 - end 7 + # Note: rails_performance doesn't have an install generator 8 + # We create the initializer manually below 11 9 12 10 say ' Creating Rails Performance initializer...', :cyan 13 11 file 'config/initializers/rails_performance.rb', <<~RUBY
+8 -9
modules/redis.rb
··· 18 18 # frozen_string_literal: true 19 19 20 20 # Use Redis for session storage 21 - Rails.application.config.session_store :redis_store, 22 - servers: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/2/session' }, 23 - expire_after: 90.minutes, 21 + Rails.application.config.session_store :redis_session_store, 24 22 key: "_\#{Rails.application.class.module_parent_name.underscore}_session", 25 - threadsafe: true, 26 - signed: true 23 + redis: { 24 + expire_after: 90.minutes, 25 + key_prefix: 'session:', 26 + url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/2' } 27 + } 27 28 RUBY 28 29 29 30 say ' Configuring Rack::Attack...', :cyan ··· 32 33 33 34 # Configure Rack Attack for rate limiting 34 35 class Rack::Attack 35 - # Use Redis for Rack Attack storage 36 - Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new( 37 - url: ENV.fetch('REDIS_URL') { 'redis://localhost:6379/5' } 38 - ) 36 + # Use Rails.cache (backed by Redis in production) 37 + Rack::Attack.cache.store = Rails.cache 39 38 40 39 # Throttle all requests by IP (300 req/5 minutes) 41 40 throttle('req/ip', limit: 300, period: 5.minutes) do |req|
+116
modules/seo.rb
··· 1 + # frozen_string_literal: true 2 + 3 + say 'Setting up SEO & navigation helpers...', :green 4 + 5 + gem 'meta-tags' 6 + gem 'active_link_to' 7 + 8 + say ' Configuring meta-tags...', :cyan 9 + initializer 'meta_tags.rb', <<~RUBY 10 + # frozen_string_literal: true 11 + 12 + MetaTags.configure do |config| 13 + # How many characters to truncate title to 14 + config.title_limit = 70 15 + 16 + # How many characters to truncate description to 17 + config.description_limit = 160 18 + 19 + # Truncate site_title if title is too long 20 + config.truncate_site_title_first = false 21 + end 22 + RUBY 23 + 24 + say ' Adding meta tags to layout...', :cyan 25 + inject_into_file 'app/views/layouts/application.html.erb', after: "<%= csp_meta_tag %>\n" do 26 + " <%= display_meta_tags site: '#{app_name.titleize}' %>\n" 27 + end 28 + 29 + say ' Creating SEO helper...', :cyan 30 + file 'app/helpers/seo_helper.rb', <<~RUBY 31 + # frozen_string_literal: true 32 + 33 + # Usage in controllers or views: 34 + # set_meta_tags title: 'Page Title', 35 + # description: 'Page description', 36 + # keywords: 'keyword1, keyword2' 37 + # 38 + # Or use the helpers: 39 + # page_title 'My Page' 40 + # page_description 'Description here' 41 + # 42 + module SeoHelper 43 + def page_title(title) 44 + set_meta_tags title: title 45 + end 46 + 47 + def page_description(description) 48 + set_meta_tags description: description 49 + end 50 + 51 + def page_image(image_url) 52 + set_meta_tags og: { image: image_url }, 53 + twitter: { image: image_url } 54 + end 55 + 56 + # Full SEO setup for a page 57 + def page_seo(title:, description: nil, image: nil, type: 'website') 58 + tags = { 59 + title: title, 60 + og: { title: title, type: type }, 61 + twitter: { card: 'summary_large_image', title: title } 62 + } 63 + 64 + if description 65 + tags[:description] = description 66 + tags[:og][:description] = description 67 + tags[:twitter][:description] = description 68 + end 69 + 70 + if image 71 + tags[:og][:image] = image 72 + tags[:twitter][:image] = image 73 + end 74 + 75 + set_meta_tags tags 76 + end 77 + end 78 + RUBY 79 + 80 + say ' Creating navigation helper...', :cyan 81 + file 'app/helpers/navigation_helper.rb', <<~RUBY 82 + # frozen_string_literal: true 83 + 84 + # Navigation helpers using active_link_to 85 + # 86 + # Usage: 87 + # <%= nav_link_to 'Home', root_path %> 88 + # <%= nav_link_to 'Users', users_path, class: 'nav-item' %> 89 + # 90 + # The link gets an 'active' class when on that page. 91 + # Customize with active_link_to options: 92 + # active: :exact # Only exact URL match 93 + # active: :inclusive # Match URL and sub-paths (default) 94 + # active: /regex/ # Match against regex 95 + # active: ['path1', 'path2'] # Match multiple paths 96 + # 97 + module NavigationHelper 98 + def nav_link_to(name, path, options = {}) 99 + default_options = { 100 + class: 'nav-link', 101 + active_class: 'active' 102 + } 103 + active_link_to(name, path, default_options.merge(options)) 104 + end 105 + 106 + # For nav items with icons 107 + def nav_link_with_icon(name, icon_class, path, options = {}) 108 + content = tag.i(class: icon_class) + ' ' + name 109 + nav_link_to(content, path, options) 110 + end 111 + end 112 + RUBY 113 + 114 + say 'SEO & navigation configured!', :green 115 + say ' Use `page_seo title: "..."` in controllers', :cyan 116 + say ' Use `nav_link_to` for auto-highlighting nav links', :cyan
-56
modules/solid_queue.rb
··· 1 - # frozen_string_literal: true 2 - 3 - say 'Setting up Solid Queue for background jobs...', :green 4 - 5 - gem 'solid_queue' 6 - gem 'mission_control-jobs' 7 - 8 - after_bundle do 9 - say ' Running Solid Queue installer...', :cyan 10 - rails_command 'generate solid_queue:install' 11 - 12 - say ' Creating queue database...', :cyan 13 - rails_command 'db:create:queue' 14 - 15 - say ' Loading Solid Queue schema...', :cyan 16 - rails_command 'db:schema:load:queue' 17 - end 18 - 19 - say ' Creating Solid Queue initializer...', :cyan 20 - file 'config/initializers/solid_queue.rb', <<~RUBY 21 - # frozen_string_literal: true 22 - 23 - # Solid Queue Configuration 24 - # https://github.com/rails/solid_queue 25 - # 26 - # Database-backed Active Job backend that uses PostgreSQL for job storage. 27 - # No need for Redis or external dependencies. 28 - 29 - Rails.application.config.solid_queue.tap do |config| 30 - # Silence polling queries in logs 31 - config.silence_polling = true 32 - 33 - # How often to check for new jobs (default: 0.1 seconds) 34 - # config.polling_interval = 0.1 35 - 36 - # Number of threads per worker (default: 3) 37 - # config.workers_per_process = 3 38 - 39 - # Concurrency per queue 40 - # config.concurrency_maintenance_interval = 600 41 - end 42 - RUBY 43 - 44 - say ' Configuring Active Job to use Solid Queue...', :cyan 45 - inject_into_file 'config/application.rb', after: "class Application < Rails::Application\n" do 46 - " config.active_job.queue_adapter = :solid_queue\n" 47 - end 48 - 49 - say ' Creating Procfile.dev entry...', :cyan 50 - append_to_file 'Procfile.dev', "jobs: bundle exec rake solid_queue:start\n" 51 - 52 - say 'Solid Queue configured!', :green 53 - say ' Dashboard at /admin/jobs (admin only)', :cyan 54 - say ' Jobs will be processed via: bin/jobs or rake solid_queue:start', :cyan 55 - say ' Run migrations: rails db:migrate', :yellow 56 - say ' Configure queues in config/solid_queue.yml', :cyan
+21 -4
template.rb
··· 21 21 end 22 22 23 23 # ═══════════════════════════════════════════════════════════════════════════════ 24 + # Remove Rails 8 defaults we'll replace 25 + # ═══════════════════════════════════════════════════════════════════════════════ 26 + 27 + remove_file 'config/initializers/content_security_policy.rb' 28 + 29 + # ═══════════════════════════════════════════════════════════════════════════════ 24 30 # Base Setup 25 31 # ═══════════════════════════════════════════════════════════════════════════════ 26 32 ··· 28 34 apply_module('credentials') 29 35 apply_module('development_tools') 30 36 apply_module('github') 37 + apply_module('logging') 31 38 32 39 # ═══════════════════════════════════════════════════════════════════════════════ 33 40 # Database & Infrastructure ··· 35 42 36 43 apply_module('database') 37 44 apply_module('redis') 45 + apply_module('caching') 46 + 47 + # ═══════════════════════════════════════════════════════════════════════════════ 48 + # Security 49 + # ═══════════════════════════════════════════════════════════════════════════════ 50 + 51 + apply_module('security') 52 + apply_module('csp') 38 53 39 54 # ═══════════════════════════════════════════════════════════════════════════════ 40 55 # Authentication & Authorization ··· 43 58 apply_module('public_identifiable') 44 59 apply_module('auth', ['run `rails db:migrate`']) 45 60 apply_module('pundit') 46 - apply_module('security') 47 61 48 62 # ═══════════════════════════════════════════════════════════════════════════════ 49 - # Frontend 63 + # Frontend & SEO 50 64 # ═══════════════════════════════════════════════════════════════════════════════ 51 65 52 66 gem 'tailwindcss-rails' 53 67 apply_module('tailwind') 68 + apply_module('seo') 54 69 55 70 # ═══════════════════════════════════════════════════════════════════════════════ 56 71 # Background Jobs & Feature Flags 57 72 # ═══════════════════════════════════════════════════════════════════════════════ 58 73 59 - apply_module('solid_queue') 74 + apply_module('good_job') 60 75 apply_module('flipper') 61 76 62 77 # ═══════════════════════════════════════════════════════════════════════════════ ··· 66 81 apply_module('blazer') 67 82 apply_module('rails_performance') 68 83 apply_module('console1984') 84 + apply_module('pghero') 69 85 70 86 # ═══════════════════════════════════════════════════════════════════════════════ 71 87 # Monitoring & Analytics ··· 86 102 apply_module('aasm') 87 103 88 104 # ═══════════════════════════════════════════════════════════════════════════════ 89 - # Pagination & Email 105 + # Email & Pagination 90 106 # ═══════════════════════════════════════════════════════════════════════════════ 91 107 92 108 apply_module('kaminari') 93 109 apply_module('mailkick') 110 + apply_module('email') 94 111 95 112 # ═══════════════════════════════════════════════════════════════════════════════ 96 113 # Admin Routes (must be last - depends on all admin modules)