# Custom CDN

## Introduction

By default, the Stream Chat Flutter SDK uploads attachments to Stream's own CDN. If you need to store files on your own infrastructure (for example AWS S3, Google Cloud Storage, or your own backend), you customize the pieces below:

- **Uploads and deletes** are handled by an `AttachmentFileUploader`. Provide a custom implementation to send files to your own storage. The URLs it returns are stored on the message, so reads automatically go wherever your uploader put the file. This is the only piece most custom CDN setups need.
- **Image URL transformation** is handled by a `StreamImageCDN`. It rewrites image URLs before they are fetched, for resizing, signed URLs, or cache keys. Override it only when you need to modify the request to your CDN, not to point reads at it.

## Custom attachment uploader

`AttachmentFileUploader` defines how the SDK uploads and deletes files. The default implementation, `StreamAttachmentFileUploader`, sends everything to Stream's CDN. To use your own storage, implement `AttachmentFileUploader` and register it on the client.

The interface has eight methods split into two groups.

**Channel-scoped methods** handle files that belong to a channel. The SDK calls `sendImage`/`sendFile` when a message with attachments is sent, and `deleteImage`/`deleteFile` for each of a message's attachments when the message is hard deleted (`channel.deleteMessage(message, hard: true)`). They also run when your app calls `channel.sendImage`, `channel.sendFile`, `channel.deleteImage` or `channel.deleteFile`. A soft delete, or removing an attachment while editing a message, does not remove the file from your storage:

| Method        | Returns                     | Purpose                                       |
| ------------- | --------------------------- | --------------------------------------------- |
| `sendImage`   | `Future<SendImageResponse>` | Upload an image attached to a message         |
| `sendFile`    | `Future<SendFileResponse>`  | Upload a non-image file attached to a message |
| `deleteImage` | `Future<EmptyResponse>`     | Delete a previously uploaded image            |
| `deleteFile`  | `Future<EmptyResponse>`     | Delete a previously uploaded file             |

**Standalone methods** upload or remove files without any channel context. The SDK does not call them itself; they back `client.uploadImage`, `client.uploadFile`, `client.removeImage` and `client.removeFile`, which you can use for things like user avatars or app-specific uploads:

| Method        | Returns                       | Purpose                   |
| ------------- | ----------------------------- | ------------------------- |
| `uploadImage` | `Future<UploadImageResponse>` | Upload a standalone image |
| `uploadFile`  | `Future<UploadFileResponse>`  | Upload a standalone file  |
| `removeImage` | `Future<EmptyResponse>`       | Remove a standalone image |
| `removeFile`  | `Future<EmptyResponse>`       | Remove a standalone file  |

### Response types

The upload methods return a `SendAttachmentResponse` whose `file` field holds the URL of the uploaded asset. `SendImageResponse`, `UploadImageResponse`, and `UploadFileResponse` are all type aliases for `SendAttachmentResponse`. Only `SendFileResponse` adds a `thumbUrl` field, which you should populate when the uploaded file is a video. The delete and remove methods return an `EmptyResponse`.

### Progress and cancellation

The upload methods accept an optional `onSendProgress` callback and a `cancelToken`. Both come from [Dio](https://pub.dev/packages/dio) and are re-exported by `stream_chat`, so you do not need to add `dio` as a direct dependency:

- `ProgressCallback` is `void Function(int count, int total)`. Forward it to your upload client to report progress in the composer.
- `CancelToken` lets you abort an in-flight upload with `cancelToken.cancel()`.

### Implementing a custom uploader

Implement `AttachmentFileUploader` and return a `SendAttachmentResponse` whose `file` field points at the URL on your CDN. Call `AttachmentFile.toMultipartFile()` if your upload client expects multipart data.

```dart
import 'package:stream_chat/stream_chat.dart';

class MyCdnUploader implements AttachmentFileUploader {
  const MyCdnUploader(this._httpClient);

  // The SDK passes its configured StreamHttpClient. Reuse it or ignore it.
  final StreamHttpClient _httpClient;

  @override
  Future<SendImageResponse> sendImage(
    AttachmentFile image,
    String channelId,
    String channelType, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
    Map<String, Object?>? extraData,
  }) async {
    final url = await _upload(image, onSendProgress: onSendProgress);
    return SendAttachmentResponse()..file = url;
  }

  @override
  Future<SendFileResponse> sendFile(
    AttachmentFile file,
    String channelId,
    String channelType, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
    Map<String, Object?>? extraData,
  }) async {
    final url = await _upload(file, onSendProgress: onSendProgress);
    // Set thumbUrl as well when the uploaded file is a video.
    return SendFileResponse()..file = url;
  }

  @override
  Future<EmptyResponse> deleteImage(
    String url,
    String channelId,
    String channelType, {
    CancelToken? cancelToken,
    Map<String, Object?>? extraData,
  }) async {
    await _delete(url);
    return EmptyResponse();
  }

  @override
  Future<EmptyResponse> deleteFile(
    String url,
    String channelId,
    String channelType, {
    CancelToken? cancelToken,
    Map<String, Object?>? extraData,
  }) async {
    await _delete(url);
    return EmptyResponse();
  }

  // Standalone uploads without channel context.
  @override
  Future<UploadImageResponse> uploadImage(
    AttachmentFile image, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
  }) async {
    final url = await _upload(image, onSendProgress: onSendProgress);
    return SendAttachmentResponse()..file = url;
  }

  @override
  Future<UploadFileResponse> uploadFile(
    AttachmentFile file, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
  }) async {
    final url = await _upload(file, onSendProgress: onSendProgress);
    return SendAttachmentResponse()..file = url;
  }

  @override
  Future<EmptyResponse> removeImage(String url, {CancelToken? cancelToken}) async {
    await _delete(url);
    return EmptyResponse();
  }

  @override
  Future<EmptyResponse> removeFile(String url, {CancelToken? cancelToken}) async {
    await _delete(url);
    return EmptyResponse();
  }

  Future<String> _upload(
    AttachmentFile file, {
    ProgressCallback? onSendProgress,
  }) async {
    // Send the bytes to your storage and return the public URL.
    // final multipart = await file.toMultipartFile();
    throw UnimplementedError('Add your upload logic here');
  }

  Future<void> _delete(String url) async {
    // Delete the asset from your storage.
    throw UnimplementedError('Add your delete logic here');
  }
}
```

> If you only need custom behaviour for some methods, you can delegate the rest to a `StreamAttachmentFileUploader` instance created with the `StreamHttpClient` the SDK passes to your factory.

### Registering the uploader

Pass a factory to the `attachmentFileUploaderProvider` parameter when creating the client. The factory receives the fully configured `StreamHttpClient` (with auth interceptors and base URL already set up):

```dart
final client = StreamChatClient(
  'your-api-key',
  attachmentFileUploaderProvider: (httpClient) => MyCdnUploader(httpClient),
);
```

When omitted, the provider defaults to `StreamAttachmentFileUploader.new`, which uploads to Stream's CDN.

## Custom image CDN

Before an image attachment (including link-preview images and media thumbnails) is fetched, the SDK passes its URL through a `StreamImageCDN`, which can rewrite the URL and produce a stable cache key. Because your custom uploader already stores files at URLs on your own CDN, reads go there directly, so most custom CDN setups do not need a custom `StreamImageCDN`.

`StreamImageCDN` exposes two methods:

```dart
const imageCDN = StreamImageCDN();

// Resolve a resized URL for a given image.
final url = imageCDN.resolveUrl(
  originalUrl,
  resize: ImageResize(
    width: 200,
    height: 300,
    mode: ResizeMode.clip,
    crop: CropMode.center,
  ),
);

// Stable cache key for use with CachedNetworkImage.
final cacheKey = imageCDN.cacheKey(url);
```

### Using a custom image CDN

Override `StreamImageCDN` only when you need to modify how image URLs are requested from your CDN: to sign URLs, apply your CDN's own resize parameters, rewrite the host, or keep cache keys stable across rotating tokens. Extend `StreamImageCDN` (overriding `resolveUrl` and/or `cacheKey`) and inject it via `StreamChatConfigurationData.imageCDN`. The base implementation returns URLs on your CDN unchanged, so your override has to build the final URL itself rather than delegating to `super`. For example, to pass the requested size to your CDN:

```dart
class MyImageCDN extends StreamImageCDN {
  const MyImageCDN();

  @override
  String resolveUrl(String sourceUrl, {ImageResize? resize}) {
    final uri = Uri.tryParse(sourceUrl);
    if (uri == null || resize == null) return sourceUrl;
    return uri.replace(
      queryParameters: {
        ...uri.queryParameters,
        'width': resize.width.round().toString(),
        'height': resize.height.round().toString(),
      },
    ).toString();
  }
}
```

```dart
StreamChat(
  client: client,
  configData: StreamChatConfigurationData(
    imageCDN: const MyImageCDN(),
  ),
  child: child,
)
```

Overriding the cache key is useful when your CDN uses signed URLs: strip the rotating token from the key so the same image stays cached across token refreshes.

---

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