Skip to content
Dee Wan 1.0 is not yet released (pre-MVP). See the 1.0 product boundary.

Media library

The media library holds a site’s images and files. Each site has its own library. Another site’s files are never shown or matched.

Open Media in the sidebar to manage the library. The same library opens as the Media Center dialog wherever you pick a file:

  • a media field of an item: Media center;
  • rich text: Insert Image → Media Center, or Upload New to upload straight into the text;
  • an image slot of a page section in the page builder.

If the library cannot load, press Try again without closing it.

Pick one or more files and press Use N selected. A media field takes only the kinds of file its model allows, up to its own size. See the media row in field types.

Reading the library follows media read access. Changes need three separate permissions:

Permission Allows
media:upload Add files: uploads, files saved from AI generation, files an import or a plugin brings in, files copied from an external source, and private uploads.
media:update Change what exists: alt text and descriptions, focal points, loading and fetch priority, replacing a file, folders, tags and public or private visibility.
media:delete Delete a file. A file in use is still refused.

The built-in admin and editor roles hold all three. The author role holds media:upload only: an author can add files and cannot change or delete one. The viewer role holds none. A custom role holds exactly what it lists under Settings → Roles. Where a control is unavailable for lack of a permission, it is disabled and says which permission it needs.

Press Upload and choose files. They go into the folder on screen. The dialog states the size limit. Files are public unless you keep them private. See Private files.

Each site chooses what its library takes under Settings → Site → Media files. Turning a kind off keeps the files already in the library.

Kind Formats Default
Images PNG, JPEG, WebP, AVIF, GIF, SVG Always on
Documents PDF, Word, Excel, PowerPoint; also ODF text, spreadsheet and presentation, RTF, Text, CSV On, with PDF and the Office formats
Audio MP3, M4A, Ogg, WAV; also FLAC, AAC Off
Video MP4, WebM; also QuickTime, Ogg video Off
Other files Any other safe file Off
  • Programs, scripts and web pages are never accepted, whatever the setting.
  • The format is checked from the file’s bytes, not only from its name.
  • SVG is an image: select it in image fields, preview it and render it with <img>. It stays vector. The static subset accepts shapes, text, gradients and internal #id references, up to 1 MiB. Scripts, event handlers, style sheets, external resources, animation, entities and unsafe XML are refused. SVG uses R2 even when raster images use Cloudflare Images; public SVG needs IMAGES_BUCKET and R2_PUBLIC_DOMAIN.
  • An installation without public file storage takes images only.

Each kind has its own size limit in the same setting. Every kind starts at 32 MiB.

  • Images: at most 32 MiB.
  • Documents, audio, video and other files uploaded here: the limit can be raised to the installation’s upload bound. That bound is 100 MB unless an operator declares a larger Cloudflare plan limit in MEDIA_REQUEST_BODY_LIMIT_BYTES, at most 5 GB.
  • Formats checked as a whole file (Word, Excel, PowerPoint, ODF, Text, CSV): at most 32 MiB.
  • Ordinary imports, plugins and management API uploads: at most 32 MiB per file. Native .deewan transfers copy files in chunks and follow the destination installation’s file-type and size limits, up to 5 GB where enabled. See Import and migration.

Uploading a file the library already holds uploads nothing. The dialog says Already in the library and offers:

  • Use existing: use the file already there. It keeps its own description and tags.
  • Upload a separate copy: store a second copy.

Do the same for other duplicates in this upload applies your choice to the rest of the batch.

  • Search by file name or alt text.
  • File type: All, Images, Documents, Audio or Video.
  • A folder, or All media and Unfiled.
  • A tag filter. Untagged shows images without tags.

Folders organize the library. They never move or rename a stored file or change its address.

  • Each file is in one folder or in Unfiled.
  • New folder creates one inside the folder on screen. Folders nest at most 5 levels.
  • A site holds at most 500 folders. A name has 1 to 80 characters and is unique under its parent, ignoring case.
  • Folder options holds Rename, Move folder to and Delete folder.
  • Select files and use Move N selected to to move up to 200 files at once.
  • Delete folder removes it and its subfolders after one confirmation. No file is deleted. Its files become Unfiled.

Tags are flat labels on images, separate from folders.

  1. Select images.
  2. In Tags · N selected, type a name in Add a tag and press Add tag. Remove a tag from the selection with its remove button.
  • A tag name has 1 to 64 characters and is unique in the site, ignoring case.
  • Manage tags renames and deletes tags. Deleting a tag never deletes an image. A tag in use asks first and names how many images carry it.
  • Tags are private to the admin. No public API serves them.

Select files and press Edit Selected. Each site language has its own tab.

  • An image needs Alt text in every language of the site, 1 to 125 characters. Save is refused until every language has it: Every language needs alt text before this can be saved. A tab without it says alt text missing.
  • Other files have an optional Description per language.
  • A content item’s version stores one alt text per image, copied from the library when the item is saved. On the ordinary save it is the site’s default-language alt text. The public API serves that stored value as alt. A library change reaches the public API after the item is saved and published again.

Framing decides which part of an image shows. It never changes the file.

  • In an item’s media field, set a Focal point on the image: click the part that must stay visible, then Use this point.
  • When the field has an aspect-ratio rule, the crop follows the field’s shape.
  • Framing belongs to that one use. The same image elsewhere keeps its own framing.
  • Default focal point in Edit Selected starts new uses of the image. Existing uses keep their own focal point and crop.
  • Only images can be framed.

How a website shows framed images is in images on a generated site.

In Edit Selected, Fetch priority sets the browser hint: High, Low or Auto. Loading chooses Eager for immediate loading or Lazy to wait until the image is needed. Leave priority on Auto unless the image needs an explicit preference. These settings do not change the stored file.

  1. Select an image, press Edit Selected, then Crop and rotate.
  2. Rotate with Rotate left and Rotate right. Drag the crop or its corners. On the keyboard, arrows move the crop, Alt and arrows resize it, R rotates.
  3. Press Save as copy, or Enter.

The copy is a new file with its own size and uses. It takes the original’s alt text, folder and tags, and says Edited from {name}. The original and every place that uses it are unchanged. If the library already has the exact result, that file is selected instead.

More actions → Replace original writes the edit over the original file.

  • First it lists the items that use the image. Their published pages will show the edited image.
  • It also counts uses in places you cannot open.
  • When the new file has a different shape, it says how many items frame the image with a crop or focal point, so you can check them.

A linked file from an external source cannot be replaced or edited.

Scripts and agents use POST /api/manage/v1/media/{id}/replace with multipart file and an idempotency key. The media id and content references stay the same; the delivery URL may change. Use the returned URL. Published versions that pin an older object keep those bytes. See Management API.

Image format in Edit Selected converts an image to WebP or AVIF, or leaves the current format unchanged. The conversion runs in your browser. Saving it replaces the original, so it shows the same where-used review first.

Before a replace or a delete, the dialog says where the file is used: Nothing references it. or Still used by {count} content reference(s): {names}.

  • A use in content you cannot read is counted, never named.
  • Older versions an item still keeps also count as uses. See how many versions are kept.

Press Delete image (or Delete file) on the file.

  • Deleting needs media:delete. Deleting a private file removes it from private storage.
  • A file in use cannot be deleted: This image is in use. Update the content that uses it first. Retained versions can keep it in use until they are trimmed.
  • A file that starts being used during the delete is kept.
  • Deleting asks first and cannot be undone.

Files are public by default: anyone with a file’s address can open it, and content can use it. A private file is stored apart from the public site, has no public address, and opens only through a link made for you.

  • Needs private storage. An operator binds a private R2 bucket as PRIVATE_MEDIA_BUCKET (see backend/wrangler.example.jsonc). It must be its own bucket, with no public domain or public access: the public media bucket must not hold private files. Without it, Keep new files private is not offered and Make private is disabled with This installation has no private file storage. Setup does not create this bucket and does not check that it is private.
  • Upload privately. Tick Keep new files private before choosing files. It applies to the uploads that follow, including large files uploaded as a stream. Files a plugin or an external source adds are always public. A site copy or history file brings private files in as private.
  • See what is private. A private file shows a Private badge in the library and in its details.
  • Open or download. Press Open or Download in a private file’s details. Each press asks the server for a link that opens that one file of this site and expires after 5 minutes. The library’s previews and the audio and video players ask for the same kind of link. Any member who can read the library can ask for one. Nothing else serves the file: the public media address does not hold it, and image transformations cannot reach it. If a private audio or video preview cannot load, choose Try again. If Open or Download fails, Try again repeats that action and keeps your edits.
  • Protected image previews. Private raster uploads create WebP thumbnails (up to 320 px) and previews (up to 1280 px) in the same private bucket. Each is at most 2 MiB. Delivery checks the parent file and uses its short-lived link; neither the source nor a preview has a public URL. Older images or failed local conversions fall back to the original. In file details, Refresh private previews retries generation; it needs media:update. SVG stays vector and uses its original. Site copies and generated application transfers carry existing previews with checksums. Replacing or deleting an unreferenced private object removes its previews too.
  • Protected rich content. A private model can use private images, galleries and downloadable attachments in ordinary rich-text fields, including rich children inside plugin blocks. The editor opens them through the same protected library delivery. Public models, page sections, snippets, social images and Site Chrome still refuse private media (media_private). A private model requires an API token and cannot become public while any retained version pins a private file (model_holds_private_media). API answers keep file identities and withhold storage locators. Generated applications read rich-content files through the owning row’s guarded file route; see the next point.
  • Site copies keep private files private. A site copy and a history file carry every private file with its bytes, checksum, details and visibility. The import stores each one in the destination’s private storage, under the private provider, and never in the public bucket; undo removes it from there. A private model arrives private. If this installation has no private file storage, the check blocks with private_storage_unavailable before anything is written. Building the file needs read access to the media library (the same access that opens a private file) as well as data:export when the site holds any private file (export_private_media_forbidden); media:update is not needed. Access is checked again on every download. Content-only exports (CSV, JSON, XLSX) never depend on private library files. If private storage is not bound, a site-copy or history export is refused by name without building a file (export_private_storage_unavailable). No storage credential or bucket name travels.
  • A generated application serves its own private files. When you exit to your own repository with private files, they are copied into your own private bucket (never the public one) and the row’s address column holds a private-file: marker, not a URL. The application serves a file at GET /api/<model>/files/<field>/<row id>, only to a caller who may read that row under the model’s own access: your account sessions, your service credential or your read tokens. Membership of the CMS library proves nothing there. Each request is checked again; nothing expires and nothing is minted. The Worker reads the bytes from its PRIVATE_FILES bucket; the Node server reads them from the directory in PRIVATE_FILES_DIRECTORY, and without either it answers 503 private_files_unavailable. See Standalone backend reference.
  • Duplicates. The duplicate check compares every file in the library, public or private.

For a rich-text field, use GET /api/<model>/files/<field>/<row id>?media=<media id> and add &variant=thumb or &variant=preview for a protected image variant. The generated src/private-files.ts exports privateRichText(html, endpoint, rowId) to replace stored private markers with these same-origin, session-protected URLs before display. It rewrites only identity-bearing media from that field; the route reads the owning row again for every request.

Select the file, press Edit Selected and use Visibility. It needs media:update.

Changing visibility never just flips a flag. The server copies the file to the other store under a new key, checks that the copy has the same length and SHA-256 as the recorded original, and only then switches the file over. After that it removes the old copy. The control shows Copying and checking the file… while it works, then says what happened:

  • The file is private now. The public copy was removed. (or the reverse for public).
  • The change is saved. The old copy was kept because retained history still names it.
  • The change is saved, but the old copy could not be confirmed removed. It may still be reachable at its old address until it is deleted from storage. Copies a browser or a cache already holds are outside the library’s reach.
  • The file was not moved. {reason} when the copy failed or did not match: nothing changed and the unfinished copy was removed. If it could not be removed, the message says so.

Making a file private is refused while any content or retained version uses it (media_in_use); update that content first. A file kept by an external media source cannot be made private.

A plugin can offer media sources. Then the dialog shows Sources beside Library.

  1. Pick a source, search, and use Show more for more results.
  2. Add a file:
    • Add copies the file into your library. It follows the same kinds, formats, size limits and duplicate check as an upload. It keeps the source’s credit.
    • Link without copying keeps the file with the source. The library holds only a link to it.

You can edit a linked file’s details, tags, folder and framing as you would for any other file. Its bytes cannot be replaced or edited. Deleting it removes only the library entry. If the plugin is turned off or removed, existing content keeps the stored address, the details say The source is not available now., and new links from it are refused.

A copied file’s credit is saved with the item and served by the public API as credit. Show it under the image.

Press Generate in the library.

  1. Describe the image. Choose the Model and the Aspect Ratio (9:16, 1:1 or 16:9) under Settings.
  2. Press Generate. Regenerate and Generate New try again. Nothing is saved yet.
  3. Press Save to add it to the library, in the folder on screen. The duplicate check applies.

Generating needs ai:generate. Saving needs media:upload. A generated image is always public. The installation needs an AI provider set up. Generation counts against the site’s hourly AI allowance.

Use files in content and on pages.

Open the image details. Choose a language and a vision model from the live catalogue, then choose Suggest alt text and caption. The image bytes go to the configured AI provider; private images stay private. Review and edit both suggestions. Apply and save descriptions replaces the saved text for that language. Discarding the suggestions keeps the author’s text. Concurrent metadata edits block the apply. After an upload, the optional Review AI descriptions after upload control opens the selected files for review. Inference accepts JPEG, PNG and WebP images, up to 10 MiB.

Open Media in the sidebar to manage files without choosing one for an item. File pickers use the same library.

In Asset fields, a site administrator can add up to 24 text, number, boolean, select, date, media or relation fields. Localized stores separate language values; other fields share one value. Fields start private. Enable Public only for values the website should read. Field configuration requires site:settings; changing a file’s values requires media:update.

Open a file’s Asset metadata, choose a language and enter values. The editor’s footer Save saves custom metadata together with descriptions and file options for the selected files. There is no separate metadata save button. Metadata history offers retained revisions. History keeps up to 50 revisions within 512 KiB for current values and history together; the oldest whole revisions are removed first. Current values are never truncated. Changing a field’s type, configuration, language mode or public setting makes retained values unavailable for public projection and historical restore until you enter values under the current definition.

Relations follow the current site’s model, document and language access. Public values can reference only public media and published public entries. Private metadata and its history are absent from ordinary media responses and public content. Public content exposes declared values under mediaMetadata, keyed by asset ID. This metadata follows the asset’s current revision, including when many entries reuse the file. Generated website artifacts capture the projection at generation time; regenerate to update it. Version-pinned file bytes, framing and descriptions keep their existing behavior.

Native site copies carry asset fields, values and retained history, remapping carried entry and media references. A copy does not overwrite metadata on an existing destination file. Undo keeps a changed or still-used file with its metadata, and refuses dependent cleanup; this includes retained references and reference cycles. Field definitions still used by retained values stay too. Public-cache clearing failures are shown after saving.

If metadata cannot be saved, the editor stays open with your draft and opens Asset metadata to show the error. Correct the values and press the footer Save again. If the fields or values changed elsewhere, choose Reload to discard the local metadata edits and load the current version, then enter your changes and retry Save.

Open Media → Edit Selected → Localized originals. Choose Language, then Replace file. Remove original returns that language to its fallback. Both actions require media:update. The asset ID, references and language-specific descriptions stay the same.

Resolution order: requested language, configured language fallbacks, default original. Published versions pin their exact files. Replacing or removing a language original keeps files needed by retained versions. Private originals use the ordinary private-file access rules. Site copies and full-history transfers carry explicit original languages and retained files.

Management operations: listMediaOriginals, replaceMediaOriginal, removeMediaOriginal. Replacement is multipart file plus expected_object_id; use the empty string when that language has no original. Removal names the current expected_object_id. A changed pointer returns 409 media_changed. Default-original replacement keeps its existing operation.


Unreleased / pre-MVP; no stable version
Source revision: fe23db481c422b40e317b92fc98116b4c0c26a87 (modified checkout)
Build date (UTC): 2026-10-18T09:22:34.630Z
Upgrade target: unavailable: modified checkout. Release notes