# Custom CDN

By default, files and images are uploaded to Stream's CDN and loaded without additional HTTP headers. However, you can customize both behaviors:

- **File Uploads**: Provide a custom `FileUploader` to upload files to your own CDN or storage service.
- **CDN Reads**: Provide a custom `CDN` implementation to intercept and transform every file/image loading request (signed URLs, auth headers, custom hosts).

## Custom File Uploads

To use a custom CDN or storage service (such as AWS S3, Firebase Storage, or your own backend), implement the `FileUploader` interface and configure it on the `ChatClient.Builder`.

### FileUploader Interface

The `FileUploader` interface defines the contract for uploading and deleting files and images:

```kotlin
public interface FileUploader {

    @WorkerThread
    public fun sendFile(
        channelType: String,
        channelId: String,
        userId: String,
        file: File,
        callback: ProgressCallback,
    ): Result<UploadedFile>

    @WorkerThread
    public fun sendImage(
        channelType: String,
        channelId: String,
        userId: String,
        file: File,
        callback: ProgressCallback,
    ): Result<UploadedFile>

    @WorkerThread
    public fun deleteFile(
        channelType: String,
        channelId: String,
        userId: String,
        url: String,
    ): Result<Unit>

    @WorkerThread
    public fun deleteImage(
        channelType: String,
        channelId: String,
        userId: String,
        url: String,
    ): Result<Unit>
}
```

<Admonition type="note">

The interface also includes overloaded versions of these methods without the `ProgressCallback` parameter, as well as methods for uploading/deleting files not associated with a specific channel (`uploadFile`, `uploadImage`, `deleteFile`, `deleteImage`). You will need to implement those as well (omitted in this snippet for brevity).

</Admonition>

### UploadedFile Model

When a file is successfully uploaded, return an `UploadedFile` containing the file URL and optional metadata:

```kotlin
public data class UploadedFile(
    val file: String,                              // The URL of the uploaded file
    val thumbUrl: String? = null,                  // Optional thumbnail URL (for videos)
    val extraData: Map<String, Any> = emptyMap(),  // Additional custom metadata
)
```

### Creating a Custom FileUploader

Here's an example of a custom `FileUploader` implementation:

```kotlin
class CustomFileUploader(
    private val uploadApi: YourUploadApi,
) : FileUploader {

    override fun sendFile(
        channelType: String,
        channelId: String,
        userId: String,
        file: File,
        callback: ProgressCallback,
    ): Result<UploadedFile> {
        return try {
            val response = uploadApi.uploadFile(
                file = file,
                path = "channels/$channelType/$channelId/files",
                onProgress = { bytesUploaded, totalBytes ->
                    callback.onProgress(bytesUploaded, totalBytes)
                }
            )

            Result.Success(
                UploadedFile(
                    file = response.url,
                    thumbUrl = response.thumbnailUrl,
                )
            )
        } catch (e: Exception) {
            callback.onError(e)
            Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
        }
    }

    override fun sendImage(
        channelType: String,
        channelId: String,
        userId: String,
        file: File,
        callback: ProgressCallback,
    ): Result<UploadedFile> {
        return try {
            val response = uploadApi.uploadImage(
                file = file,
                path = "channels/$channelType/$channelId/images",
                onProgress = { bytesUploaded, totalBytes ->
                    callback.onProgress(bytesUploaded, totalBytes)
                }
            )

            Result.Success(
                UploadedFile(
                    file = response.url,
                    thumbUrl = response.thumbnailUrl,
                )
            )
        } catch (e: Exception) {
            callback.onError(e)
            Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
        }
    }

    override fun deleteFile(
        channelType: String,
        channelId: String,
        userId: String,
        url: String,
    ): Result<Unit> {
        return try {
            uploadApi.deleteFile(url)
            Result.Success(Unit)
        } catch (e: Exception) {
            Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
        }
    }

    override fun deleteImage(
        channelType: String,
        channelId: String,
        userId: String,
        url: String,
    ): Result<Unit> {
        return try {
            uploadApi.deleteFile(url)
            Result.Success(Unit)
        } catch (e: Exception) {
            Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
        }
    }

    // Implement the remaining methods (without ProgressCallback, and channel-agnostic variants) similarly
}
```

### Configuring the Custom FileUploader

Pass your custom `FileUploader` to the `ChatClient.Builder`:

```kotlin
val client = ChatClient.Builder(apiKey, context)
    .fileUploader(CustomFileUploader(YourUploadApi()))
    .build()
```

## File Transformation Before Upload

The SDK supports transforming files before upload using the `FileTransformer` interface. This is useful for any preprocessing steps, such as image compression or format conversion.

```kotlin
public interface FileTransformer {
    @WorkerThread
    public fun transform(file: File): File
}
```

Example implementation:

```kotlin
class ImageCompressorTransformer : FileTransformer {

    override fun transform(file: File): File {
        val mimeType = MimeTypeMap.getSingleton()
            .getMimeTypeFromExtension(file.extension)

        // Only compress image files
        if (mimeType?.startsWith("image/") != true) {
            return file
        }

        return compressImage(file)
    }

    private fun compressImage(file: File): File {
        // Your compression logic here
    }
}
```

<Admonition type="note">

File transformation happens before the file is passed to the `FileUploader`.

</Admonition>

## Custom CDN Reads

If your CDN requires signed URLs, authentication tokens, or custom headers when **loading** files and images, implement the `CDN` interface and configure it on the `ChatClient.Builder`. This provides a single, unified configuration point that covers all media loading paths in the SDK:

- **Images** loaded via Coil (avatars, image attachments, link previews)
- **Videos and audio** played via ExoPlayer (video attachments, voice recordings)
- **File downloads** via OkHttp and Android's `DownloadManager`
- **Document previews** opened via external apps

### CDN Interface

The `CDN` interface defines two `suspend` methods, each returning a `CDNRequest`:

```kotlin
public interface CDN {
    public suspend fun imageRequest(url: String): CDNRequest = CDNRequest(url)
    public suspend fun fileRequest(url: String): CDNRequest = CDNRequest(url)
}
```

- `imageRequest` — called for **image** URLs (Coil loading, image attachment download).
- `fileRequest` — called for **non-image** files (video/audio playback, document opening, file download).

Both methods have default implementations that return the original URL unchanged, so you only need to override the methods relevant to your CDN.

<Admonition type="warning">

The `CDN` methods are `suspend` but are **not** guaranteed to be called on a background dispatcher. If your implementation performs network calls (e.g., fetching a signed URL from a remote service), wrap them in `withContext(Dispatchers.IO)`.

</Admonition>

### CDNRequest

`CDNRequest` holds the (potentially rewritten) URL and optional headers:

```kotlin
public data class CDNRequest(
    val url: String,
    val headers: Map<String, String>? = null,
)
```

### Configuring the CDN

Register the CDN on `ChatClient` via the builder:

```kotlin
ChatClient.Builder("apiKey", context)
    .cdn(myCdnImpl)
    .build()
```

### Example: Signed URLs with Authentication Headers

```kotlin
val myCdn = object : CDN {
    override suspend fun imageRequest(url: String): CDNRequest = withContext(Dispatchers.IO) {
        val signedUrl = mySigningService.sign(url)
        CDNRequest(
            url = signedUrl,
            headers = mapOf("Authorization" to "Bearer ${getToken()}")
        )
    }

    override suspend fun fileRequest(url: String): CDNRequest = withContext(Dispatchers.IO) {
        val signedUrl = mySigningService.sign(url)
        CDNRequest(
            url = signedUrl,
            headers = mapOf("Authorization" to "Bearer ${getToken()}")
        )
    }
}

ChatClient.Builder("apiKey", context)
    .cdn(myCdn)
    .build()
```

## Supported Image MIME Types

The SDK recognizes the following MIME types as images:

- `image/bmp`
- `image/gif`
- `image/jpeg`
- `image/png`
- `image/webp`
- `image/heic`
- `image/heic-sequence`
- `image/heif`
- `image/heif-sequence`
- `image/svg+xml`

Files with these MIME types are uploaded using `sendImage()`, while all other files use `sendFile()`.


---

This page was last updated at 2026-08-10T16:00:42.299Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/android/client/custom-cdn/](https://getstream.io/chat/docs/sdk/android/client/custom-cdn/).