# Storage

Source: https://modulify.ai/docs/data/storage

Per project file storage served from your own CDN link.

Every project gets its own object storage: a bucket fronted by your own CDN link, where each file is either public at that link or private to your site. Use it for anything created or changed at runtime, such as form submissions, user uploads, generated files and datasets. Static assets that ship with the design belong in the site's `public` folder instead.

## Open the Storage panel

Open a project and pick the **Storage** icon in the editor's toolbar, in the same pill as **Preview**, **CMS** and **Analytics**. The panel has three sub-tabs: **Storage**, **Analytics** and **Configuration**.

## Browse and upload files

The **Storage** sub-tab is a file browser. Breadcrumbs run along the top starting from **Storage**, and folders open on click. The same row also carries the top level default on its right, described under [Which files are private](https://modulify.ai/docs/data/storage#which-files-are-private).

The folder you are in is part of the page address, under `/storage/files/`. A link to a folder reopens it, a refresh keeps you where you were, and the browser's back button walks you back out.

There are three ways to get files in:

- Drag files onto the panel. A dashed overlay appears reading "Drop files to upload".
- Open the three dots menu in the toolbar and choose **Upload files**.
- Use the same menu's **New folder** first, if you want somewhere to put them. The dialog asks for a name and a **Visibility** of **Inherit**, **Public** or **Private**, which sets what new files inside the folder become. **Inherit** follows the folder around it, or the top level default.

Uploads run up to three files at a time, fewer while a large one is in flight, with a queue of up to 100 files. A single file is capped at 1 GB, and anything bigger is refused before it is read, with "File too large" naming the file, so a large file never sits there half sent. A file over 32 MB is sent in parts, three at a time, and a part that fails on a server problem is sent again on its own before the whole file counts as failed. Folders cannot be uploaded: dropping one reads as an empty file and is refused the same way, so open it and drop the files inside instead. Hand over more than the queue holds and the extras are left for you to add once the first lot finishes.

A panel appears under the toolbar while the queue runs, reading "Uploading 1 of 3" with the bytes sent so far, and a row per file carrying its size and its state. A row reads **Queued** before its turn, **Preparing** while the file is being packaged, then a percentage, then **Finishing** while the server writes it. When it finishes you get a toast: "Uploaded", with a count. If some files fail you get "Some uploads failed" with the split, and each failed row says why underneath it. If a file was stored but an older copy of it with the other visibility could not be cleaned up, a second toast, "Older copy left behind", names it, or "Older copies left behind" counts them when there are several. Upload the same file again to finish, which keeps the new copy and removes the old one.

A file that fails on a server problem is sent again on its own, up to three tries, with the row reading "Retry 2 of 3" between attempts. A file the server refuses outright, such as one that is too large, is not retried. Nine filenames are refused that way in every folder, whatever their case: `put`, `get`, `list`, `delete`, `create-folder`, `delete-folder`, `move`, `set-visibility` and `signed-url`, because the CDN routes a path ending in one of them to the [storage API](https://modulify.ai/docs/data/storage-http-api) instead of serving the file. The same goes for a file whose folder and name together end in one of the eleven paths the CDN routes to the [email API](https://modulify.ai/docs/automations/email-http-api) and the [analytics API](https://modulify.ai/docs/grow/analytics-http-api), whatever their case: `email/send`, `email/quota`, `email/email`, `email/emails`, `analytics/status`, `analytics/realtime`, `analytics/stats`, `analytics/timeseries`, `analytics/breakdown`, `analytics/overview` and `analytics/export`. So a file called `send` is refused inside any folder called `email`, however deep, and is fine in every other folder. The row reads "A file cannot be named put, get, list, delete, create-folder, delete-folder, move, set-visibility, signed-url, and a file path cannot end in email/send, email/quota, email/email, email/emails, analytics/status, analytics/realtime, analytics/stats, analytics/timeseries, analytics/breakdown, analytics/overview, analytics/export, because the CDN sends those paths to the storage, email and analytics APIs. Pick another name!", so rename it on disk first, or upload it to another folder. `list.json`, `listing` and `email/send.json` are fine, and so is a folder called `list` or `email`, because a folder has no address of its own. Anything still failed at the end keeps a circular arrow next to it that sends the same file again without you finding it on disk a second time.

Nothing is blocked while an upload runs. The three dots menu stays available, so you can create a folder or hand over more files and they join the back of the queue. Every unfinished row has an X that cancels that one file, stopping it mid transfer and dropping it from the list while the rest of the queue carries on, and **Cancel all** in the panel header stops the lot.

Uploading a file whose name is already in the folder asks first, because the upload replaces the old file for good and its address then points at the new content. The check runs against the whole folder, not just the files loaded on screen. Uploading while a search is open is refused with "Clear the search first", because the files would land in the folder you were browsing and the results would not show them. **New folder** and **Upload files** are greyed out in the menu for the same reason until you clear the search.

An empty folder reads **No files**, "Drag files here or upload them instead."

## Work with what is there

The search box is placeholdered "Search all files…" and searches the whole bucket, not just the folder you are in. One search reads up to 5,000 files, shared between public and private ones, and shows at most 200 matches, so on a very large bucket it can stop before it reaches the file you want. When that happens and nothing matched, the empty list reads "Search stopped early, try browsing folders instead." instead of "Try a different name or clear search.", and opening the folders is the sure way to find it.

Each row has a three dots menu with **View file**, which opens the preview described under [Previewing a file](https://modulify.ai/docs/data/storage#previewing-a-file), **Download file**, **Copy link** on a public file, **Rename file**, **Move to** and **Delete file**. A folder's menu has **Open**, **Move to** and **Delete folder**. Tick several rows and a bar appears above the list reading "3 selected", with its own menu holding **Download**, **Move to** and **Delete**. **Download** fetches the ticked files one by one and skips ticked folders, saying so in a toast, and is greyed out when only folders are ticked, because a folder is downloaded through [Export](https://modulify.ai/docs/data/clearing-and-exporting) instead. The same menus also carry the visibility actions described under [Public and private](https://modulify.ai/docs/data/storage#public-and-private).

**Copy link** puts the file's CDN URL on your clipboard and confirms with "Link copied". It only appears on a public file, because a private file has no link that keeps working. While a site's CDN link is not ready yet, the copied link is a temporary one that stops working after 7 days, and the toast says so and asks you to copy it again once the CDN link is ready.

Drag a file or a selection onto a folder row, or onto a breadcrumb, to move it, or choose **Move to** from a row's menu or the selection bar. The dialog is placeholdered "Leave empty for the top level" and takes a folder path like `images/2024`, with the top level, the folders above you and the folders on screen offered as one click picks. A folder that does not exist yet is created by the move. Moving into the folder it already sits in, or a folder into itself, is ignored, and the dialog says so before you confirm. A move that would leave a file at a path the upload refuses, such as a file called `send` moved into `email`, is refused with "Move failed" and the same reason, and nothing in that move goes through. A folder is never refused for this, because it keeps its own name, so every path inside it ends the same way wherever it lands.

One move covers at most 5,000 files, and the files you selected count toward that first. A folder that would take the move past it stays where it is, before any of its files move, even when it is the files you selected that used up the limit. You get "Folder not moved", or "folder was not moved (move limit)" in the toast when other items in the same move went through. Move that folder on its own, or what is inside it in smaller groups.

A move rewrites the file's path, and its CDN URL is built from that path, so the old URL stops working the moment the move lands. Everything already pointing at it breaks with it. A published page that links the file, or a CMS row whose image field holds the old address, keeps asking for a key that is no longer there and stops loading its image until you update it to the new URL. Nothing asks before a move and there is no undo, so it is worth arranging folders before you start linking files rather than after.

The **Modified** column reads in your own timezone, down to the minute.

Deleting asks first. A folder delete warns that everything inside goes too, and none of it can be undone. It also warns that the folder's privacy setting goes with it, so files added to that path later follow the parent folder or the top level default. It does the same to links as a move does: the URL stops resolving, and anything still pointing at it breaks.

Files load fifty at a time. The footer reads "Showing 50 of 312" with a **Load more** button while there is more to fetch. If the list changed underneath you, **Load more** starts the folder again from the top and says "File list reloaded". If loading more fails for another reason you get "Could not load more files", the rows already shown stay put, and **Load more** is still there to try again.

There is no refresh button, because the list keeps itself up to date. While the **Storage** sub-tab is open, the folder you are in is checked again every 30 seconds, so a file that your site, chat or a teammate adds, changes or removes shows up without a reload, and the count in the footer is checked at the same time. The check covers the first fifty entries, the ones the folder opens with, and leaves the rows you added with **Load more** as they are. It never moves you to another folder or clears your selection: it waits while you search, have rows ticked, upload, move, rename, delete, create a folder, change visibility, drag or have a dialog or a preview open, and it pauses while the browser tab is in the background. If the folder cannot be loaded at all, the list reads **Could not load storage** with a **Retry** button.

### Previewing a file

**View file** opens the file in a preview inside the panel rather than in a new tab. The dialog is titled with the file's name and shows images, video, audio, PDFs and text files, the same kinds listed under [Preview and download single files](https://modulify.ai/docs/editor/backups#preview-and-download-single-files). A file of any other kind reads **No preview**, "Download the file to open it."

A text file shows its first 256 KB, with "Only the first 256 KB of this file is shown. Download it to see the rest." under it, and an HTML file shows its source rather than the page it would render. A text file larger than 5 MB is not read at all and reads **No preview**, "This file is too large to preview." Images, video, audio and PDFs are loaded from a link to the file itself, so their size does not stop the preview.

Under the preview sit **Copy link**, on a public file only, **Open in new tab** and **Download file**, which do what the same entries in the file's menu do. If the file is no longer in your storage the preview reads **No file**, "It may have been deleted or moved.", and the three buttons are greyed out, hovering them reads "This file is no longer in your storage." If it cannot be read for another reason the preview reads **Could not load file**, "Check your connection and try again.", with a **Retry** button.

### Renaming

**Rename file** in a file's menu renames one file in place. The dialog asks for the new name, the file keeps its folder, and a name that already exists there is refused with "Name already taken". The names the upload refuses are refused here too, with "Could not rename" and the same reason underneath: the nine names `put`, `get`, `list`, `delete`, `create-folder`, `delete-folder`, `move`, `set-visibility` and `signed-url` in any case, and a name that would end the file's path in one of the eleven email and analytics paths, such as `send` for a file in a folder called `email`, because the CDN would route the renamed file to the storage, email or analytics API. Because a rename is a path change, the URL moves with it and whatever points at the old one needs updating, and the dialog says so.

[Chat](https://modulify.ai/docs/build/site-chat) renames the same way, one file at a time by moving it to the new name, and it quotes the file and the destination back to you before it touches anything you did not just upload. Folders cannot be renamed at all, in the panel or in chat. They move into another folder and keep their own name.

The manual route is to upload the file again under the name you want and delete the old one. Either way the name change is a path change, so the URL moves with it and whatever points at the old one needs updating.

## Public and private

Every file is either public or private. A public file is served at its CDN URL, `https://your-cdn-link/<key>`, to anyone who has the address. A private file has no CDN URL at all: the CDN cannot reach it, and only your site's server code can read it back, through the storage helper or the [HTTP API](https://modulify.ai/docs/data/storage-http-api) with the storage key.

A private file shows a small lock after its name in the file browser. A folder shows a grey lock when its default is private, meaning new files dropped into it are private. The lock is amber when the folder holds files on the other side of its default, such as private files in a public folder or public files in a private one. Hover the lock to see which of these it means for that row.

A file only ever lives on one side. If a file ends up with a copy on both, for example after a change that was interrupted or an upload that could not remove the old copy, the browser, search and the listings show it as public, because the public copy is the one the CDN serves. Making it private removes the public copy, and making it public removes the private one.

### Which files are private

Overwriting a file keeps the visibility it already has, so replacing an image never quietly republishes it. A new file follows the default of the folder it lands in. A folder's default is its own setting when it has one, otherwise the setting of the nearest parent folder that has one, otherwise the top level default. That top level default also covers every file uploaded outside any folder. It starts as **Private**.

The top level default sits on the right of the breadcrumb row in the **Storage** sub-tab, in every folder you open, and reads **Top level: Private** or **Top level: Public**. A grey lock sits in front of it when the default is private, the same lock a private folder shows. Hover it to see what it covers. Open it and pick **New files here are public** or **New files here are private**, with the current one highlighted. Nothing asks first, because nothing moves. You get "Default updated", and existing files keep their visibility.

A folder gets its own setting in two ways. Creating it sets one, from the visibility you give or from its parent when you give none, and creating a folder that already exists keeps the setting it has. **Make folder public** and **Make folder private** set one. Writing a file never does: a folder that only appeared because a file was written inside it has no setting of its own, and keeps following its parent or the top level default whatever visibility that file was given.

Changing the top level default later only affects new files outside any folder and new files in folders with no setting on them or on a parent folder. Existing files keep their visibility, and a folder with its own setting keeps it.

A folder's setting lives with the folder, so **Delete folder** and **Clear storage** remove it too. A file written into that path afterwards follows the parent folder, or the top level default when no parent has a setting. If your site writes private data, such as invoices or user uploads, pass `visibility: 'private'` on every write rather than relying on the folder.

Uploads from the panel follow those defaults, and so does a folder made with **New folder** unless you pick **Public** or **Private** as its **Visibility** in the dialog. Uploads from chat follow the defaults unless you say otherwise. CMS image and file fields always upload as public, because the CMS embeds them in pages. Your own site code picks per write with the `visibility` option on `putObject` or the `/put` body.

### Changing visibility

A file's menu carries **Make private** when it is public and **Make public** when it is private. A folder's menu carries the action opposite to its default, **Make folder private** on a public folder and **Make folder public** on a private one, and both when the folder holds files on the other side of its default. Either one moves everything inside to that side and sets the folder's own default for files added later. Tick several rows and the selection bar's menu offers **Make public** and **Make private** for all of them, however many you picked.

Each change asks first. Making a file public opens it to anyone with the link, so the prompt reads `Make "hero.png" public?` and confirms with **Make public**. Making it private stops its public link right away, so the prompt reads `Make "hero.png" private?` and warns that your site can still read it. On success you get "Made public" or "Made private", and "Nothing was changed" when everything was already on that side.

A folder too large to change in one pass is changed in several passes behind that one prompt. If it is still not finished after them you get "Visibility partly changed", and running the same change again picks up the rest. Files that could not be moved are counted in "Some items were not changed" and keep their old visibility. A folder whose path is longer than 1001 bytes has no room for the file that keeps its visibility setting, so it is left as it is and the same toast counts it among the items that kept their visibility because their path is too long. Move that folder to a shorter path, then change it again. In each case the ticked rows stay ticked, so you can try again straight away.

Ask [chat](https://modulify.ai/docs/build/site-chat) and it changes the top level default for you as well, the same switch as the menu.

Moving a file never changes its visibility: a private file dropped into a public folder stays private until you change it. A folder you move keeps the default it had, even when that default came from the folder it was moved out of.

Downloads you start from the panel use a temporary signed link. For a public file that is a convenience, not a protection. For a private file it is the only way the panel can hand you the bytes, and the link expires after 15 minutes.

## The CDN link and the storage key

The **Configuration** sub-tab holds both, under **Connection**.

**CDN link** is the public base URL for this project's storage. Append a public file's path to it to link to that file, for example `https://your-cdn-link/logo.png`. Private files have no CDN link, and the same address answers `404` for them: the panel opens them through a temporary signed link, and your site reads them through the storage helper. Copy and open buttons sit beside it. If storage has not been provisioned yet, the card reads "Storage is not provisioned for this site." If the settings cannot be loaded, the card reads "The CDN link could not be loaded." Nothing about your files changes. **Try again** loads the settings again, and the message also clears by itself the next time the settings load, for example when the panel refreshes after your storage changes.

**Private Storage Key** is the server only key your site uses to read and write at runtime. It is masked as `msk_••••` and never shown in full in the panel. The copy button puts the real key on your clipboard and clears it again after a minute, telling you so in the toast. Anyone holding the key can read, write and delete every file in the bucket, private ones included, and it is also what your published site presents to fetch its own build when it starts, so a leaked key exposes your compiled site as well as your files. Rotate it as soon as you suspect a leak.

Your site reads the key as `CDN_PRIVATE_KEY` and the base URL as `CDN_URL`, both injected into the environment. Both are server only. Never reference either from a client component.

### Rotating the key

The rotate button next to copy asks "Rotate storage key?" and warns that the current key stops working immediately, that your published site keeps the old key until you republish it, and that anything outside the site that uses the key needs the new one pasted in. On success: "Key rotated", with the reminder to republish the site to apply it.

Two things then need doing, and republishing covers only one of them. Your published site keeps the key it was built with, so its own storage calls fail until you publish again, which bakes the new key in. Anything outside the site that used the old key, a script, a CI job or another service calling the [HTTP API](https://modulify.ai/docs/data/storage-http-api), is not fixed by a republish: copy the new key and paste it in there yourself.

Rotating needs project delete permission in the workspace, and members without it see the rotate button disabled.

## Reading and writing from your site

Sites created from the Modulify starter ship with a server only helper at `src/lib/modulify/storage`:

```typescript
import { putObject, listObjects, getObjectText, getSignedUrl, setVisibility, deleteObjects, StorageError } from '@/lib/modulify/storage'

await putObject('submissions/123.json', JSON.stringify(data), { visibility: 'private' })
await putObject('uploads/photo.jpg', photoBytes, { contentType: 'image/jpeg', visibility: 'public' })
const { files } = await listObjects('submissions/')
const raw = await getObjectText('submissions/123.json')
const { url } = await getSignedUrl('invoices/2026-001.pdf', { download: true })
await setVisibility(['brochures/2026.pdf'], 'public')

try {
    await deleteObjects(['uploads/old-1.jpg', 'uploads/old-2.jpg'])
}

catch (error) {
    if (error instanceof StorageError && error.data && 'failedKeys' in error.data) await deleteObjects(error.data.failedKeys)
}
```

Import it only from API routes, `getServerSideProps` or other server code. It throws in the browser by design. `putObject` and `createFolder` take `visibility` in their options, `getObject` and `getObjectText` read private files too, `listObjects` returns `rootVisibility` beside `files`, `folderDetails` and `nextToken`, the default a new file gets at the top level, `deleteObject` and `deleteObjects` return `deleted` and `missing`, the keys that had no file to remove, and `setVisibility` changes existing files or whole folders.

The `putObject` body is a string, a `Buffer` or a `Uint8Array`. Hand binary data over as bytes, for example a file a visitor uploaded, and the helper base64 encodes them and sets `encoding` to `base64` for you. The `encoding` option only matters for a string body: `base64` when the string already is base64, otherwise it is treated as UTF-8. Bytes still travel inside the JSON request as base64, a third bigger than the file, but the 140 MB request body limit leaves room for the 100 MB object limit, so a file can be 100 MB whichever form you pass.

`getObject` and `getObjectText` return files up to 25 MB and refuse anything larger. To hand a visitor a private file, or any file over that size, check who is asking in your API route and redirect them to the link `getSignedUrl` returns. It works for 15 minutes, and `download: true` makes it a save-as download. Never store that link or put it in a page, because it stops working.

Every function throws when the endpoint refuses the request, and what it throws is a `StorageError` carrying `message`, `status`, the HTTP status the endpoint answered, and `data`, the counts the endpoint sent with its refusal. `data` is `deleted`, `missing`, `failed` and `failedKeys` when `deleteObject` or `deleteObjects` could remove only some keys, and the full result (`changed`, `skipped`, `failed`, `tooLong`, `complete` and `visibility`) when `setVisibility` could not move every file or met a folder whose path is too long. For every other refusal `data` is `null`. Check `error instanceof StorageError`, then send `failedKeys` again, or the same `setVisibility` call again, exactly as the [HTTP API](https://modulify.ai/docs/data/storage-http-api) describes. Misusing the helper itself, an import from the browser, an option it does not know, a body that is not a string or bytes, or a site with no storage configured, throws a plain `Error` instead.

`deleteFolder` and `setVisibility` work through a large folder in parts. Each returns `complete`, and while it is `false` you call it again with the same arguments. `setVisibility` also returns `tooLong`, the keys it left untouched because they are over the key length limits: a file key longer than 1016 bytes, or, the only case that happens in practice, a folder whose path is longer than 1001 bytes and so has no room for the marker file that stores a folder's default. When `tooLong` is above zero, nothing failed and `complete` is `true`, the call throws "The object key is too long!" with every count in `error.data`, and the fix is to move that folder to a shorter path before changing its visibility.

A helper written before private storage existed lacks `setVisibility`, `getSignedUrl` and the `visibility` option, and one written before errors carried their counts throws a plain `Error` and takes only a string body in `putObject`. Chat adds whatever is missing to `src/lib/modulify/storage.ts` when it works on storage, and each function maps one to one onto an endpoint in the [HTTP API](https://modulify.ai/docs/data/storage-http-api).

Caching is handled for you. Objects are cached hard at the edge, but every write, move and delete purges that object, so the next fetch returns the latest bytes. Reuse the same key to update a file in place rather than adding a cache busting query string.

You rarely need to write this yourself. The Configuration sub-tab has a collapsible **Using your storage** section with two tabs. **Modulify** explains that describing what you want in chat is enough, and its **Add it with AI** button drops a ready made prompt into the composer for you to review and send. **External** holds the raw endpoint reference with your own host filled in.

For calling the endpoints directly, from outside the site or from another language, see [Storage HTTP API](https://modulify.ai/docs/data/storage-http-api).

## Usage and limits

Each site is expected to stay under 500 GB. That is a soft limit: nothing in the panel or the [HTTP API](https://modulify.ai/docs/data/storage-http-api) refuses a write for crossing it, and it can be raised on request.

The **Analytics** sub-tab shows **Total data**, **Files** and **Folders** for the bucket. Those three are counted by scanning the bucket, and on a very large bucket the scan stops early, so each then carries a trailing plus sign, the file browser footer adds the same plus sign after its total, and a note under the cards says the totals are at least this much. It then shows **CDN delivery** for the period you pick with the date picker the **Analytics** tab uses: **Today** (from midnight UTC), **Last day** (the default, the last 24 hours), **Last week**, **Last month**, **Last 3 months**, **Last year**, **All time** or **Custom range**. **All time** reaches back to when the site was created. The cards show **Bandwidth**, **Requests** and **Cache hit rate**, with a **Requests over time** chart, one point per hour for **Today**, **Last day** and a custom range of a single day, and one per day otherwise. Before the CDN has served anything the chart reads **No traffic**, "Requests appear once the CDN serves files." If the CDN numbers cannot be read, the three cards show a dash and the chart reads **Could not load chart**, "Check your connection and try again.", with a **Try again** button.

## The CMS folder

Image and file fields in the CMS upload into this same storage, into a folder called `CMS` at the top level. Inside it each collection has a folder and each item a folder of its own, such as `CMS/posts/42/`. A file uploaded while you are still creating an item goes into the collection's `new` folder, such as `CMS/posts/new/`. Every file gets a short random suffix on its name, so two uploads never replace each other, and every file is public, because the CMS embeds them in your pages. Each file goes up whole in a single request, so a CMS field takes files up to 100 MB.

The CMS manages this folder. The `CMS` folder and everything in it carry a **Managed** badge, and hovering it reads "Managed by the CMS. Files here are added from CMS fields and deleted with their item." You can open it, search it, preview its files, download them and copy their links like any others, but nothing can be uploaded into it, moved in or out of it, renamed, deleted or made private from this panel. Those actions are greyed out, and hovering one says why, for example "The CMS manages this file. Change it from its CMS item." Dropping files on the panel inside it does nothing, the **Move to** dialog does not offer it as a destination, and an empty folder in it reads **No files**, "Files added from CMS fields appear here." Chat, a connected AI client and the [Storage HTTP API](https://modulify.ai/docs/data/storage-http-api#key-rules) are refused the same way, with "The CMS folder holds the files of your CMS items, so it cannot be changed from storage. Add files from a CMS field, and they are deleted with their item!"

Its files come and go with the items that use them:

- Replacing or removing an image or a file in an item, including an image in a rich text field, deletes the old file once you save the item.
- A file you upload and then do not keep, because you replaced it before saving or closed the item without saving, is deleted when you close the item.
- Deleting an item deletes its files.
- **Clear data** on a collection deletes the collection's folder, and **Clear collections** in the CMS settings deletes the whole `CMS` folder.
- **Clear storage** keeps the `CMS` folder, because the CMS still uses it.

A file stays while another item of the same collection still uses it, so a [duplicated item](https://modulify.ai/docs/data/cms#duplicate-export-or-delete-an-item) shares the original's files safely. Under each image and file field, the CMS shows where its file is saved, as described under [Images and files](https://modulify.ai/docs/data/cms#images-and-files).

## From an AI client over MCP

The files in your bucket are also reachable from a connected AI client. `list_storage_files`, `read_storage_file`, `get_storage_stats`, `get_storage_info` and `get_storage_file_url` cover browsing and reading; `upload_storage_file`, `create_storage_folder`, `move_storage_files`, `delete_storage_files`, `delete_storage_folder` and `clear_storage` cover the writes; `set_storage_visibility` and `set_storage_root_visibility` cover what this page calls public and private, and `get_storage_info` reports the CDN link and the top level default. The reading tools cover the `CMS` folder too, while every write tool refuses it, as described under [The CMS folder](https://modulify.ai/docs/data/storage#the-cms-folder). The archives described under [Backups](https://modulify.ai/docs/data/storage#backups) below have tools of their own.

The private storage key sits behind its own `credentials:reveal` scope, unticked by default, because `get_storage_key` and `rotate_storage_key` return it in plain text into the client's transcript. See [MCP tools](https://modulify.ai/docs/mcp/tools#storage) and [Tokens and scopes](https://modulify.ai/docs/mcp/tokens-and-scopes).

## Backups

On any paid plan the whole bucket is archived once a day, and you can archive it yourself up to 10 times in twenty four hours with **Backup**, which asks you to confirm and says how many manual backups that leaves. The archives live under the **Storage** tab of [Backups](https://modulify.ai/docs/editor/backups), can be browsed, restored in whole or in part, and downloaded as a `.zip`, and are kept outside your bucket so they never count toward what it holds. A day where nothing changed, or a bucket with no files, is passed over by the daily run.

Downloading an archive happens in that tab only. Chat and a connected MCP client can list the archives, take one, delete one or all of them, turn daily backups on or off, browse an archive, read a text file out of it and restore it once they have shown you the plan and you have said yes, with `list_storage_backups`, `create_storage_backup`, `delete_storage_backup`, `clear_storage_backups`, `set_storage_backup_schedule`, `browse_storage_backup`, `read_storage_backup_file`, `restore_storage_backup` and `get_storage_restore_status` over MCP, but neither ever downloads one. A restore either of them starts needs the same paid plan and **Delete projects** permission as the button. See [Backups from chat and MCP](https://modulify.ai/docs/editor/backups#backups-from-chat-and-mcp).

An archive holds the bucket as it stands when it actually starts reading it, which can be a few minutes after you ask for one while other archives finish, so wait for the row to show its file count before you delete, move or hide anything you meant that backup to hold.

To get lost files back, open an archive that still holds them with **Browse files** and restore just those files, or restore the whole archive with **Restore backup**, which makes this bucket match it exactly. Restoring needs a paid plan and the **Delete projects** permission, and takes a **Before restore** backup of the bucket first whenever it would overwrite or delete anything. While a restore runs, this panel refuses uploads, new folders, moves, renames, deletes, clearing and visibility changes with `Your storage is being restored from a backup. Try again when the restore finishes!` [Restore a storage backup](https://modulify.ai/docs/editor/backups#restore-a-storage-backup) walks through it.

The **Configuration** sub-tab has a **Backups** section with two rows. **Storage backups** has a **View backups** button that opens that tab directly. **Daily backups** is a switch, on for every site until someone turns it off, and turning it off stops only the daily archive of this site: **Backup** still works and every archive already taken stays. On the free plan both rows carry a **Paid** badge and the switch is disabled, while the button still works, so archives taken while on a paid plan stay reachable. See [Turn daily storage backups off](https://modulify.ai/docs/editor/backups#turn-daily-storage-backups-off).

**Delete all storage backups** under **Danger zone** in the same sub-tab removes every finished archive at once, and needs the **Delete projects** permission. Clearing storage does not delete any archive.

## Next

- [Storage HTTP API](https://modulify.ai/docs/data/storage-http-api) documents the endpoints and their bodies.
- [Clearing and exporting](https://modulify.ai/docs/data/clearing-and-exporting) covers the storage export and the clear action.
- [Backups](https://modulify.ai/docs/editor/backups) covers the daily archives taken of this bucket.