Skip to content

ADR-0005: Store YourBOX Video Assets In Flysystem Abstracted Object Storage

Accepted

YourBOX currently writes uploaded videos and thumbnails to a local cache/upload/ directory on the application container, then stores the absolute filesystem path in the files table. Two sample rows in yourbox.sql reference /home/anthony/Documents/homelab_devsecops/YourBOX/cache/upload/..., so the data layer carries a developer workstation path. This pattern breaks horizontal scaling, prevents the application from running with a read only filesystem, makes backups awkward and ties the application to a single deployment topology.

The storage layer must support the patterns of large streaming providers: redundancy through replication, the option of sharding by content identifier, and a clean separation between content addressing and physical storage. The same codebase deploys to the VPS smoke test, the Azure environment and the homelab Kubernetes cluster without rewriting the storage path.

Storing video binaries in the database is rejected. Object storage is the standard answer for this access pattern; the database holds metadata only.

YourBOX writes video assets through a Symfony service that wraps Flysystem. The interface exposed to the application has methods upload, download, delete, getSignedReadUrl and exists. The implementation is one Flysystem adapter selected by the YOURBOX_STORAGE_ADAPTER environment variable.

The supported adapters are:

  • s3 for MinIO in the homelab and on the VPS smoke test. MinIO runs as a single node in smoke environments and as an erasure coded distributed cluster in the homelab Kubernetes cluster. The same s3 adapter targets either deployment.
  • azure_blob for the Azure deployment target. The container is created by Terraform and the application reads the connection string from OpenBao via the External Secrets Operator.
  • local for local development without object storage. This is the only adapter that touches the container filesystem, and it is mounted on a host volume so the data is not lost on container restart.

The database stores a logical key in the form videos/YYYY/MM/<ulid>/source.mp4. The filesystem path column is removed from the schema during the PostgreSQL migration in ADR-0003. Thumbnails and HLS renditions follow the same key prefix.

A Symfony Messenger consumer handles transcoding. On upload the application stores the original file under the source key, then dispatches a TranscodeVideo message. The 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. Until the transcoding completes, the video is hidden from the catalog.

The HLS manifest is served either directly by the object store (Azure Blob with public read, or MinIO with a Traefik fronted path) or proxied through nginx with byte range support. The application generates signed URLs valid for one hour for any read access that requires authentication.

  • The application container is stateless. The filesystem can be read only at runtime in production, which simplifies image hardening and Kubernetes security context.
  • The same code path serves the VPS smoke test, the Azure environment and the homelab Kubernetes cluster. Switching adapters is a configuration change, not a code change.
  • Backups become the responsibility of the storage layer. MinIO uses object replication or versioning. Azure Blob uses geo redundant storage. The runbook for restoring a video documents the path for each backend.
  • Transcoding is asynchronous. The video catalog must indicate pending transcoding states so that the user interface does not show a broken playback link.
  • Signed URLs leak the storage backend identity to the browser. This is acceptable for HLS segment delivery but is called out in the security model documentation.
  • An attacker who gains write access to one adapter cannot reach the other adapter. Cross environment compromise requires breaching two distinct credential paths.