Files-nexgate-service(2)

File Management Service

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-09-23
Version: v1.0

Short Description: The File Management Service handles file uploads, downloads, and management for the NextGate social commerce platform. This service provides secure file storage with support for multiple file types, automatic metadata extraction, and organized directory structures for profiles, categories, and shops.

Hints:


Endpoints

1. Upload Multiple Files

Purpose: Upload multiple files to a specified directory with comprehensive metadata extraction

Endpoint: POST https://apinexgate.glueauth.com/api/v1/files/upload

Access Level: πŸ”’ Protected (Requires JWT Bearer token)

Authentication: Bearer Token (JWT access token required)

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {access_token}
Content-Type string Yes Must be "multipart/form-data"

Request Form Data Parameters:

Parameter Type Required Description Validation
files file[] Yes Array of files to upload Max 25MB per file, supported file types only
directory string Yes Target directory for files enum: PROFILE, CATEGORIES, SHOPS

Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files uploaded successfully",
  "action_time": "2025-09-23T10:30:00",
  "data": {
    "uploadedFiles": [
      {
        "fileName": "123e4567-e89b-12d3-a456-426614174000.jpg",
        "originalFileName": "product-image.jpg",
        "objectKey": "profile/123e4567-e89b-12d3-a456-426614174000.jpg",
        "directory": "PROFILE",
        "contentType": "image/jpeg",
        "fileSize": 2048576,
        "fileSizeFormatted": "2.0 MB",
        "permanentUrl": "https://files.nextgate.com/bucket-uuid/profile/123e4567-e89b-12d3-a456-426614174000.jpg",
        "thumbnailUrl": "https://files.nextgate.com/bucket-uuid/profile/123e4567-e89b-12d3-a456-426614174000.jpg",
        "fileExtension": ".jpg",
        "fileType": "IMAGE",
        "isImage": true,
        "isVideo": false,
        "isDocument": false,
        "isAudio": false,
        "width": 1920,
        "height": 1080,
        "dimensions": "1920x1080",
        "checksum": "d41d8cd98f00b204e9800998ecf8427e",
        "uploadedAt": "2025-09-23T10:30:00",
        "uploadedBy": "123e4567-e89b-12d3-a456-426614174000",
        "isPublic": true
      }
    ],
    "totalFiles": 3,
    "successfulUploads": 2,
    "failedUploads": 1,
    "totalSize": 5242880,
    "totalSizeFormatted": "5.0 MB",
    "uploadedAt": "2025-09-23T10:30:00",
    "message": "2 files uploaded successfully, 1 failed",
    "errors": [
      "Failed to upload large-file.mp4: File size exceeds maximum limit of 25MB"
    ]
  }
}

Response Fields:

Field Description
success Boolean indicating operation success
httpStatus HTTP status code as string
message Overall operation success message
action_time ISO 8601 timestamp of response generation
data.uploadedFiles Array of successfully uploaded file details
data.uploadedFiles[].fileName System-generated unique filename with UUID
data.uploadedFiles[].originalFileName Original filename provided by user
data.uploadedFiles[].objectKey Full object path in storage system
data.uploadedFiles[].directory Directory where file was stored
data.uploadedFiles[].contentType MIME type of the uploaded file
data.uploadedFiles[].fileSize File size in bytes
data.uploadedFiles[].fileSizeFormatted Human-readable file size (e.g., "2.0 MB")
data.uploadedFiles[].permanentUrl Direct public URL to access the file
data.uploadedFiles[].thumbnailUrl Thumbnail URL (same as permanentUrl for images)
data.uploadedFiles[].fileExtension File extension including dot (e.g., ".jpg")
data.uploadedFiles[].fileType File category (IMAGE, VIDEO, DOCUMENT, AUDIO, OTHER)
data.uploadedFiles[].isImage Boolean indicating if file is an image
data.uploadedFiles[].isVideo Boolean indicating if file is a video
data.uploadedFiles[].isDocument Boolean indicating if file is a document
data.uploadedFiles[].isAudio Boolean indicating if file is audio
data.uploadedFiles[].width Image width in pixels (images only)
data.uploadedFiles[].height Image height in pixels (images only)
data.uploadedFiles[].dimensions Image dimensions as "WxH" format
data.uploadedFiles[].checksum MD5 hash of file content for integrity verification
data.uploadedFiles[].uploadedAt Upload timestamp
data.uploadedFiles[].uploadedBy Account ID of uploader
data.uploadedFiles[].isPublic Boolean indicating public accessibility (always true)
data.totalFiles Total number of files in upload request
data.successfulUploads Number of successfully uploaded files
data.failedUploads Number of failed uploads
data.totalSize Total size of all uploaded files in bytes
data.totalSizeFormatted Human-readable total size
data.uploadedAt Overall upload completion timestamp
data.message Summary message of upload results
data.errors Array of error messages for failed uploads (null if no errors)

Error Responses:


2. Upload Single File

Purpose: Upload a single file to a specified directory with detailed metadata extraction

Endpoint: POST https://apinexgate.glueauth.com/api/v1/files/upload-single

Access Level: πŸ”’ Protected (Requires JWT Bearer token)

Authentication: Bearer Token (JWT access token required)

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {access_token}
Content-Type string Yes Must be "multipart/form-data"

Request Form Data Parameters:

Parameter Type Required Description Validation
file file Yes Single file to upload Max 25MB, supported file types only
directory string Yes Target directory for the file enum: PROFILE, CATEGORIES, SHOPS

Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "File uploaded successfully",
  "action_time": "2025-09-23T10:30:00",
  "data": {
    "fileName": "123e4567-e89b-12d3-a456-426614174000.pdf",
    "originalFileName": "product-catalog.pdf",
    "objectKey": "categories/123e4567-e89b-12d3-a456-426614174000.pdf",
    "directory": "CATEGORIES",
    "contentType": "application/pdf",
    "fileSize": 1048576,
    "fileSizeFormatted": "1.0 MB",
    "permanentUrl": "https://files.nextgate.com/bucket-uuid/categories/123e4567-e89b-12d3-a456-426614174000.pdf",
    "thumbnailUrl": null,
    "fileExtension": ".pdf",
    "fileType": "DOCUMENT",
    "isImage": false,
    "isVideo": false,
    "isDocument": true,
    "isAudio": false,
    "width": null,
    "height": null,
    "dimensions": null,
    "checksum": "5d41402abc4b2a76b9719d911017c592",
    "uploadedAt": "2025-09-23T10:30:00",
    "uploadedBy": "123e4567-e89b-12d3-a456-426614174000",
    "isPublic": true
  }
}

Response Fields:

Field Description
success Boolean indicating operation success
httpStatus HTTP status code as string
message Success message
action_time ISO 8601 timestamp of response generation
data Single FileResponse object with complete file metadata (same structure as uploadedFiles[] items above)

Error Responses:


3. Get User Files by Directory

Purpose: Retrieve all files uploaded by the authenticated user in a specific directory

Endpoint: GET https://apinexgate.glueauth.com/api/v1/files/directory/{directory}

Access Level: πŸ”’ Protected (Requires JWT Bearer token)

Authentication: Bearer Token (JWT access token required)

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {access_token}
Content-Type string Yes Must be "application/json"

Path Parameters:

Parameter Type Required Description Validation
directory string Yes Directory to list files from enum: PROFILE, CATEGORIES, SHOPS

Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files retrieved successfully",
  "action_time": "2025-09-23T10:30:00",
  "data": [
    {
      "fileName": "123e4567-e89b-12d3-a456-426614174000.jpg",
      "originalFileName": "profile-picture.jpg",
      "objectKey": "profile/123e4567-e89b-12d3-a456-426614174000.jpg",
      "directory": "PROFILE",
      "contentType": "image/jpeg",
      "fileSize": 2048576,
      "fileSizeFormatted": "2.0 MB",
      "permanentUrl": "https://files.nextgate.com/bucket-uuid/profile/123e4567-e89b-12d3-a456-426614174000.jpg",
      "thumbnailUrl": "https://files.nextgate.com/bucket-uuid/profile/123e4567-e89b-12d3-a456-426614174000.jpg",
      "fileExtension": ".jpg",
      "fileType": "IMAGE",
      "isImage": true,
      "isVideo": false,
      "isDocument": false,
      "isAudio": false,
      "width": 1920,
      "height": 1080,
      "dimensions": "1920x1080",
      "uploadedBy": "123e4567-e89b-12d3-a456-426614174000",
      "isPublic": true
    },
    {
      "fileName": "456e7890-e89b-12d3-a456-426614174111.png",
      "originalFileName": "banner-image.png",
      "objectKey": "profile/456e7890-e89b-12d3-a456-426614174111.png",
      "directory": "PROFILE",
      "contentType": "image/png",
      "fileSize": 1524288,
      "fileSizeFormatted": "1.5 MB",
      "permanentUrl": "https://files.nextgate.com/bucket-uuid/profile/456e7890-e89b-12d3-a456-426614174111.png",
      "thumbnailUrl": "https://files.nextgate.com/bucket-uuid/profile/456e7890-e89b-12d3-a456-426614174111.png",
      "fileExtension": ".png",
      "fileType": "IMAGE",
      "isImage": true,
      "isVideo": false,
      "isDocument": false,
      "isAudio": false,
      "width": 1600,
      "height": 900,
      "dimensions": "1600x900",
      "uploadedBy": "123e4567-e89b-12d3-a456-426614174000",
      "isPublic": true
    }
  ]
}

Response Fields:

Field Description
success Boolean indicating operation success
httpStatus HTTP status code as string
message Success message
action_time ISO 8601 timestamp of response generation
data Array of FileResponse objects for all files in the specified directory

Error Responses:


4. Delete File

Purpose: Delete a specific file uploaded by the authenticated user

Endpoint: DELETE https://apinexgate.glueauth.com/api/v1/files/{objectKey}

Access Level: πŸ”’ Protected (Requires JWT Bearer token)

Authentication: Bearer Token (JWT access token required)

Request Headers:

Header Type Required Description
Authorization string Yes Bearer {access_token}
Content-Type string Yes Must be "application/json"

Path Parameters:

Parameter Type Required Description Validation
objectKey string Yes Full object key/path of the file to delete Must be a valid object key from user's files

Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "File deleted successfully",
  "action_time": "2025-09-23T10:30:00",
  "data": null
}

Response Fields:

Field Description
success Boolean indicating operation success
httpStatus HTTP status code as string
message Success message confirming deletion
action_time ISO 8601 timestamp of response generation
data Always null for delete operations

Error Responses:


Quick Reference Guide

File Directory Types

Supported File Types

File Size and Limits

Common HTTP Status Codes

Authentication Types

Data Format Standards

File Thunder Arch & Flow

File Thunder is the dedicated media-processing microservice for the NexGate / Veepii platform. It handles all file ingestion, processing, storage, and serving-readiness β€” the main backend never touches raw bytes.


Table of Contents

  1. Architecture Overview
  2. Storage: Buckets & Object Keys
  3. MediaDomain & MediaContext
  4. Upload Flow β€” End to End
  5. The Four Wheels
  6. Progress Tracking (Redis β†’ SSE)
  7. Watermarking
  8. CDN & File Serving Per Environment
  9. Security
  10. API Reference & How to Consume

1. Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Main Backend                       β”‚
β”‚  - Only public-facing API                               β”‚
β”‚  - Calls File Thunder over internal HTTP (HMAC signed)  β”‚
β”‚  - Subscribes to Redis for progress β†’ pushes SSE        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚ HTTP (sync)              β”‚ Redis Pub/Sub (async)
             β–Ό                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     File Thunder                        β”‚
β”‚                                                         β”‚
β”‚   API Profile          Worker Profile                   β”‚
β”‚   ─────────────        ──────────────                   β”‚
β”‚   UploadController     UploadConfirmedWorker            β”‚
β”‚   MediaQueryController ThumbnailWorker                  β”‚
β”‚                        RawFilePurgeJob                  β”‚
β”‚                                                         β”‚
β”‚   Wheels (processing engines)                           β”‚
β”‚   ──────────────────────────                            β”‚
β”‚   ImageWheel  VideoWheel  ScanWheel  (MinIO ops)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚                          β”‚
             β–Ό                          β–Ό
          MinIO                      RabbitMQ
    (4 private buckets)       (internal event routing)

Key constraints:


2. Storage: Buckets & Object Keys

Buckets

Bucket Purpose Access
nexgate-raw Temporary upload landing zone β€” 24h TTL Private
nexgate-public Processed media served to end users Private (CDN in front)
nexgate-private Internal system assets (outros, future forensic assets) Private
nexgate-digital Digital product files β€” ClamAV scanned, download only Private

All buckets are fully private. MinIO is never directly accessible from the internet. Public content is served exclusively through the CDN (Cloudflare), which pulls from MinIO origin.

Object Key Pattern

{domain}/{ownerId}/{fileId}/{variant}

Examples:

posts/550e8400-e29b-41d4-a716-446655440000/f7e8d9.../original
posts/550e8400-e29b-41d4-a716-446655440000/f7e8d9.../large.webp
posts/550e8400-e29b-41d4-a716-446655440000/f7e8d9.../thumb.webp
posts/550e8400-e29b-41d4-a716-446655440000/f7e8d9.../hls/master.m3u8
posts/550e8400-e29b-41d4-a716-446655440000/f7e8d9.../hls/360p/360p.m3u8

system/outro/{ownerId}/{height}p.mp4   ← in nexgate-private

Rule: The database stores object keys only, never full URLs. URLs are assembled at the resolver boundary (CDN base URL + key in prod, presigned GET in local).


3. MediaDomain & MediaContext

MediaDomain

Defines where in the storage hierarchy a file lives. It is the top-level folder in the object key. This is an enum β€” the main backend must send one of these exact values.

Value Used For
POSTS Social posts β€” images and videos
PROFILES Profile pictures, cover photos
MESSAGES Direct message attachments
PRODUCTS Product images, product videos, digital downloads
SHOPS Shop banners, shop logos
EVENTS Event covers, event gallery images

MediaContext

Defines how a file is processed. This drives the entire processing pipeline. No logic is attached to domain β€” all logic is driven by context.

Value Type Processing Pipeline
SOCIAL_IMAGE Image ImageWheel β€” orient, strip EXIF, WebP variants, blurhash, lqip
SOCIAL_VIDEO Video VideoWheel β€” transcode + social watermark + outro + thumbnail
PROFILE_PICTURE Image ImageWheel
COVER_PHOTO Image ImageWheel
DM_IMAGE Image ImageWheel
DM_VIDEO Video VideoWheel β€” transcode, no watermark
DM_DOCUMENT Any ScanWheel β€” ClamAV scan
PRODUCT_IMAGE Image ImageWheel
PRODUCT_VIDEO Video VideoWheel β€” transcode + text watermark, no outro
DIGITAL_PRODUCT Any ScanWheel β€” ClamAV dual scan
SHOP_BANNER Image ImageWheel
SHOP_LOGO Image ImageWheel
EVENT_COVER Image ImageWheel
EVENT_GALLERY Image ImageWheel

Validation rules enforced at API layer:


4. Upload Flow β€” End to End

Standard Media Upload

Main Backend                    File Thunder                 MinIO           Client (Browser)
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚  POST /api/v1/upload/request  β”‚                         β”‚                   β”‚
     β”‚  (HMAC signed)                β”‚                         β”‚                   β”‚
     │──────────────────────────────▢│                         β”‚                   β”‚
     β”‚                               β”‚ validate + create DB    β”‚                   β”‚
     β”‚                               β”‚ record (PENDING)        β”‚                   β”‚
     β”‚                               β”‚ publish PENDING         β”‚                   β”‚
     β”‚                               β”‚ to Redis                β”‚                   β”‚
     β”‚                               β”‚ generate presigned PUT  β”‚                   β”‚
     β”‚                               │─────────────────────────▢                  β”‚
     β”‚  { fileId, presignedUrl }     │◀────────────────────────                   β”‚
     │◀──────────────────────────────│                         β”‚                   β”‚
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚  return presignedUrl          β”‚                         β”‚                   β”‚
     β”‚  to client                    β”‚                         β”‚                   β”‚
     │──────────────────────────────────────────────────────────────────────────▢ β”‚
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚                               β”‚                         β”‚  PUT (file bytes) β”‚
     β”‚                               β”‚                         │◀──────────────────│
     β”‚                               β”‚                         β”‚  200 OK           β”‚
     β”‚                               β”‚                         │──────────────────▢│
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚                               β”‚ ◀── MinIO event ────────│ s3:ObjectCreated  β”‚
     β”‚                               β”‚ MinioEventListener      β”‚                   β”‚
     β”‚                               β”‚ PENDING β†’ UPLOADED      β”‚                   β”‚
     │                               │ publish UPLOADED→Redis  │                   │
     β”‚                               β”‚ publish UploadConfirmed β”‚                   β”‚
     β”‚                               β”‚ to RabbitMQ             β”‚                   β”‚
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚                               β”‚ UploadConfirmedWorker   β”‚                   β”‚
     β”‚                               β”‚ routes by mimeType      β”‚                   β”‚
     β”‚                               β”‚ UPLOADED β†’ PROCESSING   β”‚                   β”‚
     │                               │ publish PROCESSING→Redis│                   │
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚                               β”‚ [ImageWheel or          β”‚                   β”‚
     β”‚                               β”‚  VideoWheel runs]       β”‚                   β”‚
     β”‚                               β”‚                         β”‚                   β”‚
     β”‚                               β”‚ variants uploaded       β”‚                   β”‚
     β”‚                               β”‚ to nexgate-public ──────▢                  β”‚
     β”‚                               β”‚ raw deleted from        β”‚                   β”‚
     β”‚                               β”‚ nexgate-raw ────────────▢                  β”‚
     β”‚                               β”‚ PROCESSING β†’ READY      β”‚                   β”‚
     β”‚                               β”‚ publish READY β†’ Redis   β”‚                   β”‚
     β”‚                               β”‚                         β”‚                   β”‚

User Custom Thumbnail Upload

Main Backend                    File Thunder                 MinIO
     β”‚                               β”‚                         β”‚
     β”‚  POST /api/v1/upload/         β”‚                         β”‚
     β”‚  thumbnail/{fileId}           β”‚                         β”‚
     │──────────────────────────────▢│                         β”‚
     β”‚                               β”‚ generate presigned PUT  β”‚
     β”‚                               β”‚ key: .../custom-        β”‚
     β”‚                               β”‚ thumbnail               β”‚
     β”‚  { presignedUrl }             β”‚                         β”‚
     │◀──────────────────────────────│                         β”‚
     β”‚  client uploads thumbnail     β”‚                         β”‚
     │─────────────────────────────────────────────────────────▢
     β”‚                               │◀── MinIO event ─────────│
     β”‚                               β”‚ MinioEventListener      β”‚
     β”‚                               β”‚ detects suffix=         β”‚
     β”‚                               β”‚ "custom-thumbnail"      β”‚
     β”‚                               β”‚ β†’ ThumbnailWorker       β”‚
     β”‚                               β”‚ download β†’ verify magic β”‚
     β”‚                               β”‚ bytes β†’ ImageMagick     β”‚
     β”‚                               β”‚ variants β†’ save to      β”‚
     β”‚                               β”‚ entity.userThumbnail    β”‚
     β”‚                               β”‚ β†’ delete raw            β”‚

File Status Lifecycle

PENDING β†’ UPLOADING β†’ UPLOADED β†’ SCANNING β†’ PROCESSING β†’ LIVE_PARTIAL β†’ READY
                                                                β–²
                                              (short video: after 360p uploaded)
                                              (long video: after 360p + partial HLS)

5. The Four Wheels

Wheels are the processing engines. Each runs in the worker profile.


Wheel 1 β€” ImageWheel (ImageMagick)

Triggered by: image MIME type on any image context

Pipeline:

download raw from nexgate-raw
      β”‚
      β–Ό
auto-orient + strip EXIF
      β”‚
      β”œβ”€β”€β–Ά large.webp    (1920px max width, shrink only, Q85)
      β”œβ”€β”€β–Ά medium.webp   (800px max width, shrink only, Q82)
      β”œβ”€β”€β–Ά thumb.webp    (300px max width, shrink only, Q80)
      β”œβ”€β”€β–Ά og.webp       (1200Γ—630 center crop, Q85)
      β”‚    └─ falls back to medium if source width < 1200px
      β”œβ”€β”€β–Ά blurhash      (encoded from 32Γ—32 downsample)
      └──▢ lqip          (10Γ—10 forced, base64 WebP data URI)

all WebP variants β†’ nexgate-public
keys + blurhash + lqip β†’ media_files.variants (JSONB)
raw deleted from nexgate-raw
status β†’ READY

Skipping logic: variant is skipped if source is already within bounds (never upscale).


Wheel 2 β€” VideoWheel (FFmpeg / Jaffree)

Triggered by: video MIME type on any video context

Step 1 β€” Probe & Validate

download raw from nexgate-raw
magic byte verify (must be real video container)
FFprobe β†’ codedWidth, codedHeight, rotation, duration, codec
displayWidth/displayHeight = rotation-aware (swap for 90Β°/270Β°)
size gate: > 2 GB β†’ reject
duration gate: > 4h β†’ reject
route: duration < 3min β†’ processShort()
       duration β‰₯ 3min β†’ processLong()

Step 2a β€” Short Path (< 3 min)

transcode() runs 3 variants in sequence:
  360p  (CRF 28, 96k audio)  β†’ upload β†’ status LIVE_PARTIAL
  720p  (CRF 23, 128k audio) β†’ upload
  1080p (CRF 21, 192k audio) β†’ upload

filter_complex:
  [0:v] split=2 [main][blur]
  [blur] scale={W}:{H}, boxblur=20:5 [bg]
  [main] scale={w}:{h} (fit within target, keep AR) [fg]
  [bg][fg] overlay=x=(W-w)/2:y=(H-h)/2 [out]
  + rotation transpose prepended if needed
  + watermark drawtext chain appended

if SOCIAL_VIDEO:
  append outro via concat (no re-encode)

skip logic: source displayWidth >= targetW OR displayHeight >= targetH

Step 2b β€” Long Path (β‰₯ 3 min) β€” HLS Adaptive

transcodeHls() per variant:
  360p β†’ segments + .m3u8 β†’ upload β†’ partial master.m3u8 β†’ LIVE_PARTIAL
  720p β†’ segments + .m3u8 β†’ upload
  1080p β†’ segments + .m3u8 β†’ upload β†’ full master.m3u8 β†’ READY

segment length: 2s
playlist type: VOD
master.m3u8 key: {keyBase}/hls/master.m3u8
variant keys:   {keyBase}/hls/{name}/{name}.m3u8

Step 3 β€” Thumbnail & Preview (both short and long)

selectBestFrame():
  5 candidates at 15/30/45/60/75% of duration
  scored by: brightness gate (30–230) + Laplacian variance (sharpness)
  FFmpeg extracts 720px JPEG per candidate β†’ pick winner

buildThumbnailVariants():
  poster.webp      (1280px, Q85)
  thumb.webp       (480px, Q80)
  og.webp          (1200Γ—630 center crop, Q85)
  blurhash         (from 32Γ—32 downsample)
  lqip             (10Γ—10 forced, base64 WebP data URI)
  dominant_color   (#RRGGBB from 1Γ—1 squish)

extractPreviewClip():
  skip first 5% (max 2s) β†’ read 6s β†’ setpts=0.5*PTS β†’ 3s at 2Γ— speed
  blur-pad 360Γ—640, muted
  watermarked (same moving watermark as main video)

all thumbnail variants β†’ nexgate-public
raw deleted from nexgate-raw after all variants done

Wheel 3 β€” ScanWheel (ClamAV)

Triggered by: DIGITAL_PRODUCT and DM_DOCUMENT contexts only Social content (images and videos) is never scanned by ClamAV β€” FFmpeg/ImageMagick re-encode is the sanitisation.

Pipeline:

download raw from nexgate-raw
      β”‚
      β–Ό
SHA-256 hash β†’ check file_hashes table
      β”‚
      β”œβ”€β”€ hash known + clean β†’ skip scan, copy to nexgate-digital, READY (dedup fast lane)
      β”‚
      └── hash unknown β†’
            ClamAV scan #1 (upload scan)
            if clean β†’ move to nexgate-digital
            ClamAV scan #2 (pre-download scan, confirms in-transit integrity)
            if clean β†’ save hash, status READY
            if infected β†’ status FAILED, quarantine

Wheel 4 β€” MinIO Operations (used by all wheels)

Not a standalone service β€” MinIO operations are helpers used across all wheels:

Operation Used By
Presigned PUT URL Upload request endpoint
Download object All wheels (download raw for processing)
Upload object All wheels (upload processed variants)
Remove object All wheels (delete raw after processing)
Stat object OutroService (cache check), ScanWheel (dedup check)

6. Progress Tracking (Redis β†’ SSE)

File Thunder publishes status changes to Redis. The main backend subscribes and drives SSE to the client. SSE is the main backend's responsibility β€” File Thunder only publishes.

What File Thunder Does

On every status change:

// 1. Cache current status in Redis (24h TTL)
redisTemplate.opsForValue().set("ft:status:{fileId}", status.name(), 24h);

// 2. Publish to per-file channel
redisTemplate.convertAndSend("ft:progress:{fileId}", status.name());

// 3. Append to timeline in Postgres
entity.timeline.add({ status, at })

What the Main Backend Must Do

// Subscribe to the file's progress channel
redisTemplate.subscribe((message, pattern) -> {
    String status = message.toString();
    sseEmitter.send(SseEmitter.event().data(status));
}, "ft:progress:" + fileId);

Status Channel Key

ft:progress:{fileId}     ← subscribe here for live updates
ft:status:{fileId}       ← read here for current status (24h cached)

7. Watermarking

Moving Watermark

Applied to all video variants and preview clips. Watermark position cycles through 4 corners every 3 seconds (5 seconds for social).

Text-only (PRODUCT_VIDEO):

drawtext: fontsize=max(14,H/40), white@0.65 + black shadow
position: floor(t/3) mod 4 β†’ top-left, top-right, bottom-right, bottom-left
text: from watermark.text property (default: "NexGate")

Social watermark (SOCIAL_VIDEO):

2-point diagonal: Pos A (22%, 28%) ↔ Pos B (58%, 65%) β€” switches every 5s
Logo: nexgate_logo_white.svg, 36Γ—36, 65% opacity
@ symbol: orange #F06023@0.85 + shadow
username: white@0.85 + shadow, below logo

Outro (SOCIAL_VIDEO only)

A personalized outro clip is appended to the watermarked variant (not clean variants).

Generation:

template: outro_template_with_sfx.mp4 (classpath resource)
font: Sora SemiBold 600 (classpath resource)
drawtext: @ in orange #F06023, username in cream #F4EEE9
fade-in: invisible before t=0.75s, fully opaque by t=1.2s
scale to match variant resolution (360p / 720p / 1080p)
append via: -f concat -safe 0 -c copy (no re-encode, fast)

Caching:

Generated once per ownerId + resolution
Cached to: nexgate-private / system/outro/{ownerId}/{height}p.mp4
On next upload: cache hit β†’ download and reuse, skip generation

8. CDN & File Serving Per Environment

All MinIO buckets are private. Access to public content is environment-dependent.

Environment Matrix

Environment Serving Method Config Value
Local dev Presigned GET URLs (MinIO direct, short-lived) ft.storage.mode=local
Staging CDN β€” cdn-staging.nexgate.com ft.storage.mode=cdn
Production CDN β€” cdn.nexgate.com ft.storage.mode=cdn

Local / Dev

Staging & Production (CDN)

Properties

# Local
ft.storage.mode=local
ft.cdn.base-url=

# Staging
ft.storage.mode=cdn
ft.cdn.base-url=https://cdn-staging.nexgate.com

# Production
ft.storage.mode=cdn
ft.cdn.base-url=https://cdn.nexgate.com

URL Assembly Logic (resolver boundary)

// local mode β†’ presigned GET from MinIO
// cdn mode   β†’ CDN base + object key
String url = storageMode.equals("cdn")
    ? cdnBaseUrl + "/" + objectKey
    : minioClient.getPresignedObjectUrl(GET, bucket, objectKey, 1h);

Private content (DIGITAL_PRODUCT, DM_DOCUMENT): always presigned GET URLs regardless of environment β€” these are never cached by CDN. Generated fresh per download request (15-minute expiry, audit logged).


9. Security

CORS

Configured via ft.cors.* properties. The CORS filter runs before all other filters so OPTIONS preflight requests are handled without requiring HMAC headers.

# Local
ft.cors.allowed-origins=http://localhost:3000

# Production
ft.cors.allowed-origins=https://nexgate.com,https://www.nexgate.com

ft.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
ft.cors.allowed-headers=*
ft.cors.allow-credentials=false
ft.cors.max-age-seconds=3600

Service-to-Service HMAC Authentication

Every HTTP request from the main backend to File Thunder must be HMAC-SHA256 signed. File Thunder rejects any unsigned or incorrectly signed request with 401 Unauthorized.

Filter order:

Request β†’ CorsFilter (HIGHEST_PRECEDENCE)
        β†’ HmacAuthFilter (HIGHEST_PRECEDENCE + 1)
        β†’ Controllers

Required Headers

Header Description Example
X-Service-Id Identifier of the calling service nexgate-main
X-Timestamp Unix epoch seconds at time of request 1718123456
X-Nonce Random UUID β€” unique per request f47ac10b-58cc-...
X-Signature HMAC-SHA256 hex of the canonical string a3f5c2...

Canonical String

METHOD\n
REQUEST_URI\n
TIMESTAMP\n
NONCE\n
HEX(SHA-256(requestBody))

Example for POST /api/v1/upload/request:

POST
/api/v1/upload/request
1718123456
f47ac10b-58cc-4372-a567-0e02b2c3d479
e3b0c44298fc1c149afb...   ← SHA-256 of the JSON body

Signature Computation

// Main backend β€” how to sign a request
String bodyHash = HexFormat.of().formatHex(
    MessageDigest.getInstance("SHA-256").digest(requestBodyBytes)
);

String canonical = method + "\n" + uri + "\n" + timestamp + "\n" + nonce + "\n" + bodyHash;

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(sharedSecret.getBytes(UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(UTF_8)));

// Set headers on outgoing request
request.setHeader("X-Service-Id", "nexgate-main");
request.setHeader("X-Timestamp",  String.valueOf(Instant.now().getEpochSecond()));
request.setHeader("X-Nonce",      UUID.randomUUID().toString());
request.setHeader("X-Signature",  signature);

What File Thunder Verifies (in order)

1. All 4 headers present                β†’ 401 if any missing
2. X-Service-Id in allowed list         β†’ 401 if unknown
3. X-Timestamp within Β±5 minutes        β†’ 401 if stale (replay protection)
4. X-Nonce not seen before              β†’ 401 if duplicate
   (stored in Redis with 10-min TTL)       (replay attack blocked)
5. Recompute HMAC, timing-safe compare  β†’ 401 if mismatch
   (MessageDigest.isEqual)                 (prevents timing oracle)

Properties

ft.security.hmac.secret=<long-random-secret-same-in-both-services>
ft.security.hmac.allowed-service-ids=nexgate-main
ft.security.hmac.timestamp-tolerance-seconds=300
ft.security.hmac.nonce-ttl-seconds=600

The secret must be identical in both File Thunder and the main backend config. Use a different value per environment (local / staging / prod).


10. API Reference & How to Consume

Base URL: http://file-thunder:8081 (internal network only) All requests must include HMAC headers β€” see Section 9.


POST /api/v1/upload/request

Request a presigned URL to upload a file directly to MinIO.

Request body:

{
  "ownerId":          "550e8400-e29b-41d4-a716-446655440000",
  "domain":           "POSTS",
  "context":          "SOCIAL_VIDEO",
  "originalFilename": "my-video.mp4",
  "mimeType":         "video/mp4",
  "fileSizeBytes":    104857600,
  "username":         "josh_dev"
}

username is required when context is SOCIAL_VIDEO. All other fields are always required.

Response:

{
  "status": "success",
  "message": "Presigned upload URL generated",
  "data": {
    "fileId":           "f7e8d9fa-...",
    "presignedUrl":     "http://minio:9000/nexgate-raw/posts/.../original?X-Amz-...",
    "objectKey":        "posts/{ownerId}/{entityId}/{fileId}/original",
    "bucket":           "nexgate-raw",
    "expiresInSeconds": 1800
  }
}

What to do next: PUT the file bytes directly to presignedUrl from the client browser. No auth headers needed on the PUT β€” the presigned URL is self-authenticating.


POST /api/v1/upload/thumbnail/{fileId}

Request a presigned URL to replace the system-generated video thumbnail with a user-supplied one. Only valid after the video file has been processed (status READY).

Response:

{
  "status": "success",
  "message": "Thumbnail presigned upload URL generated",
  "data": {
    "fileId":           "f7e8d9fa-...",
    "presignedUrl":     "http://minio:9000/nexgate-raw/.../custom-thumbnail?X-Amz-...",
    "objectKey":        "posts/.../custom-thumbnail",
    "bucket":           "nexgate-raw",
    "expiresInSeconds": 1800
  }
}

Accepted formats: JPEG, PNG, WebP only (verified by magic bytes).


GET /api/v1/media/{fileId}

Get full metadata for a file, including all processed variants.

Response:

{
  "status": "success",
  "data": {
    "fileId":    "f7e8d9fa-...",
    "ownerId":   "550e8400-...",
    "domain":    "POSTS",
    "context":   "SOCIAL_VIDEO",
    "status":    "READY",
    "mimeType":  "video/mp4",
    "variants": {
      "360p":          "posts/.../360p.mp4",
      "720p":          "posts/.../720p.mp4",
      "1080p":         "posts/.../1080p.mp4",
      "poster":        "posts/.../poster.webp",
      "thumb":         "posts/.../thumb.webp",
      "og":            "posts/.../og.webp",
      "preview_3s":    "posts/.../preview_3s.mp4",
      "blurhash":      "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
      "lqip":          "data:image/webp;base64,...",
      "dominant_color":"#1A2B3C"
    },
    "userThumbnail": null,
    "timeline": [
      { "status": "PENDING",    "at": "2026-06-15T10:00:00" },
      { "status": "UPLOADED",   "at": "2026-06-15T10:00:05" },
      { "status": "PROCESSING", "at": "2026-06-15T10:00:06" },
      { "status": "LIVE_PARTIAL","at": "2026-06-15T10:00:45" },
      { "status": "READY",      "at": "2026-06-15T10:02:10" }
    ],
    "createdAt": "2026-06-15T10:00:00",
    "updatedAt": "2026-06-15T10:02:10"
  }
}

Thumbnail resolution rule:

userThumbnail != null ? use userThumbnail : use variants

Both userThumbnail and variants use the same key names (poster, thumb, og, blurhash, lqip, dominant_color).


GET /api/v1/media/{fileId}/download?requesterId={uuid}

Generate a time-limited download URL for private files (DIGITAL_PRODUCT, DM_DOCUMENT only). Every call is audit-logged.

Response:

{
  "status": "success",
  "data": {
    "url":              "http://minio:9000/nexgate-digital/...?X-Amz-...",
    "expiresInSeconds": 900
  }
}

GET /api/v1/quota/{ownerId}

Get the total storage used by an owner across all their processed files. Updated atomically as each file is processed β€” never stale.

Response:

{
  "status": "success",
  "message": "Storage usage",
  "data": {
    "ownerId":   "550e8400-e29b-41d4-a716-446655440000",
    "usedBytes": 1073741824,
    "usedMb":    1024.0,
    "usedGb":    1.0
  }
}

How quota is tracked:

Use this endpoint to:


Error Responses

{ "error": "Missing required security headers" }          ← 401
{ "error": "Request timestamp out of acceptable window" } ← 401
{ "error": "Nonce already used β€” possible replay attack"} ← 401
{ "error": "Invalid signature" }                          ← 401

Validation errors return 400 with field-level messages via @ControllerAdvice.


This document covers the full File Thunder system as of 2026-06-15. CDN wiring (Cloudflare), Kafka integration, lazy transcode, and forensic watermarks are deferred.