External Storage
Manage all image assets through your own storage backend using presigned URLs, custom asset browsing, and folder management.
- React
- Vue
- Angular
- Vanilla JS
import { useRef } from "react";
import { DragbleEditor, DragbleEditorRef } from "dragble-react-editor";
function ExternalStorageEditor() {
const editorRef = useRef<DragbleEditorRef>(null);
return (
<DragbleEditor
ref={editorRef}
editorKey="db_your_key"
editorMode="email"
options={{
assetStorage: {
mode: "external",
getPresignUrl: async (file) => {
const res = await fetch("/api/storage/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
const { uploadUrl, assetUrl } = await res.json();
return { uploadUrl, assetUrl };
},
onUpload: async (file, folder) => {
const formData = new FormData();
formData.append("file", file);
formData.append("folder", folder ?? "/");
const res = await fetch("/api/storage/upload", {
method: "POST",
body: formData,
});
const { url, id } = await res.json();
return { url, id };
},
onLoadAssets: async (folder, page) => {
const res = await fetch(
`/api/storage/assets?folder=${folder}&page=${page}`,
);
return await res.json();
// Expected: { assets: [{ id, url, name, thumbnail }], hasMore: boolean }
},
onDeleteAsset: async (assetId) => {
await fetch(`/api/storage/assets/${assetId}`, { method: "DELETE" });
},
},
}}
/>
);
}
<template>
<DragbleEditor
ref="editorRef"
editor-key="db_your_key"
editor-mode="email"
:asset-storage="storageConfig"
/>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { DragbleEditor } from "dragble-vue-editor";
const editorRef = ref<InstanceType<typeof DragbleEditor>>();
const storageConfig = {
mode: "external" as const,
getPresignUrl: async (file: File) => {
const res = await fetch("/api/storage/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, contentType: file.type }),
});
return await res.json(); // { uploadUrl, assetUrl }
},
onLoadAssets: async (folder: string, page: number) => {
const res = await fetch(
`/api/storage/assets?folder=${folder}&page=${page}`,
);
return await res.json();
},
onDeleteAsset: async (assetId: string) => {
await fetch(`/api/storage/assets/${assetId}`, { method: "DELETE" });
},
};
</script>
import { Component, ViewChild } from "@angular/core";
import { DragbleEditorComponent } from "dragble-angular-editor";
import type { AssetStorageConfig } from "dragble-types";
@Component({
selector: "app-external-storage",
standalone: true,
imports: [DragbleEditorComponent],
template: `
<dragble-editor
#editor
editorKey="db_your_key"
editorMode="email"
[assetStorage]="storageConfig"
></dragble-editor>
`,
})
export class ExternalStorageComponent {
@ViewChild("editor") editorComp!: DragbleEditorComponent;
storageConfig: AssetStorageConfig = {
mode: "external",
getPresignUrl: async (file) => {
const res = await fetch("/api/storage/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, contentType: file.type }),
});
return await res.json();
},
onLoadAssets: async (folder, page) => {
const res = await fetch(
`/api/storage/assets?folder=${folder}&page=${page}`,
);
return await res.json();
},
onDeleteAsset: async (assetId) => {
await fetch(`/api/storage/assets/${assetId}`, { method: "DELETE" });
},
};
}
dragble.init({
containerId: "editor-container",
editorKey: "db_your_key",
editorMode: "email",
options: {
assetStorage: {
mode: "external",
getPresignUrl: async (file) => {
const res = await fetch("/api/storage/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file.name, contentType: file.type }),
});
return await res.json();
},
onLoadAssets: async (folder, page) => {
const res = await fetch(
`/api/storage/assets?folder=${folder}&page=${page}`,
);
return await res.json();
},
onDeleteAsset: async (assetId) => {
await fetch(`/api/storage/assets/${assetId}`, { method: "DELETE" });
},
},
},
});
Presigned URL flow
When getPresignUrl is provided, the editor uploads files directly to your storage:
- User selects a file
- Editor calls
getPresignUrl(file)to get a presigned upload URL - Editor
PUTs the file directly to that URL - Editor uses the returned
assetUrlto display the image
:::tip S3-compatible storage This pattern works with AWS S3, Google Cloud Storage, Cloudflare R2, and any S3-compatible provider. Generate presigned PUT URLs on your backend. :::
Folder management
The onLoadAssets callback receives a folder parameter. Return assets filtered by folder to enable browsing:
onLoadAssets: async (folder, page) => {
// folder is '/' for root, '/products/' for a subfolder, etc.
const res = await fetch(`/api/assets?folder=${encodeURIComponent(folder)}&page=${page}`);
const data = await res.json();
return {
assets: data.items, // [{ id, url, name, thumbnail, folder }]
hasMore: data.hasNextPage,
folders: data.subfolders, // ['products', 'banners', 'icons']
};
},
:::warning Pagination required
onLoadAssets must support pagination via the page parameter. The editor requests page 1 initially and increments as the user scrolls. Return hasMore: false on the last page.
:::
Callback reference
| Callback | Parameters | Returns | Description |
|---|---|---|---|
getPresignUrl | (file: File) | Promise<{ uploadUrl, assetUrl }> | Get a presigned URL for direct upload |
onUpload | (file: File, folder?: string) | Promise<{ url, id }> | Alternative: upload through your API |
onLoadAssets | (folder: string, page: number) | Promise<{ assets, hasMore, folders? }> | Load assets for the file manager |
onDeleteAsset | (assetId: string) | Promise<void> | Delete an asset from storage |
Next steps
- Image Upload — simpler upload configuration without full storage control
- Custom Fonts — host fonts on your own storage
- Export HTML — ensure exported HTML references your asset URLs