QuqManager documentation
QuqManager is a CDN-hosted file manager that your product can open on demand, authorize by project API key, and use to insert selected files into editors or custom workflows.
File manager UI
Open the bundled manager from your app without shipping the full core in every integration bundle.
Secure project access
Use a public project API key in the browser while keeping validation secrets on the backend.
Editor integrations
Insert images, PDFs, audio, video, and documents into CKEditor, TinyMCE, or your own content flow.
Quickstart
Open the CDN file manager
Use the loader as the small integration layer in your app. It loads the CDN core bundle only when the picker opens, sends the project API key to the file API, and returns the selected files to your code.
Small app import
Your app imports the loader, not the full manager UI bundle.
Fixed CDN core
The loader fetches the deployed core from cdn.letsoft.dev on demand.
Browser-safe values
Use apiUrl and project apiKey here. Keep validation secrets on the backend.
import { QuqManager } from '@letsoft/quq-loader'
await QuqManager.open({
apiUrl: 'http://localhost:3000',
apiKey: 'qk_test_project_key',
multiple: true,
onSelect(files) {
console.log(files)
}
})
TypeScriptArchitecture
Core bundle, integrations, markup
CDN core
The bundled web component lives on your CDN.
Project API key
Browser integrations authenticate requests by project.
Editor integrations
Adapters can open QuqManager from product surfaces.
Inserted markup
Selected files become editor HTML.
CKEditor 5 plugin
Use QuqManager from CKEditor 5
Add QuqManagerPlugin to your editor and expose the quqInsertFile toolbar item. In bundled apps install the package from NPM. In browser/CDN integrations load the browser-ready plugin build from your CDN.
For GPL usage, the browser/CDN example loads CKEditor 5 from the NPM ESM bundle and loads QuqManager from cdn.letsoft.dev. Do not use cdn.ckeditor.com with licenseKey: 'GPL'; CKEditor Cloud CDN requires a Cloud-compatible license.
Angular examples use Angular 19.2.19 as the minimum supported version for the current CKEditor Angular wrapper. Newer Angular versions are supported by the wrapper when the app also satisfies the Angular CLI Node.js requirements.
import { Alignment, Bold, ClassicEditor, Essentials, Image, ImageCaption, ImageResize, ImageStyle, ImageTextAlternative, ImageToolbar, Italic, Paragraph, Undo } from 'ckeditor5'
import { QuqManagerPlugin } from '@letsoft/quq-ckeditor5'
ClassicEditor.create(document.querySelector('#editor'), {
licenseKey: 'GPL',
plugins: [Essentials, Paragraph, Bold, Italic, Undo, Alignment, Image, ImageCaption, ImageResize, ImageStyle, ImageTextAlternative, ImageToolbar, QuqManagerPlugin],
toolbar: ['undo', 'redo', '|', 'bold', 'italic', '|', 'alignment', '|', 'quqInsertFile'],
image: {
toolbar: [
'imageTextAlternative',
'toggleImageCaption',
'|',
'imageStyle:inline',
'imageStyle:alignLeft',
'imageStyle:alignCenter',
'imageStyle:alignRight',
'|',
'resizeImage'
],styles: {
options: ['inline', 'alignLeft', 'alignCenter', 'alignRight']
},
resizeOptions: [
{ name: 'resizeImage:original', value: null, label: 'Original' },
{ name: 'resizeImage:15', value: '15', label: '15%' },
{ name: 'resizeImage:50', value: '50', label: '50%' },
{ name: 'resizeImage:100', value: '100', label: '100%' }
]
},
alignment: {
options: ['left', 'center', 'right']
},
quqManager: {
apiUrl: 'http://localhost:3000',
apiKey: 'qk_test_project_key',
multiple: true,
accept: ['image/*', 'application/pdf', 'audio/*', 'video/*'],
videoToolbar: ['quqVideoResize15', 'quqVideoResize50', 'quqVideoResize100']
}
})TypeScriptNext.js fallback
Use this only if a production Next.js build reports ckeditor-duplicated-modules or CKEditor modules are bundled twice. The regular Next.js example should work without a custom config.
import { withQuqCkeditorNextConfig } from '@letsoft/quq-ckeditor5/next'
const nextConfig = withQuqCkeditorNextConfig()
export default nextConfigJavaScriptTinyMCE plugin
Use QuqManager from TinyMCE
Import @letsoft/quq-tinymce/auto, add quqManager to TinyMCE plugins, and place quqInsertFile in the toolbar. The auto entry registers QuqManager and loads the TinyMCE modules needed by the examples.
import '@letsoft/quq-tinymce/auto'
import type tinymce from 'tinymce'
declare global {
interface Window {
tinymce: typeof tinymce
}
}
window.tinymce.init({
selector: '#editor',
license_key: 'gpl',
plugins: 'image link media quqManager',
toolbar: 'undo redo | bold italic link | image media quqInsertFile',
object_resizing: true,
quqManager: {
apiUrl: 'http://localhost:3000',
apiKey: 'qk_test_project_key',
multiple: true,
accept: ['image/*', 'application/pdf', 'audio/*', 'video/*'],
imageToolbar: 'quqalignleft quqaligncenter quqalignright | quqimageresize25 quqimageresize50 quqimageresize100',
videoToolbar: 'quqalignleft quqaligncenter quqalignright | quqvideoresize15 quqvideoresize50 quqvideoresize100',
audioToolbar: 'quqalignleft quqaligncenter quqalignright | quqaudioresize15 quqaudioresize50 quqaudioresize100'
}
})TypeScriptQuill module
Use QuqManager from Quill
Register QuqManagerModule with Quill, add quqInsertFile to the toolbar, and configure the same project API values used by the standalone loader.
Images
Inserted as selectable QuqManager media blocks. They support contextual align controls, 15/50/100 quick sizes, and drag resize handles.
Audio
Inserted at 50% width by default. Selecting or hovering the audio block shows the same contextual align and resize controls, including draggable side and corner handles.
Video
Inserted at 100% width by default. Video blocks use contextual alignment, 15/50/100 quick sizes, and drag handles capped at the editor width.
The contextual media toolbar is positioned automatically. If the selected media is the first editor element and there is no room above it, the toolbar opens below the media instead of being hidden under the Quill toolbar.
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import { registerQuqManagerModule } from '@letsoft/quq-quill'
registerQuqManagerModule(Quill)
new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic'],
[{ align: [] }],
['quqInsertFile']
],
quqManager: {
apiUrl: 'http://localhost:3000',
apiKey: 'qk_test_project_key',
multiple: true,
accept: ['image/*', 'application/pdf', 'audio/*', 'video/*']
}
}
})TypeScriptTiptap extension
Use QuqManager from Tiptap
Add QuqManager to your Tiptap extensions and call editor.commands.openQuqManager() from your toolbar. Selected images, audio, and video are inserted as selectable Tiptap nodes with contextual alignment, 15/50/100 quick sizes, and drag resize handles.
Images
Inserted at 100% width and selectable as media nodes with alignment and resize controls.
Audio
Inserted at 50% width by default, visible in the editor, selectable, alignable, and resizable.
Video
Inserted at 100% width with controls, contextual toolbar, and max-width clamped to the editor.
The CDN/browser example uses an import map for Tiptap peer dependencies. Bundled app examples install Tiptap from NPM and import the QuqManager extension directly.
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { QuqManager } from '@letsoft/quq-tiptap'
import './styles.css'
const editor = new Editor({
element: document.querySelector('#editor')!,
extensions: [
StarterKit,
QuqManager.configure({
apiUrl: 'http://localhost:3000',
apiKey: 'qk_test_project_key',
multiple: true,
accept: ['image/*', 'application/pdf', 'audio/*', 'video/*']
})
],
content: '<p>Select a file from QuqManager to test media alignment and resize controls.</p>'
})
document.querySelector('[data-action="bold"]')?.addEventListener('click', () => {
editor.chain().focus().toggleBold().run()
})
document.querySelector('[data-action="italic"]')?.addEventListener('click', () => {
editor.chain().focus().toggleItalic().run()
})
document.querySelector('[data-action="quq"]')?.addEventListener('click', () => {
editor.chain().focus().run()
editor.commands.openQuqManager()
})TypeScriptSelf-hosted API
Run the file API on your own server
QuqManager frontend integrations talk to a file API. For self-hosted projects, run the public quq-self-hosted-api service on your own server, point project backendUrl or integration apiUrl to it, and keep project validation secrets server-side.
Stores files locally
Uploads live in UPLOADS_DIR. Metadata, activity, stars, and trash state live in DATA_DIR.
Validates with platform API
Every browser request sends x-api-key. The self-hosted API verifies it through backend-pro using VALIDATION_SECRET.
Serves static files
The API exposes selected assets through /files while protected file operations stay under /api.
1. Prepare the project
Requirements
- Linux server only. Windows Server is not supported.
- Recommended server OS: Ubuntu or Debian.
- Node.js 20+ and npm if running manually with PM2.
- Docker and Docker Compose if running containers.
- Nginx and Certbot only when exposing the API through HTTPS.
- A QuqManager project with an API Key and Validation Secret.
- A public HTTPS domain for production, for example
files.example.com.
Project settings
- Copy the project API Key into browser integrations only.
- Copy Validation Secret into the server
.envonly. - Add your website domain to Allowed origins.
- Use
Allow localhostonly for local development.
2. Configure environment
Run scripts/setup.sh on the server. The wizard creates .env, prepares storage directories, asks how the API should stay alive, checks required server tools at the stage where they are needed, and can start Docker Compose or PM2 for you. Do not expose this file publicly and do not put VALIDATION_SECRET into frontend code.
git clone git@github.com:LetSoftDev/quq-self-hosted-api.git
cd quq-self-hosted-api
chmod +x scripts/setup.sh
./scripts/setup.sh
# Choose Docker Compose or PM2 in the Runtime step.shellThe wizard asks for the required runtime values and writes them for you. It detects the Linux distribution, explains what each missing dependency is for, offers automatic installation on supported Linux families, and prints a manual fallback for unknown systems or skipped installs. If your deployment needs it, you can also set the same runtime values manually instead of using the wizard: port, uploads directory, max file size, node environment, and Validation Secret. The local metadata directory is created automatically.
Docker Compose
Recommended for production containers. The wizard checks Docker and Compose, can install Docker on supported Linux distributions, can build and start Compose, attach logs, or print docker compose up/logs commands for later.
PM2 process manager
Recommended when running directly on Node.js. The wizard checks Node.js, npm, and PM2, can install missing tools on supported Linux distributions, install dependencies, build, start, configure startup, save the process list, or print manual commands for later.
Skip for now
Writes .env and prepares storage only. Use this when another deploy system starts the API.
Nginx setup
After runtime selection, the wizard explains the reverse proxy step, checks nginx, can install and start nginx on supported Linux distributions, asks for a public domain, reads the local API port and upload limit from runtime values, verifies DNS against this server, generates nginx config, and optionally checks Certbot before issuing a Let’s Encrypt certificate.
3. Nginx setup
In production, expose the API through nginx and set the integration apiUrl to https://files.example.com/api. The wizard explains the nginx step, checks nginx only after you opt in, reads the local API port and upload limit from the runtime values, checks whether the domain A record points to the current server, writes the reverse proxy config, and checks Certbot only if you choose to issue a Let’s Encrypt certificate.
If DNS is not linked yet, create an A record from your domain to the server public IP, wait for propagation, then rerun the wizard before issuing the certificate.
You can also skip the wizard and configure nginx or another reverse proxy manually. In that case, proxy the public domain to the local API port, keep upload limits aligned with MAX_FILE_SIZE, and issue the HTTPS certificate through your normal deployment process.
./scripts/setup.sh
# Choose "yes" at the Nginx setup step.shell4. Update the API
When a new self-hosted API version is available, run the update script from the server checkout. It verifies that tracked files do not have local changes, fetches and pulls the current git branch with fast-forward only, refreshes npm packages, builds the API, and can restart Docker Compose or PM2. Runtime files such as .env, uploads, and data are not modified by the updater.
chmod +x scripts/update.sh
./scripts/update.sh
# Or:
npm run updateshell5. Verify the server
After runtime and nginx setup, run the bundled health check. It reads PORT from .env by default, or you can pass the public API URL after HTTPS is configured. A healthy server returns { "status": "ok" }. If the check fails, inspect runtime logs manually.
npm run health
# Or check the public API URL after HTTPS is configured:
npm run health -- https://files.example.comshell# Docker Compose
docker compose logs -f api
# PM2
pm2 logs quq-self-hosted-apishell6. Connect frontend integrations
Use the self-hosted API URL in every loader/editor example. The browser receives only apiUrl and project apiKey. The self-hosted API reads VALIDATION_SECRET from its own environment and validates the request against backend-pro.
Health check
GET https://files.example.com/health should return { "status": "ok" }.
File endpoint
GET https://files.example.com/api/list requires x-api-key and should return files for valid project keys.
Static files
Selected files are served from https://files.example.com/files/... when they exist on disk.
Update safely
- Pull the public repository on the server.
- Run tests or build locally before restart when possible.
- Restart Docker Compose or reload PM2 after the new build is ready.
- Keep
uploads/anddata/outside disposable deploy directories.
Troubleshooting
- 503 Validation service unavailable: check
VALIDATION_SECRETand backend-pro availability. - 401 Invalid API key: check the project API Key, Allowed origins, and localhost setting.
- Uploads fail: align
MAX_FILE_SIZEwith proxy upload limits. - Selected files 404: verify that
UPLOADS_DIRis mounted and that files still exist on disk.
Auth & API keys
Use project scoped credentials
Browser integrations send the project API key to the file API. Backend validation stays server-side through the validation secret and never ships to the browser.
Allowed origins
Allow only trusted hosts
Configure project origins for deployed domains and localhost development. Requests from other origins should fail validation before the file API is used.
Troubleshooting
Common integration checks
If the picker opens but files fail to load, verify apiUrl, project API key, CORS origin, and that the CDN core URL returns the IIFE bundle.
Configuration
Required runtime values
apiUrlBackend Simple file API endpoint.
apiKeyProject API key used by the file API.
acceptOptional MIME filters for the picker.
File manager options
Control picker behavior
Pass these options to QuqManager.open() or to editor plugin configuration when you need to shape picker behavior for a specific product surface.
apiUrlstringFile API endpoint. Usually your backend-simple URL plus /api.
apiKeystringProject API key used by the browser integration.
multiplebooleanAllows selecting more than one file before confirming.
acceptstring[]Optional MIME type or extension filters, for example image/* or .pdf.
initialPathstringOptional folder path that the picker should open first.
lang'en' | 'ru'Optional UI language for the manager.
mediaToolbarbooleanQuill and Tiptap. Set to false to hide contextual align and quick-size controls.
mediaResizerbooleanQuill and Tiptap. Set to false to disable drag handles on image, audio, and video blocks.
mediaToolbarItemsMediaToolbarItem[]Quill and Tiptap. Controls which contextual media toolbar actions are shown: left, center, right, 15%, 50%, and 100%.
Events
onSelect(files)Called after the user confirms selected files. Receives an array of selected file objects.
onClose()Called when the manager window closes without requiring a selection.