Sifting through Fizzy’s pull requests, it was fascinating to see how the 37signals team thinks through problems, builds solutions, and ships software together. Outside of their massive contributions to open source libraries, the podcast and the books — this was a chance to see up close, with real code, the application building process.
Below you will read which 37signals team members owned which domains in this project, and which PRs best demonstrate their craft. If you’re new to the codebase (or want to level up in specific areas), I will point you to the PRs worth studying. Each section highlights an engineer’s domain knowledge and the code reviews that showcase their thinking.
Why Read Pull Requests?
Reading the code shows the outcome. Pull requests show the decisions.
I love reading PRs because they reveal the reasoning behind the work, the options considered and the tradeoffs made. Review comments often become real lessons as people question choices, suggest alternatives, and offer encouragement.
These reviews can create quick, informal mentorship where one person spots a cleaner abstraction and another catches an edge case before it slips through.
Follow a codebase through its PRs and you can see the product being built in real time as early spikes set patterns, refactors clear debt, and new features rise on earlier work.
Want to dive into a specific topic? Jump to the Learning Paths for curated PR sequences on AI/LLM integration, multi-tenancy with SQLite, Turbo/Hotwire, and more.
DHH (David Heinemeier Hansson)
Expertise: Rails Architecture, Code Style, API Design
DHH’s code reviews are masterclasses in Rails conventions. His 20 PRs and numerous reviews establish patterns the whole team follows. I believe this is the first publicly viewable application codebase where we see DHH doing code reviews — well worth the time to dig into those and see his thinking for yourself.
Must-Read PRs
PR #108 - Spike events system (reviewed)
This PR establishes core architectural patterns. Key feedback:
On delegated types and pagination:
“These threads need to be paginated, so you can’t do any in-memory sorting. This all needs to be converted to a delegated type, so you have a single table you can pull from.”
On lazy loading:
“Why not just delegate :user to :session? Then you get to lazy load it too.”
On naming conventions:
“
not_poppedis pretty cumbersome of a word. Consider something likeunpoppedif staying in the negative or go with something likeactive.”
On test-induced design damage:
“I think that would then qualify as test-induced design damage. Better replace that with a mock or even better just a fixture session you can use. We should never let our desire for ease of testing bleed into the application itself.”
PR #120 - Use the new params#expect syntax
Demonstrates DHH’s preference for Rails’ built-in features:
“To ensure we don’t get spurious 500s when we can serve 400s on bad data.”
PR #119 - Start adding caching
Foundation for Fizzy’s caching strategy.
PR #425 - Plain text mentions (reviewed)
Key feedback on code style:
On method naming:
“
collectimplies that we’re returning an array of mentions (as #collect). Would usecreate_mentionswhen you don’t care about the return value.”
On Rails built-ins:
“You can use
after_save_commitinstead ofafter_commit on: %i[ create update ].”
On API design:
“Bit too heavy-handed, imo. Better to make action return a StringInquirer. Then you can do
event.action.completed?.”
DHH’s Style Patterns
- Prefer delegation over accessor methods
- Use Rails’ built-in features (StringInquirer, counter caches,
after_save_commit) - Name methods by what they do, not what they return
- Avoid test-induced design damage
- Extract explaining methods for complex logic
Jorge Manrubia
Expertise: Turbo/Hotwire, Caching, AI/LLM Integration, ActiveRecord Patterns
With 285 PRs, Jorge is the most prolific backend contributor. His deep knowledge spans Hotwire, caching strategies, and AI integration.
Must-Read PRs
PR #339 - Enable page refreshes with morphing
Introduction of Turbo morphing for smoother updates:
“This makes the update actions on cards feel smoother.”
PR #483 - Fizzy Do with AI
A complete guide to integrating LLMs in Rails:
“Adds LLM processing to the Fizzy Do bar, enabling:
- Filtering cards using natural language. E.g:
cards assigned to jz.- Act on cards using natural language: E.g:
close cards assigned to jz.- Get insight on cards: E.g:
summarize this card.”
Key implementation details:
- Using
ruby_llmgem for model flexibility - Translating natural language to commands via structured output
- Vector embeddings with SQLite-vec extension
- Token limit handling
PR #1052 - Add more caching
HTTP caching and server-side template caching patterns:
“When rendering a day timeline, we:
- HTTP cache the full request.
- Cache the filter menu.
- Cache the columns of events (so this could be shared across users who see the same set of events).
We leave out things like the day header, since this can vary on a per-user basis (e.g: timezones)”
PR #929 - Conversation cost limits (reviewed)
Jorge’s review demonstrates advanced patterns:
On extracting models:
“The shared param is often a smell that something is missing. My mind goes to having a new record
Ai::Quotaso that each user has a quota. The quota has an amount that gets reset weekly in a cron job.”
On custom ActiveRecord types:
“If you wanted to make an AR attribute, you can use a custom Active Model/Record type; there is first-class support for it. You can define a type, register it, and then declare attributes with it.”
On the .wrap convention:
“We have the internal in-house convention of using a static method
.wrapwhen you want to get an object from several different types values.”
Jorge’s Patterns
- Memoize expensive computations called multiple times per render
- Use transient filters for temporary state
- Consider cache keys carefully (user-specific vs. shared)
- Extract concerns when responsibilities become clear
- Use Stimulus values API over arbitrary data attributes
Mike Dalessio
Expertise: Infrastructure, Solid Queue, Dependencies, AI/Parsing, DevOps
With 317 PRs, flavorjones handles the infrastructure backbone: dependencies, background jobs, deployment, and parsing.
Must-Read PRs
PR #457 - Recursive descent parser for Fizzy Do
Hand-written parser design:
“Using a hand-written recursive descent parser instead of a LALR parser generator (like lex/yacc or rex/racc) because IMHO it’s easier to understand, extend, and debug. It also allows us to be a bit more error-tolerant where we need to.”
“The AST I’m introducing here is pretty simple, and I’m not tracking token location because my assumption is that the parsed strings are all going to be short.”
PR #159 - Introduce tenanting
Multi-tenant SQLite architecture with comprehensive documentation:
“This PR does a few notable things to implement a tenanted sqlite3 database for Fizzy:
- Introduce a
Tenantmodel in a non-tenanted secondary database- Tenant the ApplicationRecord models using active_record-tenanted
- Introduce middleware for selecting the tenant based on subdomain
- Extend the ‘first run’ flow to include a subdomain field
- Modify
TestCaseto handle parallel testing”
Design decisions documented:
- Subdomain vs URL path tenanting
- Slug separate from database name for changeability
- SecureRandom for permanent database file names
PR #1109 - Yabeda for Solid Queue metrics
Monitoring background job infrastructure.
PR #501 - Stimulus controller for bubbles
Learning Stimulus through code review. Jorge’s feedback:
“Consider using Stimulus values instead of arbitrary data properties. It’s not a life-changing improvement, but nice to use what’s available and the API is nice. You would be able to do
this.closeAtValueto read the attribute.”
“In javascript, our convention is
closesAtinstead ofcloses_at.”
flavorjones’s Patterns
- Structured logging with tenant context
- Careful dependency management (Rails edge testing)
- Comprehensive test helpers for complex infrastructure
- VCR cassettes for AI/external API testing
- Clear PR descriptions explaining design decisions
Jason Zimdars
Expertise: UI/UX Design, CSS Architecture, ERB Templates, Filtering Systems
217 PRs focused on design implementation. JZ bridges design and code.
Must-Read PRs
- PR #305 - New visual design - Complete UI overhaul that defined Fizzy’s visual language
- PR #131 - New filtering UI - Filter interface patterns
- PR #335 - Two column Collection design - Layout architecture decisions
- PR #265 - Quick filters - User-facing filter shortcuts
- PR #608 - Add multiple cards quickly - Rapid input UX patterns
JZ’s Patterns
- CSS file structure by component
- Visual documentation via screenshots in PRs
For more on Fizzy’s CSS approach, see my post Vanilla CSS Is All You Need.
Stanko Krtalic Rusendic
Expertise: Webhooks, Authentication, Security, External Integrations
76 PRs with deep focus on external system integration and security.
Must-Read PRs
PR #1083 - Webhooks
Comprehensive webhook implementation:
“Webhooks hook into the events model. For every event that’s created a WebhookDispatch job is spawned that checks which webhooks should get triggered.”
Security mechanisms:
- HMAC SHA256 signing for verification
- Timestamps to prevent replay attacks
- URL resolution checking for loopback/private networks (SSRF protection)
- Delinquent webhook auto-deactivation
“I used the limits concurrency method here to preserve causality as much as possible. It’s not a guarantee that the hooks will get delivered in order, but it helps preserve the order in most cases.”
PR #929 - Conversation cost limits
AI quota management with Money value object:
“We still aren’t sure how much Fizzy Ask is going to cost us, so to keep cost under control I’ve added a cost limit per conversation.”
Implementation details:
- Quota tracking per user
- Weekly reset periods
- Money object for microcent calculations
- Clear error messaging for users
PR #1304 - Magic links
Passwordless authentication with security considerations. Key discussion from flavorjones:
“One thing I’m nervous about here is the loss of authentication per account. It’s possible that an Identity (a person) would have multiple accounts under multiple email addresses… compromising one of those email addresses exposes all of those Fizzy accounts.”
monorkin’s Patterns
- Security-first design (SSRF checks, signing, rate limits)
- Detailed PR descriptions with implementation rationale
- State machine patterns for deliveries (pending, in_progress, completed, errored)
- Active Job Continuations for reliable delivery
Andy Rankin
Expertise: CSS/Styling, Stimulus Controllers, UI Components, Accessibility
With 268 PRs, Andy is the third most prolific contributor, focused entirely on frontend implementation.
Must-Read PRs
- PR #962 - Combobox a11y - Accessibility patterns for complex components
- PR #920 - Popup orientation - Positioning logic for popups
- PR #1199 - Column animation - CSS animation patterns
- PR #1187 - Save expanded state to localStorage - Client-side state persistence
- PR #836 - Lightbox improvements - Image viewing UX
- PR #379 - Btn dropdown - Dropdown component patterns
Andy’s Patterns
- Component-focused CSS organization
- Accessibility-first approach
- localStorage for UI state persistence
- Stimulus controllers for interactive behavior
Other Key Contributors
Jose Farias
Expertise: Filtering Systems, Form Handling, Early Architecture
23 PRs establishing core patterns in Fizzy’s early development.
- PR #113 - Persisted filters - Filter state management
- PR #124 - Build threads from a single table - Delegated types implementation
- PR #135 - Counter cache for comments - ActiveRecord counter cache patterns
Kevin McConnell
Expertise: Infrastructure, Notifications, Deployment, Performance
49 PRs focused on operational concerns.
- PR #199 - Notification spike - Notification system foundation
- PR #274 - Notification preferences - User preference architecture
- PR #1377 - Extend public caching to 30 seconds - HTTP caching for CDN optimization
Jeremy Daer
Expertise: Security, Ruby Infrastructure, DevOps
36 PRs with security focus.
- PR #1903 - Security: DNS rebinding protection - Closing security vulnerabilities
- PR #1905 - Security: Web Push SSRF - SSRF mitigation patterns
- PR #1964 - Content Security Policy - CSP implementation
Rosa Gutierrez
Expertise: CSRF Protection, Security Headers
- PR #1721 - Sec-Fetch-Site header reporting - Modern browser security header usage
- PR #1751 - Sec-Fetch-Site CSRF protection - Moving from report-only to enforcement
Topic-Based Learning Paths
Turbo/Hotwire
- PR #339 - Enable page refreshes with morphing - Morphing basics
- PR #396 - Refresh timers when morphing happens - Handling side effects
- PR #416 - Use morphing in stream replace - Stream + morphing
- PR #490 - Refresh local time target when morphed - Component refresh patterns
- PR #696 - Turbo frames to avoid resetting forms - Turbo Frames for partial updates
- PR #1091 - Speed up filter expansion with turbo stream - Performance patterns
- PR #1327 - Fix morphing + pagination issues - Edge cases
- PR #1413 - Upgrade turbo - Upgrade patterns
Stimulus
- PR #501 - Stimulus controller for bubbles - Introduction with review feedback on values API
- PR #586 - Knobs - Custom input components
- PR #592 - Set value when using slider - State management
- PR #920 - Popup orientation - Positioning logic
- PR #962 - Combobox a11y - Accessibility patterns
- PR #1187 - Save expanded state to localStorage - Persistence
- PR #1199 - Column animation - Animation coordination
- PR #1936 - Built-in :self support - Modern Stimulus patterns
Rails Caching
- PR #119 - Start adding caching - Foundation
- PR #135 - Counter cache for comments - Counter caches
- PR #317 - Assignee dropdown cache bypass - Fragment cache invalidation
- PR #340 - Fix caching issue with cards - Debugging cache issues
- PR #566 - Invalidate card caches when editing workflows - Cross-model invalidation
- PR #1052 - Add more caching - HTTP + template caching
- PR #1132 - Fix caching issues + refactor - Systematic fixes
- PR #1377 - Extend public caching - CDN/proxy caching
- PR #1571 - HTTP caching for menus - Component caching
- PR #1607 - CSRF vs HTTP caching - When NOT to cache
AI/LLM Integration
- PR #457 - Recursive descent parser - Command parsing fundamentals
- PR #460 - Fizzy Do initial - Command system foundation
- PR #464 - Fizzy Do: confirmations, close cards - Action execution
- PR #466 - Fizzy Do: tags, help menu - Expanding commands
- PR #483 - Fizzy Do with AI - Complete LLM integration guide
- PR #857 - Fizzy Ask - AI assistant implementation
- PR #929 - Conversation cost limits - Cost management with Quota model
- PR #978 - Track costs of AI summaries - Usage tracking
Multi-tenancy (SQLite)
- PR #159 - Introduce tenanting - Full design discussion
- PR #168 - Tenanting attempt 2 - Iteration on design
- PR #279 - Tenanting v3 - Production implementation
- PR #283 - Migration script for multi-tenant - Data migration
- PR #311 - Extracted tenant resolver - Middleware patterns
- PR #372 - Scope data by account - Data isolation
- PR #403 - Tenanted db is the account scope - Architectural clarity
- PR #879 - Tenanted session token cookies - Session handling
Webhooks & External APIs
- PR #1083 - Webhooks - Complete webhook system with security
- PR #1161 - Custom labels for Webhooks - User customization
- PR #1169 - Separate auto-close from close events - Event granularity
- PR #1196 - Link unfurling - URL previews
- PR #1229 - Postpone as webhook trigger - Event expansion
- PR #1292 - Webhook cleanup recurring job - Maintenance
Security
- PR #1114 - Escape HTML everywhere - XSS prevention
- PR #1721 - Sec-Fetch-Site header reporting - Modern CSRF detection
- PR #1751 - Sec-Fetch-Site CSRF protection - Enforcement
- PR #1903 - DNS rebinding protection - Network security
- PR #1905 - Web Push SSRF - SSRF mitigation
- PR #1964 - Content Security Policy - CSP implementation
- PR #1083 - Webhook security - HMAC signing, SSRF protection, replay prevention
Notifications
- PR #199 - Notification spike - Foundation
- PR #208 - Notification index - UI structure
- PR #274 - Notification preferences - User preferences
- PR #306 - Quieter notifications - Noise reduction
- PR #405 - Refactor notifications - Clean architecture
- PR #425 - Plain text mentions - @mention notifications
- PR #475 - Broadcast notification readings - Real-time updates
- PR #974 - Bundled notification emails - Batching
- PR #1448 - Group notifications by card - Aggregation
- PR #1574 - Aggregate email notifications - Email digests
Background Jobs (Solid Queue)
- PR #469 - bin/dev ensures puma runs solid queue - Development setup
- PR #494 - Recurring job for unused tags - Cleanup jobs
- PR #559 - Mission control for jobs - Admin interface
- PR #943 - Recurring job to clean finished jobs - Maintenance
- PR #1109 - Yabeda for Solid Queue metrics - Monitoring
- PR #1290 - Match job workers to CPUs - Scaling
- PR #1329 - Performance tuning: jobs - Optimization
- PR #1664 - Jobs enqueued after transaction commit - Reliability
- PR #1924 - Retry mailer jobs on errors - Error handling
Filtering & Search
- PR #113 - Persisted filters - State management
- PR #115 - Move filtering to the model - Model layer
- PR #116 - BubbleFilter extraction - Service object
- PR #131 - New filtering UI - Interface design
- PR #138 - Filter chips as links - URL-based filters
- PR #265 - Quick filters - Shortcuts
- PR #567 - Revamp filter menu - Complete overhaul
- PR #624 - Render and filter tags/users in menu - Dynamic filtering
Active Storage & Attachments
- PR #328 - Move attachments from Account to Bubble - Model restructure
- PR #557 - Production mirrors to purestorage - Storage services
- PR #707 - Card attachments - UI implementation
- PR #767 - Preprocess image variants - Performance
- PR #770 - Call blob.preview for previewable attachments - Preview handling
- PR #773 - Fix slow uploads - Performance fix
- PR #941 - Turn off previews on large files - Resource management
- PR #1689 - Improve avatar image handling - Avatar processing
Action Text & Rich Content
- PR #560 - Drop action_text_markdowns table - Cleanup
- PR #564 - Autolink emails and URLs at render time - Content processing
- PR #873 - Sanitizer config for ActionText - Security config
- PR #912 - Fix rich text content not applied - Debugging
- PR #964 - Rename Action Text Lexical to Lexxy - Custom editor
- PR #1859 - Handle ill-formed remote images - Error handling
Real-time (Action Cable & Web Push)
- PR #475 - Broadcast notification readings - Broadcasting patterns
- PR #699 - Clean up cable meta tag - Setup
- PR #705 - Broadcast when notifications cleared - Event broadcasting
- PR #781 - Web push - Push notifications
- PR #1291 - Yabeda ActionCable metrics - Monitoring
- PR #1432 - Subscribe to page changes via turbo streams - Page subscriptions
- PR #1765 - Fix action cable error in OSS mode - Configuration
- PR #1800 - Scope broadcasts by account - Multi-tenant broadcasting
- PR #1810 - Disconnect action cable on user deactivation - Cleanup
Email (Action Mailer)
- PR #314 - Tenanted Action Mailer URL helpers - Multi-tenant setup
- PR #974 - Bundled notification emails - Batching
- PR #1067 - Mailer styles and type hierarchy - Email design
- PR #1326 - User timezone in notification emails - Timezone handling
- PR #1525 - SVG avatars in emails - Compatibility
- PR #1574 - Aggregate email notifications by card - Aggregation
- PR #1911 - Email delivery via env vars - Configuration
- PR #1924 - Retry mailer jobs on errors - Reliability
Performance Optimization
- PR #380 - Paginate cards - Pagination basics
- PR #773 - Fix slow uploads - Upload performance
- PR #1089 - Several performance optimizations - Multiple fixes
- PR #1129 - Improved performance of cleaning inaccessible data - Batch operations
- PR #1254 - Pagination improvements - Advanced pagination
- PR #1283 - Performance tuning round 1 - Systematic tuning
- PR #1329 - Performance tuning: jobs - Job optimization
- PR #1747 - Address N+1 query situations - Query optimization
- PR #1927 - Faster D&D with optimistic insertion - UI responsiveness
Observability & Monitoring
- PR #285 - Structured JSON logging - Logging setup
- PR #301 - Structured logs with tenant - Multi-tenant logging
- PR #472 - Log authenticated user - User context
- PR #1109 - Yabeda for Solid Queue - Job metrics
- PR #1112 - Yabeda for Puma - Server metrics
- PR #1118 - OTel collector for Prometheus - Metrics pipeline
- PR #1165 - More Yabeda modules - Extended monitoring
- PR #1291 - Yabeda ActionCable metrics - WebSocket monitoring
- PR #1602 - Logging tweaks - Log refinement
- PR #1834 - console1984 and audits1984 - Access auditing
Concern Extraction & Refactoring
- PR #116 - Pull out BubbleFilter - Service extraction
- PR #324 - Extract pagination controller - Controller extraction
- PR #346 - Refactor collections perma - View restructure
- PR #370 - Extract helper with fallback - Helper patterns
- PR #398 - Comments refactoring - Model cleanup
- PR #405 - Refactor notifications - Architecture cleanup
- PR #508 - Extract Card::Entropy concern - Concern extraction
- PR #985 - Separate method with two outputs - Method clarity
- PR #1105 - Extract proprietary integrations into engine - Engine extraction
Workflows & State Machines
- PR #121 - Spike workflows - Initial exploration
- PR #218 - Filtering by workflow stage - Stage filters
- PR #329 - Set buckets at workflow level - Workflow assignment
- PR #389 - Colors on stages - Visual differentiation
- PR #413 - Refactor workflow default stages - Default handling
- PR #662 - Stage command - Command interface
- PR #763 - Resolve stages - Stage resolution
- PR #1258 - Cleanup workflow/stages - Code cleanup
Auto-close & Entropy System
- PR #327 - Add staleness sort order - Staleness concept
- PR #436 - Configure autoclose period - User configuration
- PR #489 - Handle collections without auto-close - Edge cases
- PR #508 - Extract Card::Entropy concern - Concern extraction
- PR #585 - Entropy improvements - Refinement
- PR #591 - Invalidate cache on entropy config change - Cache coordination
- PR #1451 - Entropy::Configuration to Entropy - API cleanup
Keyboard Navigation & Accessibility
- PR #302 - First round of accessibility fixes - Foundation
- PR #537 - Prevent default on keyboard shortcuts - Shortcut handling
- PR #581 - Wire-up keyboard navigation - Navigation system
- PR #695 - Focus hover styles - Visual feedback
- PR #834 - Trays keyboard nav - Tray navigation
- PR #962 - Combobox a11y - Complex components
- PR #994 - Fix up hotkey labels - Label accuracy
Mobile & Responsive Design
- PR #480 - Responsive card view - Card responsiveness
- PR #597 - Responsive trays - Tray adaptation
- PR #604 - Mobile columns - Column layout
- PR #739 - Mobile insets - Safe areas
- PR #740 - Mobile card improvements - Touch optimization
- PR #778 - Smaller action size on mobile - Size adaptation
- PR #881 - Mobile workflow styles - Workflow on mobile
- PR #1208 - Dynamic height for pins based on viewport - Viewport handling
Watching & Subscriptions
- PR #310 - Collection notification settings - Preference foundation
- PR #1088 - Watchers - Watcher system
- PR #1099 - Watching polish - UI refinement
- PR #1228 - Fix watchers list caching - Cache issues
- PR #1231 - Fix watching card inconsistencies - State consistency
- PR #1239 - Higher fidelity watching/unwatching - Reliability
- PR #1432 - Subscribe to page changes via turbo streams - Real-time updates
- PR #1519 - Clean watchers when losing access - Data cleanup
Credentials & Configuration
- PR #554 - Create beta environment and move secrets - Rails credentials
- PR #584 - Introduce staging environment - Environment setup
- PR #647 - Don’t require encrypted credentials in test - Test isolation
- PR #863 - Dev env improvements for new accounts - Development DX
- PR #1911 - Email delivery via env vars - Env configuration
- PR #1976 - CSP gives env config precedence - Config override
Drag & Drop
- PR #207 - Drag bubble divider - Basic dragging
- PR #209 - Fix divider drag jankiness - Smooth interaction
- PR #607 - Drag and drop cards between stages - Card movement
- PR #1927 - Faster D&D with optimistic insertion - Performance