Modulify

Storage HTTP API

The seven endpoints your site and your integrations use to read and write storage.

On this page

Your project's storage is a small REST API hosted on your own CDN link. Every method is a POST with a JSON body and a storage key header. Anything you write is then publicly readable at the same host.

Your own base URL, and a copy of this reference with that host filled in, live in the editor's Storage tab under Configuration. Expand Using your storage, switch to the External tab, and use Copy guide to take the whole thing away as plain text.

Authentication

Send the key as an X-Storage-Key header on every request.

curl -X POST https://your-cdn-link/list \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix":""}'

Keys start with msk_. Your site reads its own key from process.env.CDN_PRIVATE_KEY and the base URL from process.env.CDN_URL. Both are server side only. A missing, malformed or unknown key returns 401 with "A valid storage key is required!"

A key is scoped to exactly one project's storage. Rotating it from the Storage panel invalidates the old one immediately.

Response envelope

Every response, success or failure, has the same shape:

{
    "success": true,
    "message": "Object stored successfully.",
    "data": { "key": "notes/hello.txt", "url": "https://your-cdn-link/notes/hello.txt", "size": 8 },
    "code": 200
}

Treat a call as failed unless the HTTP status is ok and success is true. Read the payload from data.

Endpoints

Method Path Body Returns
POST /put { key, body, contentType, encoding } { key, url, size }
POST /get { key } { key, contentType, encoding, body, size }
POST /list { prefix, token } { folders, files, nextToken }
POST /delete { key } or { keys } { deleted }
POST /create-folder { name, path } { name }
POST /delete-folder { path } { deleted }
POST /move { keys, destination } { moved, skipped, failed }

/put

Create or overwrite a file. body must be a string. Set encoding to base64 for binary data, otherwise it is treated as UTF-8. contentType is optional and is guessed from the key when omitted.

curl -X POST https://your-cdn-link/put \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"notes/hello.txt","body":"hi there"}'

Writing to a key that already exists replaces it and purges that object from the CDN cache, so the next fetch returns the new bytes.

/get

Read a file back. The body always comes back base64 encoded, whatever the file is, so encoding in the response is always base64.

curl -X POST https://your-cdn-link/get \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"notes/hello.txt"}'

A missing key returns 404 with "No object exists at that key!"

/list

List one level of the bucket. folders is an array of folder names relative to the prefix, files is an array of { key, name, size, url }. Results come back fifty at a time.

curl -X POST https://your-cdn-link/list \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"notes/"}'

When nextToken is not null, pass it back as token to get the next page. Keep going until it is null.

/delete

Delete one file with key, or several with keys.

curl -X POST https://your-cdn-link/delete \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys":["notes/hello.txt","notes/old.txt"]}'

At most 1000 keys per request. Above that you get a 400 saying so.

/create-folder

Create an empty folder. name is the folder, path is where to put it, empty for the root.

curl -X POST https://your-cdn-link/create-folder \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"invoices","path":""}'

/delete-folder

Delete a folder and everything inside it.

curl -X POST https://your-cdn-link/delete-folder \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"invoices"}'

/move

Move or rename files. destination is a folder prefix, empty for the root.

curl -X POST https://your-cdn-link/move \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys":["notes/hello.txt"],"destination":"archive/"}'

The response splits the outcome into moved, skipped and failed.

Reading files publicly

Reads do not go through this API. Every file is served straight from your CDN link at its own key:

curl https://your-cdn-link/uploads/logo.png

No key, no headers. That is what makes it good for images and downloads, and it is why private data must be read back server side with /get rather than linked to.

Limits

Limit Value
Object size on /put 100 MB
Request body on /put 140 MB
Request body on every other endpoint 2 MB
Object size on /get 25 MB
Object key length 1024 bytes
Keys per /delete 1000
Objects per /list page 50
Total storage per site 500 GB

Exceeding a body limit returns 413 with "The request body is too large." An object over the /put size limit returns 413 with "The object exceeds the storage upload size limit!"

Key rules

Keys are paths, with / separating folders. Leading slashes, . and .. segments and backslashes are stripped before the key is used, so ../../etc/passwd cannot escape your bucket. A key that reduces to nothing is rejected with "A valid object key is required!"

.modulify-keep is reserved. It is the marker file used to keep empty folders alive, and writes to it are rejected.

From your site

Modulify sites ship with a typed server only wrapper over these endpoints at src/lib/modulify/storage, so you rarely call them by hand. See Storage for that helper and the panel it belongs to.

Next

  • Storage covers the panel, the CDN link and key rotation.
  • Site databases is the other half of your project's persistence.