Developer docsCDN first

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)
  }
})
TypeScript

Architecture

Core bundle, integrations, markup

1

CDN core

The bundled web component lives on your CDN.

2

Project API key

Browser integrations authenticate requests by project.

3

Editor integrations

Adapters can open QuqManager from product surfaces.

4

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']
  }
})
TypeScript

Next.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.

next.config.mjs
import { withQuqCkeditorNextConfig } from '@letsoft/quq-ckeditor5/next'

const nextConfig = withQuqCkeditorNextConfig()

export default nextConfig
JavaScript

TinyMCE 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'
  }
})
TypeScript

Quill 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/*']
    }
  }
})
TypeScript

Tiptap 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()
})
TypeScript

Self-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 .env only.
  • Add your website domain to Allowed origins.
  • Use Allow localhost only 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.

terminal
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.
shell

The 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.

terminal
./scripts/setup.sh
# Choose "yes" at the Nginx setup step.
shell

4. 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.

terminal
chmod +x scripts/update.sh
./scripts/update.sh

# Or:
npm run update
shell

5. 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.

terminal
npm run health

# Or check the public API URL after HTTPS is configured:
npm run health -- https://files.example.com
shell
terminal
# Docker Compose
docker compose logs -f api

# PM2
pm2 logs quq-self-hosted-api
shell

6. 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/ and data/ outside disposable deploy directories.

Troubleshooting

  • 503 Validation service unavailable: check VALIDATION_SECRET and backend-pro availability.
  • 401 Invalid API key: check the project API Key, Allowed origins, and localhost setting.
  • Uploads fail: align MAX_FILE_SIZE with proxy upload limits.
  • Selected files 404: verify that UPLOADS_DIR is 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

apiUrl

Backend Simple file API endpoint.

apiKey

Project API key used by the file API.

accept

Optional 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.

apiUrlstring

File API endpoint. Usually your backend-simple URL plus /api.

apiKeystring

Project API key used by the browser integration.

multipleboolean

Allows selecting more than one file before confirming.

acceptstring[]

Optional MIME type or extension filters, for example image/* or .pdf.

initialPathstring

Optional folder path that the picker should open first.

lang'en' | 'ru'

Optional UI language for the manager.

mediaToolbarboolean

Quill and Tiptap. Set to false to hide contextual align and quick-size controls.

mediaResizerboolean

Quill 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.