# 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).
- **Image Loading** _(deprecated)_: Provide an `ImageHeadersProvider` to add custom HTTP headers (e.g., authentication tokens) when loading images. Use the `CDN` interface instead.

## 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 fun 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 Image Loading Headers

<admonition type="warning">

`ImageHeadersProvider` is deprecated. Use the [`CDN`](#custom-cdn-reads) interface instead — return headers in `CDNRequest.headers` from `CDN.imageRequest()`.

</admonition>

If your CDN requires authentication or custom headers when loading images, implement the `ImageHeadersProvider` interface to inject headers into all image loading requests.

### ImageHeadersProvider Interface

```kotlin
public interface ImageHeadersProvider {

    /**
     * Returns a map of headers to be used for the image loading request.
     *
     * @param url The URL of the image to load.
     * @return A map of headers to be used for the image loading request.
     */
    public fun getImageRequestHeaders(url: String): Map<String, String>
}
```

### Creating a Custom ImageHeadersProvider

```kotlin
class AuthenticatedImageHeadersProvider(
    private val tokenProvider: () -> String,
) : ImageHeadersProvider {

    override fun getImageRequestHeaders(url: String): Map<String, String> {
        // Only add headers for your CDN URLs
        return if (url.contains("your-cdn.com")) {
            mapOf("Authorization" to "Bearer ${tokenProvider()}")
        } else {
            emptyMap()
        }
    }
}
```

### Configuring ImageHeadersProvider

#### XML UI Components

Set the provider on `ChatUI`:

```kotlin
ChatUI.imageHeadersProvider = AuthenticatedImageHeadersProvider {
    // Return your auth token
    authRepository.getToken()
}
```

#### Compose UI Components

Pass the provider to `ChatTheme`:

```kotlin
ChatTheme(
    imageHeadersProvider = AuthenticatedImageHeadersProvider {
        authRepository.getToken()
    },
) {
    // Your chat UI content
}
```

## Async Custom Image Loading Headers

<admonition type="warning">

`AsyncImageHeadersProvider` is deprecated. Use the [`CDN`](#custom-cdn-reads) interface instead — `CDN.imageRequest()` is already a `suspend` function.

</admonition>

For scenarios where obtaining headers requires asynchronous or blocking operations — such as reading an auth token from encrypted storage or fetching one from a remote endpoint — use `AsyncImageHeadersProvider` instead of `ImageHeadersProvider`.

<admonition type="note">

`AsyncImageHeadersProvider` is currently only available for the **Compose UI Components**. For XML UI Components, continue using `ImageHeadersProvider` with `ChatUI`.

</admonition>

### AsyncImageHeadersProvider Interface

Unlike `ImageHeadersProvider`, this interface exposes a `suspend` function and is always invoked on `Dispatchers.IO`, making blocking calls safe:

```kotlin
public interface AsyncImageHeadersProvider {

    /**
     * Returns a map of headers to be used for the image loading request.
     *
     * Always called on [kotlinx.coroutines.Dispatchers.IO], so blocking operations are safe.
     *
     * @param url The URL of the image to load.
     * @return A map of headers to be used for the image loading request.
     */
    public suspend fun getImageRequestHeaders(url: String): Map<String, String>
}
```

### Creating a Custom AsyncImageHeadersProvider

```kotlin
class AsyncAuthenticatedImageHeadersProvider(
    private val tokenProvider: suspend () -> String,
) : AsyncImageHeadersProvider {

    override suspend fun getImageRequestHeaders(url: String): Map<String, String> {
        // Safe to call blocking or suspending operations here — runs on Dispatchers.IO
        return if (url.contains("your-cdn.com")) {
            mapOf("Authorization" to "Bearer ${tokenProvider()}")
        } else {
            emptyMap()
        }
    }
}
```

### Configuring AsyncImageHeadersProvider

#### Compose UI Components

Pass the provider to `ChatTheme` via the `asyncImageHeadersProvider` parameter:

```kotlin
ChatTheme(
    asyncImageHeadersProvider = AsyncAuthenticatedImageHeadersProvider {
        // Safe to call blocking or suspending operations here
        authRepository.getToken()
    },
) {
    // Your chat UI content
}
```

Internally, `ChatTheme` wraps the provider in a Coil interceptor that injects the headers into each image request as part of Coil's background pipeline.

<admonition type="warning">

If you supply a custom `StreamCoilImageLoaderFactory` to `ChatTheme`, you must override the two-argument `imageLoader(context, interceptors)` overload in addition to the single-argument one. `ChatTheme` delivers the `AsyncImageHeadersProvider` interceptor through this overload, so a factory that only implements `imageLoader(context)` will silently ignore the provider.

</admonition>

## Custom CDN Reads

<admonition type="info">

This feature is available starting from version [6.37.0](https://github.com/GetStream/stream-chat-android/releases/tag/v6.37.0) of the Android SDK.

</admonition>

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** (when `useDocumentGView` is set to `false`)

### 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()
```

### Document Attachments

The SDK treats the following MIME types as **document** attachments:

- `application/pdf`
- `application/msword`
- `text/plain`
- `text/html`
- Any `application/vnd.*` type (Office formats: DOCX, XLSX, PPTX, etc.)

Documents are not images or videos, so they are not loaded via Coil or ExoPlayer. Instead, when a user taps a document attachment, the SDK needs to open it for preview. How this happens depends on the `useDocumentGView` flag:

| `useDocumentGView` | Behavior                                                                                                                                                                     |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `true` (default)   | Opens the document via **Google Docs Viewer** — the SDK passes the raw file URL to `docs.google.com/gview`, which fetches and renders the document server-side in a WebView. |
| `false`            | Downloads the document **client-side** through the SDK's HTTP stack and opens it with an external application (e.g., a PDF viewer, Office app).                              |

<admonition type="warning">

If you implement `CDN.fileRequest()`, you **must** set `useDocumentGView` to `false`. When set to `true`, Google Docs Viewer fetches the file URL from Google's servers — it does not go through the SDK's HTTP stack, so your CDN signed URLs and auth headers will not be applied. This means document previews will fail for any CDN that requires authentication.

</admonition>

#### Compose UI Components

```kotlin
ChatTheme(useDocumentGView = false) {
    // Your chat UI content
}
```

#### XML UI Components

```kotlin
ChatUI.useDocumentGView = false
```

## Migrating to CDN

The `CDN` interface replaces several previously scattered configuration points. While the old APIs remain functional, they are deprecated and we recommend migrating to the unified `CDN` interface.

### Backwards Compatibility

The new `CDN` interface is fully backwards-compatible with the deprecated APIs. You can keep using the old interceptors (e.g. `ImageHeadersProvider`) unchanged while only providing file interception via `CDN.fileRequest()`. While we recommend consolidating all interception logic into the `CDN` interface, mixing old and new approaches will work correctly — CDN headers take precedence over deprecated provider headers when both are set for the same key.

<admonition type="warning">

If you implement `CDN.fileRequest()`, you **must** set `useDocumentGView` to `false` for document previews to work through your CDN pipeline. See the [Document Attachments](#document-attachments) section above.

</admonition>

### Migration Example

**Before** — scattered across multiple locations:

```kotlin
// Compose: image loading headers, download URI generation, and download request interception
ChatTheme(
    imageHeadersProvider = object : ImageHeadersProvider {
        override fun getImageRequestHeaders(url: String) =
            mapOf("Authorization" to "Bearer $token")
    },
    downloadAttachmentUriGenerator = DownloadAttachmentUriGenerator { attachment ->
        Uri.parse(signingService.sign(attachment.assetUrl ?: attachment.imageUrl))
    },
    downloadRequestInterceptor = DownloadRequestInterceptor { request ->
        request.addRequestHeader("Authorization", "Bearer $token")
    },
) {
    // Your chat UI content
}

// XML: image loading headers, video headers, download URI generation, and download request interception
ChatUI.imageHeadersProvider = object : ImageHeadersProvider {
    override fun getImageRequestHeaders(url: String) =
        mapOf("Authorization" to "Bearer $token")
}
ChatUI.videoHeadersProvider = object : VideoHeadersProvider {
    override fun getVideoRequestHeaders(url: String) =
        mapOf("Authorization" to "Bearer $token")
}
ChatUI.downloadAttachmentUriGenerator = DownloadAttachmentUriGenerator { attachment ->
    Uri.parse(signingService.sign(attachment.assetUrl ?: attachment.imageUrl))
}
ChatUI.downloadRequestInterceptor = DownloadRequestInterceptor { request ->
    request.addRequestHeader("Authorization", "Bearer $token")
}
```

**After** — single configuration point:

```kotlin
ChatClient.Builder("apiKey", context)
    .cdn(object : CDN {
        override suspend fun imageRequest(url: String) = CDNRequest(
            url = signingService.sign(url),
            headers = mapOf("Authorization" to "Bearer $token")
        )
        override suspend fun fileRequest(url: String) = CDNRequest(
            url = signingService.sign(url),
            headers = mapOf("Authorization" to "Bearer $token")
        )
    })
    .build()
```

### Deprecation Mapping

| Deprecated API                                             | CDN Replacement                                                                         |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `ImageHeadersProvider`                                     | `CDN.imageRequest()` — return headers in `CDNRequest.headers`                           |
| `AsyncImageHeadersProvider`                                | `CDN.imageRequest()` — the CDN method is already `suspend`                              |
| `VideoHeadersProvider`                                     | `CDN.fileRequest()` — return headers in `CDNRequest.headers`                            |
| `ImageAssetTransformer`                                    | `CDN.imageRequest()` — return transformed URL in `CDNRequest.url`                       |
| `DownloadAttachmentUriGenerator`                           | `CDN.imageRequest()` / `CDN.fileRequest()` — return transformed URL in `CDNRequest.url` |
| `DownloadRequestInterceptor`                               | `CDN.imageRequest()` / `CDN.fileRequest()` — return headers in `CDNRequest.headers`     |
| `ChatClient.Builder.shareFileDownloadRequestInterceptor()` | `CDN.fileRequest()` — return URL / headers in `CDNRequest`                              |

## 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-04-17T17:33:31.924Z.

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