Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/LMendoza70/SSA/llms.txt

Use this file to discover all available pages before exploring further.

From the first day of development, the SSA Health Platform treats file storage as an external dependency — not a built-in capability. By routing all file operations through a StorageProvider interface, the platform eliminates vendor lock-in at the infrastructure level: a developer running the system locally uses the filesystem; a production deployment in AWS uses S3; a future migration to Azure changes a single environment variable, not a single line of business logic. The Multimedia Module, CMS, and any other module that handles files are completely unaware of where or how files are physically stored. This separation is enforced as an architectural constraint, not merely a convention.

StorageProvider Interface

All file operations in the platform go through the StorageProvider interface. No service, controller, or domain class is permitted to access the filesystem, an AWS SDK, or any cloud storage client directly. The interface is injected via NestJS dependency injection, making providers interchangeable and individually testable.
interface StorageProvider {
  /**
   * Upload a file and return the public-accessible URL or storage path.
   * @param file  The incoming multipart file from Multer
   * @param path  The desired storage path (e.g. "media/images/2024/photo.jpg")
   */
  upload(file: Express.Multer.File, path: string): Promise<string>;

  /**
   * Permanently delete a file from the storage backend.
   * @param path  The storage path returned by upload()
   */
  delete(path: string): Promise<void>;

  /**
   * Resolve a storage path to a publicly accessible URL.
   * @param path  The storage path returned by upload()
   */
  getUrl(path: string): string;
}
The upload method returns the canonical storage path for the file. That path is stored in the database and later passed to getUrl() to generate the public URL. This two-step design ensures URLs can be regenerated (e.g., after migrating providers) without touching the stored path.

Available Providers

The platform ships with four concrete implementations of StorageProvider. The active provider is selected at startup based on the STORAGE_DRIVER environment variable.
ProviderUse CaseConfig Key
LocalStorageProviderDevelopment, on-premise deploymentsSTORAGE_DRIVER=local
S3StorageProviderAWS production environmentsSTORAGE_DRIVER=s3
AzureBlobStorageProviderAzure production environmentsSTORAGE_DRIVER=azure
GCSStorageProviderGoogle Cloud production environmentsSTORAGE_DRIVER=gcs

LocalStorageProvider

Writes files to the local filesystem under the configured path. Suitable for local development and on-premise servers where cloud storage is not available. Files are served as static assets by NestJS.

S3StorageProvider

Uploads files to an Amazon S3 bucket using the AWS SDK. The recommended provider for AWS-hosted production deployments.

AzureBlobStorageProvider

Stores files in an Azure Blob Storage container using the Azure SDK. Use for deployments in the Azure ecosystem.

GCSStorageProvider

Uploads files to a Google Cloud Storage bucket using a service account key file. Use for deployments on Google Cloud Platform.

Configuration

Each provider is configured exclusively through environment variables. Only the variables for the active STORAGE_DRIVER need to be present.
# ── Local filesystem ────────────────────────────────────────────
STORAGE_DRIVER=local
STORAGE_LOCAL_PATH=./uploads

# ── Amazon S3 ───────────────────────────────────────────────────
STORAGE_DRIVER=s3
STORAGE_S3_BUCKET=ssa-media
STORAGE_S3_REGION=us-east-1
STORAGE_S3_ACCESS_KEY=...
STORAGE_S3_SECRET_KEY=...

# ── Azure Blob Storage ──────────────────────────────────────────
STORAGE_DRIVER=azure
STORAGE_AZURE_CONTAINER=ssa-media
STORAGE_AZURE_CONNECTION_STRING=...

# ── Google Cloud Storage ────────────────────────────────────────
STORAGE_DRIVER=gcs
STORAGE_GCS_BUCKET=ssa-media
STORAGE_GCS_KEY_FILE=/path/to/service-account.json
Never commit storage credentials to version control. All secrets must be managed through environment variables or a secrets manager (e.g., AWS Secrets Manager, Azure Key Vault, or Google Secret Manager).

File Deduplication

The Multimedia Module ensures that the same file is never stored twice, regardless of how many times it is uploaded. Before persisting any incoming file, the system computes a content hash (SHA-256) of the file’s binary data and checks the MediaAsset table for an existing record with that hash.
1

File arrives

A multipart upload request reaches the Multimedia Module endpoint. The file is held in memory by Multer — it has not been written to storage yet.
2

Hash computed

The service computes a SHA-256 hash of the file buffer. This hash uniquely identifies the file’s content, independent of its filename or upload metadata.
3

Duplicate check

The repository queries MediaAsset for a record where hash = computedHash AND deletedAt IS NULL. If a match is found, the existing MediaAsset ID is returned immediately.
4

Store if new

If no duplicate exists, the file is passed to the StorageProvider.upload() method, a new MediaAsset record is created, and the new ID is returned.
Because deduplication works at the content level, renaming a file before re-uploading it will still correctly detect the duplicate. Two files with different names but identical content are treated as one asset.

Supported File Types

The platform accepts the following file types through the Multimedia Module. Uploads outside this list are rejected with a 422 Unprocessable Entity response before reaching the storage layer.
CategoryFormats
ImagesJPEG, PNG, WebP, SVG
VideosMP4, WebM
DocumentsPDF
AudioMP3, OGG
MIME type validation is performed server-side on the binary content of the file, not on the filename extension. Renaming a .exe to .jpg will not bypass validation.

Usage in Modules

Modules that need to handle files declare a dependency on StorageProvider in their NestJS module definition and inject it into the relevant service:
// Example: using StorageProvider inside a service
@Injectable()
export class MultimediaService {
  constructor(
    private readonly storageProvider: StorageProvider,
    private readonly mediaAssetRepository: MediaAssetRepository,
  ) {}

  async uploadFile(file: Express.Multer.File): Promise<MediaAsset> {
    // Deduplication check happens here before calling storageProvider
    const storagePath = await this.storageProvider.upload(file, `media/${file.originalname}`);
    return this.mediaAssetRepository.create({ path: storagePath });
  }
}
Never access storage directly in services or controllers. Always inject and use the StorageProvider interface. Direct calls to fs, @aws-sdk/client-s3, or any cloud SDK bypass the abstraction layer and break provider portability.

Multimedia Module

How MediaAsset records are managed, deduplicated, and reused across content.

Architecture Overview

Where the storage abstraction fits within the Clean Architecture layers.

Deployment

Configuring storage provider environment variables in production environments.

Build docs developers (and LLMs) love