YourBOX Architecture
Application Layout
Section titled “Application Layout”YourBOX gathers a Symfony 7 application under app/ and the legacy 2020 codebase under
YourBOX/. The Symfony kernel owns routing, dependency injection, security and
templating. The legacy tree is mounted as a fallback bundle that serves any URL the new
controllers do not yet claim.
flowchart LR Browser[User browser] -- HTTPS --> Edge[Traefik or nginx HTTPS termination] Edge --> PhpFpm[php-fpm under Symfony kernel] PhpFpm --> Router[Symfony router] Router -- new route --> Controllers[Symfony controllers] Router -- no match --> Legacy[LegacyController fallback] Legacy -- include --> Files[YourBOX/src/**/*.php] Controllers --> Domain[Bounded contexts] Domain --> Adapters[Doctrine, Meilisearch, Flysystem, Mailer, OIDC]The strangler bundle is a deliberate intermediate state. As each route is rewritten, the matching legacy file is removed and the new controller wins on its URL. The migration order is documented in the Symfony Migration page of this service section.
Bounded Contexts
Section titled “Bounded Contexts”The application code is organized into four bounded contexts under app/src/<Context>/:
flowchart TB subgraph Catalog[Catalog] CatalogD[Domain: Video, Tag, MediaFile] CatalogA[Application: UploadVideo, PublishVideo, ListCatalog] CatalogI[Infrastructure: Doctrine, Flysystem, Meilisearch] CatalogU[UI: VideoController, Twig templates] end subgraph Engagement[Engagement] EngagementD[Domain: Comment, Vote] EngagementA[Application: PostComment, CastVote, MapThread] EngagementI[Infrastructure: Doctrine repositories] EngagementU[UI: CommentController, vote endpoints] end subgraph Identity[Identity] IdentityD[Domain: User, Role, Profile] IdentityA[Application: RegisterUser, MirrorOidcSubject] IdentityI[Infrastructure: Doctrine, Authentik claim mapper] IdentityU[UI: ProfileController, OIDC callback] end subgraph Moderation[Moderation] ModerationD[Domain: Report, AdminAction] ModerationA[Application: ReviewReport, DeleteVideo, BanUser] ModerationI[Infrastructure: Doctrine, audit log writer] ModerationU[UI: EasyAdmin dashboards] end
Catalog -- domain event: video published --> Engagement Catalog -- domain event: video published --> Moderation Engagement -- domain event: comment flagged --> Moderation Identity -- domain event: role changed --> ModerationCross context communication uses Symfony Messenger transports. Events propagate asynchronously. Commands inside a context run synchronously by default and switch to async only for heavy work such as video transcoding.
Data And Search
Section titled “Data And Search”PostgreSQL 16 holds users, videos, comments and votes. Doctrine migrations rebuild the
schema with foreign keys, partial indexes and a unique constraint on
(votes.ref, votes.ref_id, votes.user_id). Meilisearch holds a denormalized projection of
the video catalog and is refreshed by a Messenger consumer.
flowchart LR Controllers -- write --> PG[(PostgreSQL 16)] PG -- domain event --> Bus[Symfony Messenger] Bus -- VideoIndexerHandler --> Meili[(Meilisearch)] Bus -- TranscodeVideo --> Worker[FFmpeg worker] Worker -- HLS segments --> Storage Controllers -- read catalog --> PG Controllers -- search query --> Meili Controllers -- signed read URL --> Storage[(MinIO or Azure Blob)]The replication topology is delegated to the database provider. Azure attaches a read replica to the Flexible Server. CloudNativePG in the homelab runs a primary plus a hot standby with WAL archiving to MinIO. The application reads from the primary today and will route read only queries to the replica once the read endpoint is declared.
Video Pipeline
Section titled “Video Pipeline”Videos enter the catalog through the upload flow. The application writes the original
file to object storage under videos/YYYY/MM/<ulid>/source.<ext>, then dispatches a
TranscodeVideo message. The Messenger consumer runs FFmpeg, writes the HLS manifest
and segments under videos/YYYY/MM/<ulid>/hls/index.m3u8, then updates the database row
with the manifest key and duration.
sequenceDiagram participant Browser participant Controller participant Storage as Object storage participant Bus as Messenger bus participant Worker as FFmpeg worker participant PG as PostgreSQL participant Meili as Meilisearch
Browser->>Controller: POST upload (multipart) Controller->>Storage: PUT source key Controller->>PG: INSERT video row (status pending) Controller->>Bus: TranscodeVideo message Controller-->>Browser: 202 Accepted with status URL Bus->>Worker: dispatch Worker->>Storage: GET source key Worker->>Worker: ffmpeg transcode to HLS Worker->>Storage: PUT manifest and segments Worker->>PG: UPDATE video row (status published, manifest key) Worker->>Bus: VideoPublished event Bus->>Meili: reindex video documentPlayback uses HLS. The browser requests the manifest URL and signed segment URLs through the Flysystem adapter. The HTTPS layer is provided by Traefik on the VPS and on the homelab Kubernetes cluster, by the Azure App Service in Azure, and by Traefik with a local self signed certificate on docker compose.
Identity Flow
Section titled “Identity Flow”In production the Symfony Security firewall stack delegates authentication to Authentik
through OIDC. The first time a subject logs in, the application creates a row in its
local user table linking the subject by oidc_sub. Subsequent logins refresh
display_name, email and last_seen_at.
flowchart LR Browser -- /login --> Symfony[Symfony firewall] Symfony -- redirect --> Authentik Authentik -- callback with code --> Symfony Symfony -- token exchange --> Authentik Symfony -- find or create by oidc_sub --> PG[(PostgreSQL users)] Symfony -- session cookie --> BrowserA second firewall is enabled when APP_ENV is dev or vps. It exposes a form login
backed by Argon2id password hashes in the same user table, seeded by a Doctrine fixture
with admin and user accounts.
Deployment Topology
Section titled “Deployment Topology”The same container image deploys to four targets. Each target binds the application to its env contract from ADR-0008 and to the storage adapter from ADR-0005.
flowchart TD Image[GitLab registry: yourbox:tag@digest] Image --> Compose[docker compose: workstation] Image --> VPSAnsible[VPS via Ansible playbook] Image --> Azure[Azure Web App for containers] Image --> K8s[Kubernetes via Argo CD]
Compose --> ComposeStack[PostgreSQL, Meilisearch, MinIO, Mailpit, Traefik] VPSAnsible --> VPSStack[Same compose, Traefik with ACME, SOPS decryption on tmpfs] Azure --> AzureStack[Azure Database for PostgreSQL, Azure Blob, App Service] K8s --> K8sStack[CloudNativePG, Meilisearch chart, MinIO chart, sealed secrets]Each target runs the same smoke test: the health endpoint returns 200, an upload followed by transcoding and playback succeeds, and an OIDC or form login completes. The test runs in CI for docker compose, in the deployment job for the VPS, in a post deploy job for Azure, and through a Kubernetes job in the homelab.
Operational Boundaries
Section titled “Operational Boundaries”- The container image is stateless. The application filesystem can be read only at runtime in production.
- The legacy
YourBOX/tree is treated as untrusted from a security review standpoint during the transition. Authentik, network policies and the WAF mitigate that risk until the strangler bundle empties. - Cross context coupling goes through published domain events only. Reviewers reject direct method calls across contexts.
- Image references use the immutable form
<registry>/<image>:<tag>@sha256:<digest>as mandated by ADR-0009 and the Plumber configuration.