A TinyMCE plugin that lets users browse/pick files from multiple cloud providers and insert them into editor content as links, images, or embeds.
Built-in provider adapters:
No open-source TinyMCE plugin existed that lets users pick files directly from their own Google Drive, OneDrive, or Dropbox accounts without routing files through a paid third-party service. The commercial alternatives (Filestack, Uploadcare) work by copying the selected file to their own CDN — which adds per-month costs, creates a vendor dependency, and moves file ownership away from the user.
This plugin was built to fill that gap: files stay in the user's own cloud account, the plugin just brokers the picker and returns a URL. No CDN, no per-upload quota, no vendor lock-in.
| TinyMCE MultiCloud Plugin | Filestack | Uploadcare | |
|---|---|---|---|
| License / cost | MIT, free | $69–$379+/month | $0–$119+/month |
| File storage | User's own cloud account | Filestack CDN (vendor) | Uploadcare CDN (vendor) |
| Cloud sources | Google Drive, OneDrive, Dropbox, Nextcloud/BayernCloud | Drive, Dropbox, OneDrive, Box, + more | Drive, Dropbox, OneDrive, + more |
| Embed support | ✅ link / image / iframe / audio+video | ✅ via CDN URL | ✅ via CDN URL |
| Files stay in user's account | ✅ Yes | ❌ Copied to vendor CDN | ❌ Copied to vendor CDN |
| Bandwidth quota | None (provider handles delivery) | 75–400 GB/month (plan limit) | Varies by plan |
| Self-hostable | ✅ Fully (drop a JS file) | ❌ SaaS only | ❌ SaaS only |
| Open source | ✅ MIT | ❌ | ❌ |
The trade-off: because files remain in the user's cloud storage, they must be publicly accessible for embedded content to be viewable by readers. The plugin handles this automatically — it creates the public share link via the provider's API so the user does not need to configure sharing manually. A warning is shown in the upload dialog to make clear that the file will be publicly accessible to anyone with the link.
Different cloud providers have different OAuth flows, SDKs, and picker UX constraints. The plugin uses a provider adapter contract and popup bridge protocol, so each provider can implement its own picker while TinyMCE integration stays consistent.
This plugin uses XDBC for Design-by-Contract (DBC) validation of all plugin options and provider configurations. Instead of scattered if/throw guards, every precondition is expressed as a typed contract that fires before any plugin logic runs.
providers map — must be a plain object if provideddefaultProvider — must be a string and must exist in the providers mapdefaultInsertMode — must be "link", "image", or "embed" if providedpopupTimeoutMs — must be a positive number if provideddialogTitle / defaultProvider — must be non-empty strings if providedenabled: true and no pickerUrl override):
clientId and apiKey must be defined, string, and match the API-key patternclientId must be defined, string, and match the API-key patternappKey must be defined, string, and match the API-key patternbaseUrl (valid URL), username, and either password or bearerToken (at least one non-empty)DBC.Infringement (subclass of Error), making them easy to catch and distinguish from runtime errors.globalThis.MultiCloud.Validation.Config — governs configuration contract checksglobalThis.MultiCloud.Validation.Boundary — governs Zod boundary schema checks on provider API dataDBC.Infringement (XDBC's ZOD.tsCheck routes through DBC.reportTsCheckInfringement internally).In addition to XDBC DBC contracts on configuration, the plugin validates all data that crosses provider API boundaries at runtime using XDBC's Zod integration (ZOD.tsCheck from xdbc/src/DBC/ZOD). Zod schemas are defined with the zod library but validation is always run through XDBC — keeping the error shape and behaviour consistent with the rest of the contract layer.
Where DBC validates input configuration before any logic runs, XDBC's Zod implementation validates what providers return before that data is trusted and used.
| Boundary | Schema | Validates |
|---|---|---|
| Any provider result | pickerResultSchema |
item.id, item.name, item.url non-empty; all URL fields are valid URLs; mode is a known enum value |
| Google Picker callback | googleDocSchema |
id required and non-empty; url, thumbnailLink are valid URLs when present |
| OneDrive navigable picker | oneDriveFileSchema |
name required; webUrl, @microsoft.graph.downloadUrl are valid URLs when present; file.mimeType is a string when present |
| Dropbox Chooser callback | dropboxFileSchema |
link required and a valid URL; thumbnailLink is a valid URL when present |
| BayernCloud WebDAV node | webDavNodeSchema |
id, name, url, webdavPath non-empty; url is a valid URL; isDirectory is a boolean |
Because validation runs through ZOD.tsCheck which calls DBC.reportTsCheckInfringement on failure, both DBC contract violations and Zod schema failures throw DBC.Infringement. The boundary layer uses its own DBC instance (globalThis.MultiCloud.Validation.Boundary) so it can be configured independently from config-layer checks.
import { DBC } from "xdbc";
try {
tinymce.init({ plugins: "multicloud", multicloud_providers: myConfig });
} catch (e) {
if (e instanceof DBC.Infringement) {
// either a configuration contract or a provider boundary schema was violated
}
}
npm install
npm run build
| TinyMCE version | Status |
|---|---|
| 6.x | ✅ Tested and supported |
| 7.x | ✅ Tested and supported |
npm install
npm run dev # watches src/ and rebuilds dist/ on change
For local development with real cloud provider SDKs:
demo/multicloud.config.example.js to demo/multicloud.config.js.demo/tinymce-demo.html in a browser (via a local HTTP server, not file://).demo/multicloud.config.example.js to demo/multicloud.config.js.docs/PRODUCTION_SETUP.md.Note:
demo/multicloud.config.jsis gitignored — never commit real credentials to the repository.
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/7/tinymce.min.js"></script>
<script src="./dist/index.global.js"></script>
<textarea id="editor"></textarea>
<script>
tinymce.init({
selector: "#editor",
plugins: "link image media multicloud",
toolbar: "undo redo | bold italic | link image media | multicloud multicloud_upload",
multicloud_providers: {
googleDrive: {
enabled: true,
clientId: "GOOGLE_OAUTH_CLIENT_ID",
apiKey: "GOOGLE_BROWSER_API_KEY",
scopes: ["https://www.googleapis.com/auth/drive.file"]
},
oneDrive: {
enabled: true,
clientId: "ONEDRIVE_CLIENT_ID",
action: "query"
},
dropbox: {
enabled: true,
appKey: "DROPBOX_APP_KEY",
linkType: "preview"
},
bayerncloud: {
enabled: true,
// Option 1: Use interactive picker (prompts user for credentials)
pickerUrl: "./pickers/bayerncloud.html"
// Option 2: Use WebDAV with pre-configured credentials
// mode: "nextcloud-webdav",
// baseUrl: "https://your-nextcloud.example.com",
// username: "your-username",
// password: "app-password", // Use app-specific password
// webdavPath: "", // Optional subfolder
// createPublicShare: true // Creates public share links
}
},
multicloud_default_provider: "googleDrive",
multicloud_default_insert_mode: "link",
multicloud_dialog_title: "Insert From Cloud",
multicloud_popup_timeout_ms: 120000
});
</script>
The plugin provides two toolbar buttons:
multicloud: Opens a file picker to browse and select files from cloud providersmulticloud_upload: Opens a dialog to upload local files to cloud providersSome providers support uploading local files directly to the cloud:
Google Drive: ✅ Full upload support
Nextcloud/BayernCloud: ✅ Full upload support (both modes)
pickerUrl): Opens upload UI in picker, uses OAuth authenticationcreatePublicShare: true)OneDrive: ✅ Full upload support
Dropbox: ✅ Full upload support
localStorage with expiry trackingTo use upload, add multicloud_upload to your toolbar:
toolbar: "undo redo | bold italic | link image media | multicloud multicloud_upload"
Each provider picker page should call window.opener.postMessage with this payload:
{
source: "tinymce-multicloud-plugin",
type: "picked", // or "cancelled"
providerId: "googleDrive",
payload: {
item: {
id: "file-id",
name: "filename.png",
url: "https://...",
embedUrl: "https://..." // optional
},
mode: "image" // "link" | "image" | "embed"
}
}
A mock bridge page is available at demo/picker-bridge-example.html.
clientId, apiKey.OneDrive.open).clientId.Dropbox.choose).appKey.pickerUrl): Opens a popup with Nextcloud Login Flow v2 (OAuth-like), browses files via WebDAV, creates public share linksmode: "nextcloud-webdav"): Uses pre-configured credentials for programmatic file accessPROPFIND to list filesdocs/CLOUDFLARE_WORKER_SETUP.md for free Cloudflare Worker proxy (100k requests/day free)Nextcloud instances block browser requests from different domains. To enable browser-based access:
Option 1: Cloudflare Worker (Recommended - Free)
docs/CLOUDFLARE_WORKER_SETUP.mdcloudflare-worker/nextcloud-proxy.jsOption 2: Same-Origin Deployment
Option 3: Server-Side Integration
Every built-in provider supports pickerUrl. If pickerUrl is set, the plugin opens that custom picker page and uses the bridge contract instead of the built-in SDK flow.
⚠️ Google Drive embeds require the viewer to be signed in to Google
All Google Drive embeds (
drive.google.com/file/d/.../preview) require Google cookies inside the iframe. Modern browsers block third-party cookies by default, so any viewer who is not already signed in to Google in their browser will see a login prompt instead of the file.Uploaded files — the plugin automatically sets sharing to "Anyone with the link" after upload, so the file itself is accessible, but the embed still requires Google cookies.
Picked files — sharing is not changed by the plugin. Files remain at whatever sharing setting they had in Drive (usually "Restricted" / private). Viewers who are not the file owner will not be able to see them at all.
Recommendations:
- For content that must be publicly visible to all readers, use the upload button (↑) rather than the picker (↓), or manually set the file to "Anyone with the link" in Google Drive first.
- To set a file to "Anyone with the link" in Google Drive: right-click the file → Share → click "Restricted" dropdown → select "Anyone with the link" → click Done.
- For truly public embeds with no sign-in requirement, prefer Dropbox or OneDrive — their embed mechanisms do not depend on the viewer having a Google account.
The plugin uses two independent DBC instances — one for configuration checks and one for Zod boundary schema checks — so each layer can be configured separately. By default both layers throw DBC.Infringement on violations.
Use configureMultiCloudValidation with config and/or boundary sub-keys to change the behaviour of either layer:
import { configureMultiCloudValidation } from 'tinymce-multicloud-plugin';
// Soft-log config violations only (useful for hardened production deployments).
// Boundary checks remain strict — unexpected API shapes still throw.
configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
});
// Both layers in soft logging mode:
configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
boundary: { throwOnInfringement: false, logToConsole: true },
});
// Then initialize TinyMCE as usual
tinymce.init({ ... });
Or using the global bundle:
<script src="./dist/index.global.js"></script>
<script>
TinyMceMultiCloudPlugin.configureMultiCloudValidation({
config: { throwOnInfringement: false, logToConsole: true },
});
tinymce.init({ ... });
</script>
| Layer | DBC path | What it covers |
|---|---|---|
config |
globalThis.MultiCloud.Validation.Config |
Plugin options, provider credential shape, defaultProvider contract |
boundary |
globalThis.MultiCloud.Validation.Boundary |
Provider API response shapes (Zod schemas) |
Note:
configureMultiCloudValidationmust be called beforetinymce.init(). Contract checks run at plugin initialization time — once the plugin is registered and your options have been validated, changing these settings has no retroactive effect.
Recommendation: Keep both layers at their default
throwOnInfringement: trueduring development. Soft logging mode forconfigis intended for hardened production deployments where all configuration has been verified. Theboundarylayer should generally stay strict — unexpected shapes in provider API responses indicate a real integration problem.
Open demo/tinymce-demo.html after building. The demo includes mock pickers under demo/pickers/ for all providers.
The demo auto-loads demo/multicloud.config.js if present; otherwise it falls back to local mock picker pages.
How each provider handles different file types. Insert modes: image = <img>, embed = <iframe>, audio = <audio>, video = <video>, link = <a>.
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, webp, bmp, svg, tiff | image | Direct <img> via Drive preview URL |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | audio | <iframe> preview (Drive transcodes audio) |
| Video | mp4, webm, mov, avi, mkv, m4v, wmv, flv | embed | <iframe> preview (Drive transcodes video) |
| embed | <iframe> via Drive preview URL |
||
| Office (OOXML) | docx, xlsx, pptx | embed | <iframe> via Drive preview URL |
| Office (legacy) | doc, xls, ppt | embed | <iframe> via Drive preview URL |
| OpenDocument | odt, ods, odp | embed | <iframe> via Drive preview URL |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, svg, webp, bmp | image | Raw CDN URL via dl.dropboxusercontent.com |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus, oga, weba | audio | <audio> with raw CDN URL |
| Video | mp4, webm, ogg, mov, m4v, avi, wmv, flv, mkv | embed | <video> with raw CDN URL |
| embed | <iframe> via Google Docs Viewer |
||
| Office (OOXML) | docx, xlsx, pptx, doc, xls, ppt | embed | <iframe> via Microsoft Office Online viewer |
| OpenDocument | odt, ods, odp | link | Google Docs Viewer cannot reliably load ODF |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, svg, webp, bmp, tiff, apng, avif | image | Direct embed URL from OneDrive |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | audio | <audio> or <iframe> depending on download URL availability |
| Video | mp4, webm, ogg, mov, m4v, avi, wmv, flv, mkv | embed | <video> or <iframe> |
| embed | <iframe> |
||
| Office (OOXML) | docx, xlsx, pptx, doc, xls, ppt | embed | <iframe> |
| OpenDocument | odt, ods, odp | embed | <iframe> |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
| File type | Extensions | Insert mode | Notes |
|---|---|---|---|
| Images | png, jpg, jpeg, gif, webp, bmp, tiff, apng, avif | image | <img> via public share URL |
| SVG | svg | link | Cross-origin SVG cannot be embedded reliably |
| Audio | mp3, wav, ogg, aac, m4a, flac, opus | link | Cross-origin streaming unreliable |
| Video | mp4, webm, ogg, mov, avi, wmv, flv, mkv | link | Cross-origin streaming unreliable |
| embed | <iframe> via Google Docs Viewer (requires public share) |
||
| Office (OOXML) | docx, xlsx, pptx | embed | <iframe> via Google Docs Viewer (requires public share) |
| Office (legacy) | doc, xls, ppt | link | Viewer support unreliable for legacy formats |
| OpenDocument | odt, ods, odp | link | Viewer support unreliable for ODF |
| Archives | zip, rar, 7z, tar, gz, bz2, xz | link | Download link only |
| Other | anything else | link | Download link |
Note on Nextcloud embedding: Google Docs Viewer fetches files from its own servers, so the Nextcloud share link must be publicly accessible (not password-protected or on a private network). Embedding may fail intermittently due to Google's rate limiting on the viewer service.
This repository gives you:
What you still need per provider: