Back

Loading…

Documentation

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:

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.php
  • src/Repository/ClientRepository.php
  • src/Service/ClientService.php
  • src/Service/DashboardService.php
  • src/Controller/ClientController.php
  • src/Controller/DashboardController.php
  • src/Controller/HomeController.php
  • src/Controller/ProjectManagementController.php
  • src/Controller/SecurityAuditController.php
  • src/Form/ClientType.php
  • src/Command/*
  • src/Security/UserChecker.php
  • src/Pagination/Pagination.php

Current interpretation:

  • Client is 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, SiteUrlAuditResult
  • Domain/Enum: audit status, severity, finding status, scrape status, review URL source
  • Application/Service: AuditService, FindingService
  • Application/AuditArtifact: import and parse Composer/pnpm security artifacts
  • Infrastructure/Automation: auto-finding generation and technical SEO file checks
  • Infrastructure/Messenger: async preflight handler
  • UI: audit and finding controllers/forms

src/Site/

Owns auditable websites, URL inventory, repository links, and crawl automation.

  • Domain/Entity: Site, SiteUrl, SiteRepository
  • Domain/Enum: ProjectStatus
  • Application/Service: SiteService
  • Infrastructure/Automation: scraper, HTML extraction, on-page analyzer, PageSpeed, sitemap discovery, crawl queue manager
  • Infrastructure/Messenger: page crawl handler
  • UI: site CRUD and repository/url forms

src/Report/

Owns generated reports and archived API payloads.

  • Domain/Entity: Report, ApiResponseArchive
  • Domain/Enum: ReportFormat
  • Application/Service: ReportService
  • Infrastructure/Pdf: DOMpdf and command-based PDF generation
  • Infrastructure/Messenger: async PDF generation
  • UI: report controller and report views

src/GitLab/

Owns GitLab integration and repository/security views.

  • Application/Service: GitLabService, SecurityArtifactViewService, GitLabActivityViewService
  • Infrastructure/Api: GitLabApiClient
  • Infrastructure/Console: GitLab audit artifact import command
  • Infrastructure/Messenger: async repository cache refresh
  • Domain/Enum: RepositoryProvider
  • UI: GitLab controller

src/AI/

Owns AI agent configuration, execution, and usage tracking.

  • Domain/Entity: AiAgentConfiguration, AiAgentUsage
  • Domain/Agent: runtime contracts and result objects
  • Application/Agent: agent registry and concrete agent registration
  • Application/Service: configuration resolution, execution, token/cost accounting
  • Infrastructure/Provider: OpenAI, Anthropic, and Gemini providers
  • Infrastructure/Prompt: prompt builders
  • UI: admin configuration and usage screens

src/User/

Owns platform users and profile/auth management.

  • Domain/Entity: User
  • Application/Service: UserService
  • Infrastructure/Persistence: UserRepository
  • UI: auth, profile, and user-management controllers/forms

src/Notification/

Owns in-app notifications.

  • Domain/Entity: Notification
  • Application/Service: NotificationService
  • Infrastructure/Persistence: NotificationRepository
  • UI: 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

  • Audit depends on Site and User
  • Report depends on Audit and User
  • AI depends on User
  • Notification depends on User
  • Site depends on legacy Client
  • SiteRepository depends on GitLab\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/Entity
  • AI -> src/AI/Domain/Entity
  • Audit -> src/Audit/Domain/Entity
  • Notification -> src/Notification/Domain/Entity
  • Report -> src/Report/Domain/Entity
  • Site -> src/Site/Domain/Entity
  • User -> 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:

  1. A user creates or manages a Site and its related SiteRepository records.
  2. An auditor creates an Audit for a site.
  3. Audit preflight runs asynchronously and stores SiteAuditCheck records.
  4. Sitemap discovery populates SiteUrl entries.
  5. Selected pages are queued for crawl.
  6. Crawl workers persist SiteUrlAuditResult data and generate automatic Finding records.
  7. Reports are generated in Report, with optional AI-assisted Markdown and async PDF generation.
  8. 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.js is the single entrypoint
  • assets/sass/ follows a 7-1 style split: abstracts, base, components, layout, pages, themes, vendors
  • templates/ 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

  • Client domain 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
  • Client remains the only mapped business entity under src/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, and UI folders
  • avoid adding new root-level src/Service, src/Repository, or src/Controller classes 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, and Notification as 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
Contents