@@ -19,21 +19,100 @@ Rails.application.routes.draw do
1919end
2020```
2121
22- ** Multi-tenancy** via URL (not subdomain):
22+ ** Verb-to-noun conversion:**
23+ | Action | Resource |
24+ | --------| ----------|
25+ | close a card | ` card.closure ` |
26+ | watch a board | ` board.watching ` |
27+ | mark as golden | ` card.goldness ` |
28+ | archive a card | ` card.archival ` |
29+
30+ ** Shallow nesting** - avoid deep URLs:
2331``` ruby
24- # /{account_id}/boards/...
25- scope " /:account_id" do
26- resources :boards
32+ resources :boards do
33+ resources :cards , shallow: true # /boards/:id/cards, but /cards/:id
2734end
2835```
2936
30- Benefits:
31- - No subdomain DNS complexity
32- - Deep links work naturally
33- - Middleware extracts account_id, moves to SCRIPT_NAME
34- - ` Current.account ` available everywhere
37+ ** Singular resources** for one-per-parent:
38+ ``` ruby
39+ resource :closure # not resources
40+ resource :goldness
41+ ```
42+
43+ ** Resolve for URL generation:**
44+ ``` ruby
45+ # config/routes.rb
46+ resolve(" Comment" ) { |comment | [comment.card, anchor: dom_id(comment)] }
47+
48+ # Now url_for(@comment) works correctly
49+ ```
3550</routing >
3651
52+ <multi_tenancy>
53+ ## Multi-Tenancy (Path-Based)
54+
55+ ** Middleware extracts tenant** from URL prefix:
56+
57+ ``` ruby
58+ # lib/tenant_extractor.rb
59+ class TenantExtractor
60+ def initialize (app )
61+ @app = app
62+ end
63+
64+ def call (env )
65+ path = env[" PATH_INFO" ]
66+ if match = path.match(%r{^/(\d +)(/.*) ?$} )
67+ env[" SCRIPT_NAME" ] = " /#{ match[1 ] } "
68+ env[" PATH_INFO" ] = match[2 ] || " /"
69+ end
70+ @app .call(env)
71+ end
72+ end
73+ ```
74+
75+ ** Cookie scoping** per tenant:
76+ ``` ruby
77+ # Cookies scoped to tenant path
78+ cookies.signed[:session_id ] = {
79+ value: session.id,
80+ path: " /#{ Current .account.id} "
81+ }
82+ ```
83+
84+ ** Background job context** - serialize tenant:
85+ ``` ruby
86+ class ApplicationJob < ActiveJob ::Base
87+ around_perform do |job , block |
88+ Current .set(account: job.arguments.first.account) { block.call }
89+ end
90+ end
91+ ```
92+
93+ ** Recurring jobs** must iterate all tenants:
94+ ``` ruby
95+ class DailyDigestJob < ApplicationJob
96+ def perform
97+ Account .find_each do |account |
98+ Current .set(account: account) do
99+ send_digest_for(account)
100+ end
101+ end
102+ end
103+ end
104+ ```
105+
106+ ** Controller security** - always scope through tenant:
107+ ``` ruby
108+ # Good - scoped through user's accessible records
109+ @card = Current .user.accessible_cards.find(params[:id ])
110+
111+ # Avoid - direct lookup
112+ @card = Card .find(params[:id ])
113+ ```
114+ </multi_tenancy>
115+
37116<authentication >
38117## Authentication
39118
130209- No Redis required
131210- Same transactional guarantees as your data
132211- Simpler infrastructure
212+
213+ ** Transaction safety:**
214+ ``` ruby
215+ # config/application.rb
216+ config.active_job.enqueue_after_transaction_commit = true
217+ ```
218+
219+ ** Error handling** by type:
220+ ``` ruby
221+ class DeliveryJob < ApplicationJob
222+ # Transient errors - retry with backoff
223+ retry_on Net ::OpenTimeout , Net ::ReadTimeout ,
224+ Resolv ::ResolvError ,
225+ wait: :polynomially_longer
226+
227+ # Permanent errors - log and discard
228+ discard_on Net ::SMTPSyntaxError do |job , error |
229+ Sentry .capture_exception(error, level: :info )
230+ end
231+ end
232+ ```
233+
234+ ** Batch processing** with continuable:
235+ ``` ruby
236+ class ProcessCardsJob < ApplicationJob
237+ include ActiveJob ::Continuable
238+
239+ def perform
240+ Card .in_batches.each_record do |card |
241+ checkpoint! # Resume from here if interrupted
242+ process(card)
243+ end
244+ end
245+ end
246+ ```
133247</background_jobs>
134248
249+ <database_patterns>
250+ ## Database Patterns
251+
252+ ** UUIDs as primary keys** (time-sortable UUIDv7):
253+ ``` ruby
254+ # migration
255+ create_table :cards , id: :uuid do |t |
256+ t.references :board , type: :uuid , foreign_key: true
257+ end
258+ ```
259+
260+ Benefits: No ID enumeration, distributed-friendly, client-side generation.
261+
262+ ** State as records** (not booleans):
263+ ``` ruby
264+ # Instead of closed: boolean
265+ class Card ::Closure < ApplicationRecord
266+ belongs_to :card
267+ belongs_to :creator , class_name: " User"
268+ end
269+
270+ # Queries become joins
271+ Card .joins(:closure ) # closed
272+ Card .where.missing(:closure ) # open
273+ ```
274+
275+ ** Hard deletes** - no soft delete:
276+ ``` ruby
277+ # Just destroy
278+ card.destroy!
279+
280+ # Use events for history
281+ card.record_event(:deleted , by: Current .user)
282+ ```
283+
284+ Simplifies queries, uses event logs for auditing.
285+
286+ ** Counter caches** for performance:
287+ ``` ruby
288+ class Comment < ApplicationRecord
289+ belongs_to :card , counter_cache: true
290+ end
291+
292+ # card.comments_count available without query
293+ ```
294+
295+ ** Account scoping** on every table:
296+ ``` ruby
297+ class Card < ApplicationRecord
298+ belongs_to :account
299+ default_scope { where(account: Current .account) }
300+ end
301+ ```
302+ </database_patterns>
303+
135304<current_attributes>
136305## Current Attributes
137306
@@ -339,3 +508,146 @@ end
339508
340509** Webhooks driven by events** - events are the canonical source.
341510</events >
511+
512+ <email_patterns>
513+ ## Email Patterns
514+
515+ ** Multi-tenant URL helpers:**
516+ ``` ruby
517+ class ApplicationMailer < ActionMailer ::Base
518+ def default_url_options
519+ options = super
520+ if Current .account
521+ options[:script_name ] = " /#{ Current .account.id} "
522+ end
523+ options
524+ end
525+ end
526+ ```
527+
528+ ** Timezone-aware delivery:**
529+ ``` ruby
530+ class NotificationMailer < ApplicationMailer
531+ def daily_digest (user )
532+ Time .use_zone(user.timezone) do
533+ @user = user
534+ @digest = user.digest_for_today
535+ mail(to: user.email, subject: " Daily Digest" )
536+ end
537+ end
538+ end
539+ ```
540+
541+ ** Batch delivery:**
542+ ``` ruby
543+ emails = users.map { |user | NotificationMailer .digest(user) }
544+ ActiveJob .perform_all_later(emails.map(& :deliver_later ))
545+ ```
546+
547+ ** One-click unsubscribe (RFC 8058):**
548+ ``` ruby
549+ class ApplicationMailer < ActionMailer ::Base
550+ after_action :set_unsubscribe_headers
551+
552+ private
553+ def set_unsubscribe_headers
554+ headers[" List-Unsubscribe-Post" ] = " List-Unsubscribe=One-Click"
555+ headers[" List-Unsubscribe" ] = " <#{ unsubscribe_url } >"
556+ end
557+ end
558+ ```
559+ </email_patterns>
560+
561+ <security_patterns>
562+ ## Security Patterns
563+
564+ ** XSS prevention** - escape in helpers:
565+ ``` ruby
566+ def formatted_content (text )
567+ # Escape first, then mark safe
568+ simple_format(h(text)).html_safe
569+ end
570+ ```
571+
572+ ** SSRF protection:**
573+ ``` ruby
574+ # Resolve DNS once, pin the IP
575+ def fetch_safely (url )
576+ uri = URI .parse(url)
577+ ip = Resolv .getaddress(uri.host)
578+
579+ # Block private networks
580+ raise " Private IP" if private_ip?(ip)
581+
582+ # Use pinned IP for request
583+ Net ::HTTP .start(uri.host, uri.port, ipaddr: ip) { |http | ... }
584+ end
585+
586+ def private_ip? (ip )
587+ ip.start_with?(" 127." , " 10." , " 192.168." ) ||
588+ ip.match?(/^172\. (1[6-9] |2[0-9] |3[0-1] ) \. / )
589+ end
590+ ```
591+
592+ ** Content Security Policy:**
593+ ``` ruby
594+ # config/initializers/content_security_policy.rb
595+ Rails .application.configure do
596+ config.content_security_policy do |policy |
597+ policy.default_src :self
598+ policy.script_src :self
599+ policy.style_src :self , :unsafe_inline
600+ policy.base_uri :none
601+ policy.form_action :self
602+ policy.frame_ancestors :self
603+ end
604+ end
605+ ```
606+
607+ ** ActionText sanitization:**
608+ ``` ruby
609+ # config/initializers/action_text.rb
610+ Rails .application.config.after_initialize do
611+ ActionText ::ContentHelper .allowed_tags = %w[
612+ strong em a ul ol li p br h1 h2 h3 h4 blockquote
613+ ]
614+ end
615+ ```
616+ </security_patterns>
617+
618+ <active_storage>
619+ ## Active Storage Patterns
620+
621+ ** Variant preprocessing:**
622+ ``` ruby
623+ class User < ApplicationRecord
624+ has_one_attached :avatar do |attachable |
625+ attachable.variant :thumb , resize_to_limit: [100 , 100 ], preprocessed: true
626+ attachable.variant :medium , resize_to_limit: [300 , 300 ], preprocessed: true
627+ end
628+ end
629+ ```
630+
631+ ** Direct upload expiry** - extend for slow connections:
632+ ``` ruby
633+ # config/initializers/active_storage.rb
634+ Rails .application.config.active_storage.service_urls_expire_in = 48 .hours
635+ ```
636+
637+ ** Avatar optimization** - redirect to blob:
638+ ``` ruby
639+ def show
640+ expires_in 1 .year, public: true
641+ redirect_to @user .avatar.variant(:thumb ).processed.url, allow_other_host: true
642+ end
643+ ```
644+
645+ ** Mirror service** for migrations:
646+ ``` yaml
647+ # config/storage.yml
648+ production :
649+ service : Mirror
650+ primary : amazon
651+ mirrors : [google]
652+ ` ` `
653+ </active_storage>
0 commit comments