# Architecture - DHH Rails Style ## Routing Everything maps to CRUD. Nested resources for related actions: ```ruby Rails.application.routes.draw do resources :boards do resources :cards do resource :closure resource :goldness resource :not_now resources :assignments resources :comments end end end ``` **Multi-tenancy** via URL (not subdomain): ```ruby # /{account_id}/boards/... scope "/:account_id" do resources :boards end ``` Benefits: - No subdomain DNS complexity - Deep links work naturally - Middleware extracts account_id, moves to SCRIPT_NAME - `Current.account` available everywhere ## Authentication Custom passwordless magic link auth (~150 lines total): ```ruby # app/models/session.rb class Session < ApplicationRecord belongs_to :user before_create { self.token = SecureRandom.urlsafe_base64(32) } end # app/models/magic_link.rb class MagicLink < ApplicationRecord belongs_to :user before_create do self.code = SecureRandom.random_number(100_000..999_999).to_s self.expires_at = 15.minutes.from_now end def expired? expires_at < Time.current end end ``` **Why not Devise:** - ~150 lines vs massive dependency - No password storage liability - Simpler UX for users - Full control over flow **Bearer token** for APIs: ```ruby module Authentication extend ActiveSupport::Concern included do before_action :authenticate end private def authenticate if bearer_token = request.headers["Authorization"]&.split(" ")&.last Current.session = Session.find_by(token: bearer_token) else Current.session = Session.find_by(id: cookies.signed[:session_id]) end redirect_to login_path unless Current.session end end ``` ## Background Jobs Jobs are shallow wrappers calling model methods: ```ruby class NotifyWatchersJob < ApplicationJob def perform(card) card.notify_watchers end end ``` **Naming convention:** - `_later` suffix for async: `card.notify_watchers_later` - `_now` suffix for immediate: `card.notify_watchers_now` ```ruby module Watchable def notify_watchers_later NotifyWatchersJob.perform_later(self) end def notify_watchers_now NotifyWatchersJob.perform_now(self) end def notify_watchers watchers.each do |watcher| WatcherMailer.notification(watcher, self).deliver_later end end end ``` **Database-backed** with Solid Queue: - No Redis required - Same transactional guarantees as your data - Simpler infrastructure ## Current Attributes Use `Current` for request-scoped state: ```ruby # app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :session, :user, :account, :request_id delegate :user, to: :session, allow_nil: true def account=(account) super Time.zone = account&.time_zone || "UTC" end end ``` Set in controller: ```ruby class ApplicationController < ActionController::Base before_action :set_current_request private def set_current_request Current.session = authenticated_session Current.account = Account.find(params[:account_id]) Current.request_id = request.request_id end end ``` Use throughout app: ```ruby class Card < ApplicationRecord belongs_to :creator, default: -> { Current.user } end ``` ## Caching **HTTP caching** with ETags: ```ruby fresh_when etag: [@card, Current.user.timezone] ``` **Fragment caching:** ```erb <% cache card do %> <%= render card %> <% end %> ``` **Russian doll caching:** ```erb <% cache @board do %> <% @board.cards.each do |card| %> <% cache card do %> <%= render card %> <% end %> <% end %> <% end %> ``` **Cache invalidation** via `touch: true`: ```ruby class Card < ApplicationRecord belongs_to :board, touch: true end ``` **Solid Cache** - database-backed: - No Redis required - Consistent with application data - Simpler infrastructure ## Configuration **ENV.fetch with defaults:** ```ruby # config/application.rb config.active_job.queue_adapter = ENV.fetch("QUEUE_ADAPTER", "solid_queue").to_sym config.cache_store = ENV.fetch("CACHE_STORE", "solid_cache").to_sym ``` **Multiple databases:** ```yaml # config/database.yml production: primary: <<: *default cable: <<: *default migrations_paths: db/cable_migrate queue: <<: *default migrations_paths: db/queue_migrate cache: <<: *default migrations_paths: db/cache_migrate ``` **Switch between SQLite and MySQL via ENV:** ```ruby adapter = ENV.fetch("DATABASE_ADAPTER", "sqlite3") ``` **CSP extensible via ENV:** ```ruby config.content_security_policy do |policy| policy.default_src :self policy.script_src :self, *ENV.fetch("CSP_SCRIPT_SRC", "").split(",") end ``` ## Testing **Minitest**, not RSpec: ```ruby class CardTest < ActiveSupport::TestCase test "closing a card creates a closure" do card = cards(:one) card.close assert card.closed? assert_not_nil card.closure end end ``` **Fixtures** instead of factories: ```yaml # test/fixtures/cards.yml one: title: First Card board: main creator: alice two: title: Second Card board: main creator: bob ``` **Integration tests** for controllers: ```ruby class CardsControllerTest < ActionDispatch::IntegrationTest test "closing a card" do card = cards(:one) sign_in users(:alice) post card_closure_path(card) assert_response :success assert card.reload.closed? end end ``` **Tests ship with features** - same commit, not TDD-first but together. **Regression tests for security fixes** - always. ## Event Tracking Events are the single source of truth: ```ruby class Event < ApplicationRecord belongs_to :creator, class_name: "User" belongs_to :eventable, polymorphic: true serialize :particulars, coder: JSON end ``` **Eventable concern:** ```ruby module Eventable extend ActiveSupport::Concern included do has_many :events, as: :eventable, dependent: :destroy end def record_event(action, particulars = {}) events.create!( creator: Current.user, action: action, particulars: particulars ) end end ``` **Webhooks driven by events** - events are the canonical source.