WatchTower Current Project Architecture
Last updated: 2026-05-27
Scope: current codebase structure after the recent modular-monolith refactor commits
Purpose
This document explains the current WatchTower architecture as it exists in the repository today. It is a current-state reference for contributors who need to understand:
- where code lives now
- which modules were moved in the latest refactor
- how requests, async jobs, and integrations are wired
- which areas are still legacy and not yet migrated
It complements:
docs/PROJECT_OVERVIEW.mdfor product and domain contextdocs/modular-monolith-review.mdfor the refactor review and follow-up recommendations
Recent Structural Changes
The latest commits completed the modular migration for the remaining core bounded contexts and then cleaned up shared leftovers:
| Commit | Summary |
|---|---|
a225aa7 |
Moved the User bounded context into src/User/ |
b3ea04b |
Moved the Site bounded context into src/Site/ |
6b39379 |
Moved the Audit bounded context into src/Audit/ |
22a30b3 |
Moved ProjectStatus and technical SEO automation into their owning modules |
0a9439e |
Fixed security.yaml to use App\User\Domain\Entity\User |
Earlier commits in the same refactor sequence already migrated AI, GitLab, Report, and Notification.
Architecture Style
WatchTower is now a modular monolith built on Symfony. The application still deploys as one app and one database, but the main business areas are organized as bounded contexts with lightweight DDD and hexagonal layering.
Each migrated module follows this structure:
| Layer | Responsibility |
|---|---|
Domain |
Entities, enums, domain events, core business state |
Application |
Use-case services and orchestration |
Infrastructure |
Doctrine repositories, Messenger handlers, HTTP/API clients, automation |
UI |
Symfony controllers and form types |
This keeps framework details and external integrations out of the core domain objects while still staying pragmatic for a Symfony monolith.
High-Level Architecture
flowchart LR
User[Auditor / Reviewer / Admin] --> UI[Symfony Controllers + Twig + HTMX]
UI --> App[Application Services]
App --> Domain[Domain Entities + Enums]
App --> DB[(PostgreSQL)]
App --> MQ[Messenger async queue]
MQ --> AuditJobs[Audit preflight]
MQ --> CrawlJobs[Page crawl]
MQ --> ReportJobs[PDF generation]
MQ --> GitLabJobs[GitLab cache refresh]
CrawlJobs --> PSI[PageSpeed API]
AuditJobs --> Web[robots.txt / sitemap.xml / llms.txt]
GitLabJobs --> GitLab[GitLab API]
App --> AI[AI Providers]
Top-Level Project Structure
| Path | Purpose |
|---|---|
src/ |
Backend application code |
config/ |
Symfony, Doctrine, Messenger, security, and service wiring |
templates/ |
Twig templates for all server-rendered UI |
assets/ |
Frontend entrypoint and SCSS design system |
public/ |
Public web root and compiled frontend build output |
migrations/ |
Doctrine database migrations |
tests/ |
PHPUnit tests |
docs/ |
Product, architecture, refactor, and process documentation |
var/ |
Cache, logs, worker output, generated artifacts |
compose.yaml |
Local PostgreSQL service for development |
Source Code Layout
Modular bounded contexts
The main codebase now lives in these module roots:
src/AI/src/Audit/src/GitLab/src/Notification/src/Report/src/Site/src/User/
Remaining legacy root-level code
These areas are still outside the modular structure:
src/Entity/Client.phpsrc/Repository/ClientRepository.phpsrc/Service/ClientService.phpsrc/Service/DashboardService.phpsrc/Controller/ClientController.phpsrc/Controller/DashboardController.phpsrc/Controller/HomeController.phpsrc/Controller/ProjectManagementController.phpsrc/Controller/SecurityAuditController.phpsrc/Form/ClientType.phpsrc/Command/*src/Security/UserChecker.phpsrc/Pagination/Pagination.php
Current interpretation:
Clientis the main remaining domain object that does not yet have its own module.- dashboard, project management, and security audit pages are still cross-cutting entry points over several modules.
- console commands and generic utilities have not yet been reorganized into module-local infrastructure folders.
Module Inventory
src/Audit/
Owns the audit lifecycle and findings model.
Domain/Entity:Audit,Finding,SiteAuditCheck,SiteUrlAuditResultDomain/Enum: audit status, severity, finding status, scrape status, review URL sourceApplication/Service:AuditService,FindingServiceApplication/AuditArtifact: import and parse Composer/pnpm security artifactsInfrastructure/Automation: auto-finding generation and technical SEO file checksInfrastructure/Messenger: async preflight handlerUI: audit and finding controllers/forms
src/Site/
Owns auditable websites, URL inventory, repository links, and crawl automation.
Domain/Entity:Site,SiteUrl,SiteRepositoryDomain/Enum:ProjectStatusApplication/Service:SiteServiceInfrastructure/Automation: scraper, HTML extraction, on-page analyzer, PageSpeed, sitemap discovery, crawl queue managerInfrastructure/Messenger: page crawl handlerUI: site CRUD and repository/url forms
src/Report/
Owns generated reports and archived API payloads.
Domain/Entity:Report,ApiResponseArchiveDomain/Enum:ReportFormatApplication/Service:ReportServiceInfrastructure/Pdf: DOMpdf and command-based PDF generationInfrastructure/Messenger: async PDF generationUI: report controller and report views
src/GitLab/
Owns GitLab integration and repository/security views.
Application/Service:GitLabService,SecurityArtifactViewService,GitLabActivityViewServiceInfrastructure/Api:GitLabApiClientInfrastructure/Console: GitLab audit artifact import commandInfrastructure/Messenger: async repository cache refreshDomain/Enum:RepositoryProviderUI: GitLab controller
src/AI/
Owns AI agent configuration, execution, and usage tracking.
Domain/Entity:AiAgentConfiguration,AiAgentUsageDomain/Agent: runtime contracts and result objectsApplication/Agent: agent registry and concrete agent registrationApplication/Service: configuration resolution, execution, token/cost accountingInfrastructure/Provider: OpenAI, Anthropic, and Gemini providersInfrastructure/Prompt: prompt buildersUI: admin configuration and usage screens
src/User/
Owns platform users and profile/auth management.
Domain/Entity:UserApplication/Service:UserServiceInfrastructure/Persistence:UserRepositoryUI: auth, profile, and user-management controllers/forms
src/Notification/
Owns in-app notifications.
Domain/Entity:NotificationApplication/Service:NotificationServiceInfrastructure/Persistence:NotificationRepositoryUI: notification controller
Current Dependency Shape
The modules are separated by folders, but they still share a single codebase and a single relational model. Cross-module entity references are normal in the current design.
Main dependency edges
Auditdepends onSiteandUserReportdepends onAuditandUserAIdepends onUserNotificationdepends onUserSitedepends on legacyClientSiteRepositorydepends onGitLab\Domain\Enum\RepositoryProvider- GitLab async refresh depends on
SiteRepository
Practical consequence
User is the central identity module, Audit is the operational core, and Site acts as the bridge between client ownership, crawl data, and GitLab-linked repositories.
Module dependency map
flowchart LR
Client[Legacy Client]
Site[Site]
Audit[Audit]
Report[Report]
GitLab[GitLab]
AI[AI]
User[User]
Notification[Notification]
Client --> Site
Site --> Audit
Site --> GitLab
GitLab --> Site
Audit --> Report
Audit --> User
Report --> User
AI --> User
Notification --> User
Runtime Wiring
Doctrine
Doctrine mappings are split between one legacy namespace and the new modules:
App->src/EntityAI->src/AI/Domain/EntityAudit->src/Audit/Domain/EntityNotification->src/Notification/Domain/EntityReport->src/Report/Domain/EntitySite->src/Site/Domain/EntityUser->src/User/Domain/Entity
This means the database model is already aligned with the modular filesystem, except for legacy Client.
Service container
config/services.yaml autowires the full src/ tree and adds explicit configuration for infrastructure services that require runtime parameters:
- PDF generator selection via
APP_PDF_GENERATOR - external PDF command template via
APP_PDF_COMMAND - PageSpeed API key
- GitLab token, timeout, and cache TTL
- AI provider API keys
Security
Symfony security now uses the migrated user entity:
- provider class:
App\User\Domain\Entity\User - custom checker:
App\Security\UserChecker - hierarchy:
ROLE_ADMIN->ROLE_REVIEWER->ROLE_AUDITOR
Async Processing
WatchTower uses Symfony Messenger with a Doctrine-backed async transport.
Routed messages
| Message | Handler | Purpose |
|---|---|---|
App\Audit\Infrastructure\Messenger\RunAuditAutomationMessage |
RunAuditAutomationMessageHandler |
run preflight checks, discover sitemap URLs, generate initial findings |
App\Site\Infrastructure\Messenger\CrawlPageMessage |
CrawlPageMessageHandler |
crawl one page, run PageSpeed and on-page analysis, persist URL audit result |
App\Report\Infrastructure\Messenger\GenerateReportPdfMessage |
GenerateReportPdfMessageHandler |
generate PDF from report content |
App\GitLab\Infrastructure\Messenger\RefreshGitLabCacheMessage |
RefreshGitLabCacheMessageHandler |
refresh cached repository metadata from GitLab |
Background workflow
flowchart LR
UI[Controller / Form] --> APP[Application Service]
APP --> DB[(PostgreSQL)]
APP --> MQ[Messenger async queue]
MQ --> PRE[Audit preflight handler]
MQ --> CRAWL[Page crawl handler]
MQ --> PDF[PDF generation handler]
MQ --> GL[GitLab refresh handler]
PRE --> DB
CRAWL --> DB
PDF --> DB
GL --> DB
Audit Execution Flow
flowchart TD
A[Create site and repositories] --> B[Create audit]
B --> C[Dispatch audit preflight]
C --> D[Check robots.txt, sitemap.xml, llms.txt]
D --> E[Persist SiteAuditCheck records]
E --> F[Discover sitemap URLs]
F --> G[Store or sync SiteUrl entries]
G --> H[Select URLs for crawl]
H --> I[Dispatch CrawlPageMessage jobs]
I --> J[Scrape HTML and headers]
J --> K[Run PageSpeed and on-page analysis]
K --> L[Persist SiteUrlAuditResult]
L --> M[Generate automatic findings]
M --> N[Generate Markdown/PDF report]
Main Functional Flow
The current audit pipeline crosses several modules:
- A user creates or manages a
Siteand its relatedSiteRepositoryrecords. - An auditor creates an
Auditfor a site. - Audit preflight runs asynchronously and stores
SiteAuditCheckrecords. - Sitemap discovery populates
SiteUrlentries. - Selected pages are queued for crawl.
- Crawl workers persist
SiteUrlAuditResultdata and generate automaticFindingrecords. - Reports are generated in
Report, with optional AI-assisted Markdown and async PDF generation. - Notifications are sent to the responsible user when long-running jobs complete or fail.
Async Sequence
sequenceDiagram
actor Reviewer
participant UI as Symfony UI
participant App as Controller/Service
participant Queue as Messenger
participant Worker as Worker Handler
participant DB as PostgreSQL
participant External as External API
Reviewer->>UI: Trigger audit or report action
UI->>App: Submit request
App->>DB: Persist state
App->>Queue: Dispatch message
Queue->>Worker: Deliver job
Worker->>External: Call API / crawl target / GitLab / AI
External-->>Worker: Return payload
Worker->>DB: Save results
Worker-->>UI: Result becomes available on next refresh/view
Frontend and Template Architecture
The UI is server-rendered and intentionally simple:
- Twig templates under
templates/ - HTMX for progressive enhancement
- Bootstrap 5 for base UI behavior
- custom SCSS system under
assets/sass/ - Webpack Encore for asset compilation
Current frontend structure:
assets/app.jsis the single entrypointassets/sass/follows a 7-1 style split:abstracts,base,components,layout,pages,themes,vendorstemplates/is organized by feature folders that mostly match the backend module names
This is not a SPA. The UI architecture is Symfony controller + Twig template first, with HTMX used for incremental interactions.
Testing Layout
Tests still reflect the older flat structure more than the new module layout:
tests/Command/tests/Service/tests/Service/AuditArtifact/
Current coverage is strongest around:
- PDF generation
- GitLab service views
- audit artifact parsing/import
- selected console commands
There is still limited direct coverage for:
- domain rules inside migrated modules
- cross-module integration paths
- Messenger handlers
- site crawl automation services
Current Legacy Hotspots
The refactor is structurally complete for the main bounded contexts, but the project is still in a hybrid state.
Not yet modularized
Clientdomain and CRUD flow- dashboard aggregation
- project management and security-audit cross-cutting screens
- root-level commands and shared utilities
Migration residue to keep in mind
- some documentation still describes the pre-refactor layout
- tests have not been reorganized to mirror the new module folders
Clientremains the only mapped business entity undersrc/Entity/- root-level services/controllers still coordinate data from several modules
Where New Code Should Go
Use the modular structure by default.
| If you are adding... | Put it in... |
|---|---|
| audit workflow logic | src/Audit/ |
| page crawling or site automation | src/Site/ |
| report rendering or export logic | src/Report/ |
| GitLab integration | src/GitLab/ |
| AI execution/configuration | src/AI/ |
| user management or auth-related forms/controllers | src/User/ |
| notifications | src/Notification/ |
| client ownership logic | currently legacy src/Entity, src/Repository, src/Service, src/Controller, src/Form until a Client module exists |
Default rule:
- prefer module-local
Domain,Application,Infrastructure, andUIfolders - avoid adding new root-level
src/Service,src/Repository, orsrc/Controllerclasses unless the concern is genuinely cross-cutting and cannot yet be placed cleanly
Summary
WatchTower is currently a Symfony modular monolith with seven migrated bounded contexts and one notable legacy holdout: Client plus a handful of cross-cutting dashboard/controller surfaces. The core audit, site crawl, report, GitLab, AI, user, and notification flows now live in module-scoped folders and are wired through Doctrine, Messenger, Twig, and a small SCSS/HTMX frontend layer.
For day-to-day development, the safest mental model is:
- treat
Audit,Site,Report,GitLab,AI,User, andNotificationas the canonical module boundaries - treat root-level
src/*code as migration residue or shared entry points - add new backend code to module folders first, not the old flat structure