Skip to content

Application-managed folders

An application-managed folder is a folder your backend marks managed. The folder and everything below it stay visible to the people who may view them, but people cannot change them: only your application’s backend clients can. This page explains why and when to use the mark, what people can and cannot do, and how a mark ends. The first half is for backend developers; In the Files console is for administrators.

Your application keeps references to the files it stores: a post lists its pictures, a contract its signed copy. If a person renames, moves, replaces or deletes one of those files in the Files console, your application shows a missing or wrong file, and nothing tells it why. A managed folder prevents that. For people it is read-only, whoever they are: members, holders of files.manage and the tenant owner alike. Your backend keeps full control.

Use a managed folder for files whose place and content your application decides: media attached to records, generated documents, synced copies. Leave folders that people organise by hand unmanaged.

  • One managed top folder per kind of record, created by your backend with managed: true: /posts, /contracts. Everything your backend creates below it is managed through it, so you mark one folder, not each record’s.
  • One folder per record below it, such as /posts/<post id>, with your record’s id as its external reference. You find it again with GET P/folders/by-external-ref/:external_ref, without storing a folder id.
  • Store the file id in your database, never the path. Ids never change; paths change with a rename or a move. See What to store.
  • Upload through your backend. People cannot create upload tickets inside a managed folder. Your backend creates the ticket and completes it with its backend client, and your app only sends the bytes (Uploading on behalf of users), or your backend uploads the bytes itself.
managed-folders.ts
import { isFolderManagedError, type File, type Folder } from '@central-code-station/ccs-files-client';
import { db } from '../shared/app';
import { files } from './files-client';
// One managed top folder per kind of record: people see it in the console but cannot change it.
async function postsFolder(): Promise<Folder> {
const { items } = await files.folders.listByExternalRef('posts:root');
if (items.length > 0) return items[0];
return files.folders.create({ parent_id: 'root', name: 'posts', external_ref: 'posts:root', managed: true });
}
// /posts/<post id>, managed through its parent and found again by your own id.
async function postFolder(postId: string): Promise<Folder> {
const ref = `post:${postId}`;
const { items } = await files.folders.listByExternalRef(ref);
if (items.length > 0) return items[0];
const parent = await postsFolder();
return files.folders.create({ parent_id: parent.folder_id, name: postId, external_ref: ref });
}
// Your backend uploads as before. Keep the file id in your database, never the path.
export async function attachToPost(postId: string, authorId: string, bytes: Buffer, name: string, mimeType: string): Promise<File> {
const folder = await postFolder(postId);
const file = await files.upload(bytes, { name, mime_type: mimeType, folder_id: folder.folder_id, owner_id: authorId, acting_user_id: authorId, external_ref: `post:${postId}` });
await db.posts.addMedia(postId, { file_id: file.file_id, kind: file.kind, mime_type: file.mime_type, name: file.name });
return file; // file.managed is true; file.managed_by.managed_folder_id is the posts folder
}
// Nobody, your backend included, creates a share link inside a managed folder.
export async function shareLink(fileId: string): Promise<string | null> {
try {
const share = await files.shares.create({ fileId }, { actions: ['view'] });
return share.url;
} catch (error) {
if (isFolderManagedError(error)) return null; // error.managedFolderId names the folder that carries the mark
throw error;
}
}
Machine scope: files:manage
Route Does
POST P/folders with managed: true Creates the folder already managed
PATCH P/folders/:folder_id with managed: true Marks an existing folder. A folder that already carries a mark keeps it unchanged
PATCH P/folders/:folder_id with managed: false Removes the mark. No reason is needed

Only a machine token with files:manage sets managed. A user token that sends it, true or false, the tenant owner’s included, gets 403 principal_not_allowed. The root cannot be managed (400 invalid_request).

The mark records the backend client that set it and its display name: the tenant application’s name, or a platform application’s slug such as chat. Marking writes the audit row folder.managed; unmarking writes folder.unmanaged.

Every folder and file carries two fields, in every answer: reads, listings, search hits, the trash, the legal holds list, the external reference lists, upload completions and every change.

Field Meaning
managed true for the marked folder and everything below it
managed_by null when not managed; otherwise { client_id, name, managed_folder_id, managed_at }: the backend client that set the mark, its display name (it can be null), the folder that carries the mark, and when it was set
{ "managed": true, "managed_by": { "client_id": "fitness-backend", "name": "Fitness", "managed_folder_id": "01J9Z6R2M3N4P5Q6R7S8T9V0W1", "managed_at": "2026-10-24T09:00:00.000Z" } }

my_actions does not change: it still lists the actions the caller holds. Decide whether to show editing controls from managed. The user clients have a helper for it: isReadOnly(item) in the web client, item.isReadOnly in the Flutter package.

“People” means every user token: the console, your app’s users with a token from your token route, holders of files.manage and the tenant owner. Inside a managed folder, they are refused with 403 folder_managed:

Refused for people Routes
Uploading new files, and completing a ticket created before the folder was marked (aborting it still works) POST P/uploads, POST P/uploads/:upload_id/complete
New versions, restoring, pinning, commenting on or deleting a version POST P/files/:file_id/versions, POST P/files/:file_id/versions/:version_no/restore, PATCH P/files/:file_id/versions/:version_no, DELETE P/files/:file_id/versions/:version_no
Creating a folder inside POST P/folders
Renaming, moving (into, out of or within it), and every other edit: metadata, tags, status, visibility, owner, external_ref PATCH P/files/:file_id, PATCH P/folders/:folder_id
Copying into it POST P/files/:file_id/copy, POST P/folders/:folder_id/copy
Moving to the trash, restoring (also what your backend trashed), purging DELETE P/files/:file_id, DELETE P/folders/:folder_id, POST P/files/:file_id/restore, POST P/folders/:folder_id/restore, POST P/files/:file_id/purge
Changing access entries or inheritance PUT P/files/:file_id/permissions, POST P/files/:file_id/permissions/entries, DELETE P/files/:file_id/permissions/entries/:entry_id, and the same for folders

A folder outside that contains a managed folder somewhere below it cannot be renamed, moved or trashed by people either, because that would move or trash the managed subtree. Its description can still change.

Everything else works as usual, with the usual permissions:

  • viewing, listing, search, previews, downloads and URL minting;
  • copying out to an unmanaged folder: the copy is an ordinary file or folder, not managed;
  • regenerating renditions;
  • legal holds: placing, changing and releasing them;
  • emptying the trash with files.manage (POST P/trash/empty): it also purges what your backend trashed in a managed folder, except held files;
  • revoking existing share links, and reading access entries and the audit log.

Your backend clients with files:manage change managed content as before: upload, version, rename, move, trash, restore, purge, access entries, unmark.

The refusal names the folder that carries the mark:

{ "status_code": 403, "error": "folder_managed", "message": "People cannot upload files into a folder an application manages (managed by Fitness); …", "path": "…", "timestamp": "…", "managed_folder_id": "01J9Z6R2M3N4P5Q6R7S8T9V0W1" }

The libraries read it for you: isFolderManagedError(error) and FilesForbiddenError.managedFolderId in the Node and web clients, FilesForbiddenException.isFolderManaged and .managedFolderId in the Flutter package.

Inside a managed folder only your application creates share links: your backend’s POST P/files/:file_id/shares and POST P/folders/:folder_id/shares work as usual, and people get 403 folder_managed. A link a person made before its target came under a mark stops working (GET /shares/:token and POST /shares/:token/download answer 410 share_gone); your backend’s links keep working. For your own users you can also mint short-lived URLs with POST P/files/urls or GET P/files/:file_id/download.

A managed folder can contain another marked folder, for example one marked by a second application. An item reports the nearest mark: managed_by names the deepest marked folder above it (or the item itself). Removing the inner mark leaves its subtree managed through the outer one.

Who How
Your backend PATCH P/folders/:folder_id with managed: false. The normal way
The tenant owner, in an emergency POST P/folders/:folder_id/release-management with { "reason": "…" }

The release is for when the application cannot unmark the folder itself: its backend was retired, or the files must be handed over to people. It needs a user token of the tenant owner (GET P/me/capabilities answers tenant_owner: true) and a reason of 1 to 500 characters, kept in the audit log. It answers the folder, now with managed: false unless an outer mark still covers it.

Refusal When
403 tenant_owner_required A user who is not the tenant owner
403 principal_not_allowed A machine token: your backend unmarks with managed: false instead
400 invalid_request No reason, a longer one, or a folder that does not itself carry the mark: the message names the managing folder; call the route on managed_by.managed_folder_id

For administrators, a managed folder or file shows Managed by and the application’s name (its client id when it has none), with a short explanation: on the rows of the list and the grid, on the folder’s header, in the details panel, on the file page, in search results and in the trash. The folder tree marks managed folders with a lock.

Inside a managed folder the console hides what people cannot do: upload (a dropped file is refused with a message), new folder, rename, move, delete, restore and purge in the trash, metadata, status and visibility changes, new versions and version restore, pin and delete, access entry changes (the entries are still shown) and new share links. The move, copy and restore pickers grey out managed folders.

What you can still do there: view, preview and download, copy out to an unmanaged folder, place and release legal holds, read the activity, revoke links, and empty the trash with files.manage.

The tenant owner alone sees Release from app management on the folder that carries the mark, and from a folder or file below it for the managing folder. The dialog explains the risk and requires a reason before Release the folder. If a change still meets folder_managed, because the folder was marked while the page was open, the console shows the service’s message.

Action Written when
folder.managed A backend client marks a folder, on creation or later (client_id, name)
folder.unmanaged A backend client removes the mark (client_id, name of the removed mark)
folder.management_released The tenant owner releases a folder (reason, client_id, name of the released mark)

Read them in the application’s audit log (GET P/audit with action=folder.managed,folder.unmanaged,folder.management_released) or the folder’s own trail (GET P/folders/:folder_id/audit). See Audit actions.