This page is generated from the following source files:
The system is built upon a multi-tenant Rails 8 architecture utilizing the "Hotwire" stack (Turbo and Stimulus) to deliver real-time, reactive user interfaces without heavy client-side JavaScript frameworks. The application is designed to support both a single-tenant self-hosted mode (OSS) and a multi-tenant SaaS mode, configurable via environment variables.
The architecture follows a domain-centric design where the Account model serves as the tenant root, owning all major domain entities such as Boards, Cards, and Users. Data isolation is enforced at the application level, ensuring that tenant data is strictly segregated. The frontend leverages Stimulus controllers for behavior enhancement and Turbo Frames for dynamic updates, while ActionCable (via Solid Cable) handles WebSocket connections for real-time broadcasting.
正在加载图表渲染器...
Key Architectural Characteristics:
Account model acts as the aggregate root (app/models/account.rb:1-56), meaning all operational data (Boards, Cards, Users) belongs to a specific account, enforcing strict boundaries in multi-tenant mode.external_account_id from the request environment (app/channels/application_cable/connection.rb:1-22), ensuring broadcasts are scoped correctly to the tenant.app/javascript/controllers/application.js:1-9) and eagerly loads controllers from the import map (app/javascript/controllers/index.js:1-11), promoting a modular and maintainable client-side architecture.Gemfile:1-66).The domain layer is structured around three primary entities: Account, Board, and Card. These models utilize Rails concerns heavily to mix in cross-cutting functionalities like event tracking, storage handling, and access control.
The Account model serves as the tenant root. It is responsible for holding the global configuration and ownership of all data within a tenant.
AccountSlug encoding.create_with_owner(account:, owner:): A class-level factory method that atomically creates an account and assigns the first owner user (app/models/account.rb:24-29).active?: Determines if an account is currently active by checking for cancellations or ongoing imports (app/models/account.rb:44-46).external_account_id: A unique identifier used for slugs and external references, assigned via ExternalIdSequence (app/models/account.rb:53-55).has_many relationships with boards, cards, users, webhooks, and tags, all configured with dependent: :destroy to ensure data integrity when an account is deleted (app/models/account.rb:4-13).The Board model represents a collaborative workspace (e.g., a project or a Kanban board) containing cards.
Accessible concern.creator and account associations: Defaults to Current.user and creator.account respectively, ensuring automatic scoping to the current context (app/models/board.rb:4-5).alphabetically and ordered_by_recently_accessed scopes for flexible querying (app/models/board.rb:13-14).Broadcastable, Filterable, and Triageable, delegating specialized behaviors to these modules (app/models/board.rb:1-2).The Card model is the core unit of work (e.g., a task, bug, or feature request). It is the most complex model, featuring a rich set of behaviors.
move_to(new_board): A critical transactional method that moves a card to a different board. It updates the card's board ID, migrates associated events, and handles access control updates (app/models/card.rb:57-63).number: A human-readable sequential number unique to the account, assigned via account.increment!(:cards_count) (app/models/card.rb:91-93).after_save and after_touch to trigger a touch on the parent board, ensuring cache invalidation and updated timestamps (app/models/card.rb:18-19).handle_board_change callback wraps state updates in a transaction (app/models/card.rb:77-85). If access grants or data cleaning fail, the entire move operation is rolled back.saved_change_to_board_id? to conditionally trigger logic only when the board association actually changes (app/models/card.rb:20).The application implements a configurable multi-tenancy strategy that allows the same codebase to function as either a single-tenant application (self-hosted) or a multi-tenant SaaS platform.
The multi-tenant mode is determined by the Fizzy module and an initializer. The logic checks the SAAS environment variable or the existence of a specific flag file (lib/fizzy.rb:1-33). This flag influences database adapter selection (SQLite for OSS, MySQL for SaaS) and the Gemfile used.
The Account model includes the MultiTenantable concern, which sets a class attribute multi_tenant (app/models/account/multi_tenantable.rb:1-13). This attribute is finalized during the after_initialize phase of the Rails application startup, reading from ENV["MULTI_TENANT"] or application config (config/initializers/multi_tenant.rb:1-5).
Account model acts as the primary boundary. While the schema uses UUIDs which helps prevent accidental ID guessing, the application relies on the Current.account thread-local variable (set in controllers/channels) to scope queries.Account from the request environment. The ApplicationCable::Connection class looks up the account using fizzy.external_account_id provided by the middleware (app/channels/application_cable/connection.rb:12). If the account cannot be found, the connection is rejected.accepting_signups? method on the Account class returns true if in multi-tenant mode or if no accounts exist yet (initial setup) (app/models/account/multi_tenantable.rb:9-11). This prevents public sign-ups on a single-tenant instance that is already configured.The system utilizes a polymorphic event tracking mechanism to record the history of changes within a Board.
The Eventable concern is mixed into models that require audit trails. It establishes a polymorphic has_many :events relationship (app/models/concerns/eventable.rb:4-6).
track_event(action, creator:, board:, **particulars) method is the primary entry point. It constructs an event action name by combining the model's underscored class name with the specific action (e.g., card_board_changed) (app/models/concerns/eventable.rb:8-12).action (string), creator (User), board (Board), and particulars (a hash of serialized details like old/new values).When a card is moved to a new board, the Card model demonstrates the event tracking integration:
handle_board_change callback is triggered.track_board_change_event, passing the old board's name (app/models/card.rb:87-89).track_event from the concern, creating an Event record linked to the card and the new board context (app/models/card.rb:74-89).particulars hash captures the transition data (old_board, new_board), providing a complete audit log for the move operation.The frontend uses Stimulus, a modest JavaScript framework that augments HTML with behavior. Controllers are scoped to specific DOM elements and handle user interactions, API requests, and dynamic UI updates.
The JavaScript entry point initializes the Stimulus Application instance. It sets application.debug to false in production and exposes the instance globally as window.Stimulus (app/javascript/controllers/application.js:1-9). Controllers are registered automatically using eagerLoadControllersFrom, which scans the import map for files matching the */*_controller pattern (app/javascript/controllers/index.js:1-11).
The PaginationController implements infinite scrolling using the Intersection Observer API.
IntersectionObserver with a root margin of 300px, triggering the load logic when the pagination link enters the viewport (app/javascript/controllers/pagination_controller.js:31).<turbo-frame> element, sets its src to the link's href, and inserts it into the DOM (app/javascript/controllers/pagination_controller.js:95-106). This allows the server to return HTML fragments that Turbo seamlessly merges into the existing page.keepingScrollPosition) to ensure the user's view does not jump erratically while new content is loaded (app/javascript/controllers/pagination_controller.js:59).The LocalTimeController (and its associated formatters) handles the display of relative and absolute timestamps. It contains classes like AgoFormatter and DaysAgoFormatter to convert date objects into human-readable strings (e.g., "2 days ago") (app/javascript/controllers/local_time_controller.js:98-146). This ensures consistent time formatting across the UI without relying on complex server-side rendering logic for every timestamp.
The following sequence diagram illustrates the lifecycle of a Card Move operation, highlighting the interaction between the Client, Rails Controllers, the Card Model, and the Event system.
正在加载图表渲染器...
Flow Explanation:
move_to method.Card model wraps the move in a database transaction (app/models/card.rb:58). This ensures that updating the card's board ID and migrating its associated events happens atomically.handle_board_change callback is triggered. It calls track_event (app/models/card.rb:79), which creates an Event record via the Eventable concern (app/models/concerns/eventable.rb:10).Broadcastable concern included in Board/Card) triggers an ActionCable broadcast.The project utilizes a modern Rails stack optimized for developer productivity and operational simplicity.
| Technology | Purpose | Rationale |
|---|---|---|
| Rails 8 (main) | Web Framework | Provides the latest features including solid queue and improved async handling. |
| Hotwire (Turbo & Stimulus) | Frontend Stack | Enables reactive, real-time UIs with minimal JavaScript complexity. |
| Solid Queue | Background Jobs | Database-backed job processor; eliminates the need for Redis in production. |
| Solid Cable | WebSocket Server | Database-backed ActionCable adapter; simplifies infrastructure compared to Redis. |
| Solid Cache | Cache Store | Database-backed cache; consistent with the "Solid" philosophy of reducing dependencies. |
| SQLite / MySQL | Database | SQLite for OSS/self-hosted simplicity; MySQL for SaaS scalability. |
| Trilogy | MySQL Driver | A high-performance, drop-in replacement for the standard MySQL2 driver. |
| Kamal | Deployment Tool | Used for deploying the application to servers (implied by presence in Gemfile). |
The deployment strategy is bifurcated into two modes managed by the Fizzy module:
lib/fizzy.rb:9) and runs tests against the standard Gemfile.Gemfile.saas (lib/fizzy.rb:13-15).The CI pipeline (config/ci.rb) reflects this duality. It runs separate test suites for SaaS and OSS environments, including system tests (config/ci.rb:20-28). It also enforces security standards using Brakeman, Bundler Audit, and Gitleaks before allowing merges (config/ci.rb:15-18).
The following diagram illustrates the high-level dependencies between the core modules and subsystems.
正在加载图表渲染器...
Dependency Insights:
Eventable, MultiTenantable) to share behavior, adhering to the Composition over Inheritance principle.