Files API
The Files API provides upload, metadata management, folder organization, and asset serving backed by Supabase Storage.
Endpoints
GET /api/files → 200 — list files
POST /api/files → 201 — upload a file (multipart) or create metadata (JSON)
GET /api/files/:id → 200 — get file metadata
PATCH /api/files/:id → 200 — update file metadata or replace file (multipart)
DELETE /api/files/:id → 204 — delete a file
PATCH /api/files → 200 — batch update
DELETE /api/files → 204 — batch delete
POST /api/files/import → 200 — import from URL
GET /api/files/:id/download → 200 — get download URL
GET /api/assets/:id → 200 — serve file asset (public URL)
GET /api/folders → 200 — list folders
POST /api/folders → 201 — create folder
PATCH /api/folders/:id → 200 — update folder
DELETE /api/folders/:id → 204 — delete folderFile Object
The File entity has the following fields. Fields marked * are always returned; others are null when not applicable.
| Field | Type | Description |
|---|---|---|
id * | string | Unique identifier (UUID) |
storage * | string | Storage bucket name (default: "files") |
filename_disk | string | null | Filename as stored on disk |
filename_download * | string | Original filename for downloads |
title | string | null | Display title |
type | string | null | MIME type |
filesize * | number | File size in bytes (0 for metadata-only records) |
width | number | null | Image width in pixels (not computed by server — set client-side) |
height | number | null | Image height in pixels (not computed by server — set client-side) |
duration | number | null | Duration in seconds (video/audio) |
charset | string | null | Character encoding for text files |
folder | string | null | Parent folder ID |
description | string | null | Description of the file |
location | string | null | Location metadata |
tags | string[] | Array of tags |
metadata | object | Arbitrary JSON metadata (EXIF, etc.) |
embed | string | null | Embed code/URL for external media |
focal_point_x | number | null | Focal point X coordinate for cropping |
focal_point_y | number | null | Focal point Y coordinate for cropping |
tus_id | string | null | TUS resumable upload identifier |
tus_data | object | null | TUS upload data |
uploaded_by | string | null | User who uploaded the file |
uploaded_on | string | null | Upload timestamp |
created_on * | string | Record creation timestamp |
modified_by | string | null | User who last modified the file |
modified_on * | string | Last modification timestamp |
Upload a File
Multipart Upload
Send a multipart/form-data request with the file binary and optional metadata:
curl
curl -X POST https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/image.png" \
-F "title=My Image" \
-F "description=A sample image" \
-F "folder=<folder_id>" \
-F "storage=files"| Form field | Required | Description |
|---|---|---|
file | Yes (multipart) | The file binary |
title | No | Display title |
description | No | Description text |
folder | No | Parent folder ID |
storage | No | Storage bucket name (default: "files") |
Response (201):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"storage": "files",
"filename_disk": "550e8400-e29b-41d4-a716-446655440000.png",
"filename_download": "image.png",
"title": "My Image",
"description": "A sample image",
"type": "image/png",
"filesize": 204800,
"width": null,
"height": null,
"folder": null,
"tags": [],
"metadata": {},
"uploaded_by": "<user_id>",
"uploaded_on": "2024-01-15T10:30:00Z",
"created_on": "2024-01-15T10:30:00Z",
"modified_on": "2024-01-15T10:30:00Z"
}
}width and height are not computed by the server. They will be null unless the client provides them on upload or updates them later via PATCH.
Upload Constraints
Size and type limits are read from the storage bucket, so you change them in
Supabase Studio — Storage → the bucket → File size limit and Allowed MIME
types — without redeploying. Supabase enforces them on the upload itself; DaaS
reads the same values so an upload that would be refused is rejected up front,
with a 400 in the standard error shape, before a signed URL is issued.
| Constraint | Where it is configured | Fallback |
|---|---|---|
| Maximum upload size | Bucket File size limit (Studio) | FILES_MAX_UPLOAD_SIZE, default 100 MB |
| Allowed MIME types | Bucket Allowed MIME types (Studio) | FILES_MIME_TYPE_ALLOW_LIST, default all types |
| Buckets signed uploads may target | FILES_STORAGE_BUCKET_ALLOW_LIST (comma-separated) | files |
A bucket field that is left unset — the default for a new bucket — falls back to
the environment variable beside it. image/* style wildcards work in both.
Bucket configuration is cached for up to a minute, so a change made in Studio takes effect within that window.
The bucket allow-list is not bucket configuration and has no Studio equivalent: signed URLs are minted with service-role credentials, which can write to every bucket in the project, so this list is what confines the Files API to the buckets you intend it to use. It stays an environment variable deliberately.
Signed Direct Uploads
Multipart uploads pass through the Next.js server, so they are bounded by its request body limit. To upload large files, send the bytes straight to Supabase Storage and register the metadata afterwards.
Step 1 — ask for a signed URL.
curl -X POST https://your-domain.com/api/files/signed-url \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"filename_download": "recording.mp4",
"type": "video/mp4",
"filesize": 734003200,
"folder": "<folder_id>"
}'filename_download, type and filesize are required. The response carries an
uploadUrl to PUT to and an uploadToken to quote in step 3:
{
"data": {
"uploadUrl": "https://<project>.supabase.co/storage/v1/object/upload/sign/files/<id>.mp4?token=...",
"token": "<supabase upload token>",
"uploadToken": "<server-issued token>",
"primaryKey": "<file_id>",
"storagePath": "<folder_id>/<file_id>.mp4",
"filenameDisk": "<file_id>.mp4",
"storageBucket": "files"
}
}Step 2 — PUT the bytes to uploadUrl. This request never touches the
application server, so the body limit does not apply.
Step 3 — register the metadata, quoting upload_token:
curl -X POST https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"upload_token": "<server-issued token>",
"filename_download": "recording.mp4",
"title": "Recording"
}'id, filename_disk, storage, folder, type and filesize are taken
from the token and from the stored object, not from this request body — size
and type are read back from Supabase Storage, so a client cannot misreport
what it uploaded or point a record at somebody else’s object. Any of these
fields sent alongside upload_token are ignored.
A token is bound to the user it was issued to, expires after two hours, and is
single-use: once POST /api/files has registered the upload, the token is spent.
If step 2 or step 3 fails, discard the orphaned object so it does not linger in the bucket:
curl -X DELETE https://your-domain.com/api/files/signed-url \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"upload_token": "<server-issued token>"}'This needs only a valid token, not create permission on daas_files, so a
permission change mid-upload cannot strand the object. It returns 204 whether
or not the object was still there, and is refused once the upload has been
registered.
JSON Mode (Metadata-Only)
Create a file record without uploading binary content. Useful for tracking external assets or placeholder entries:
curl -X POST https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"type": "image/png",
"filename_download": "external-asset.png",
"title": "External Asset",
"storage": "files"
}'The type and filename_download fields are required in JSON mode. The response has the same shape as the multipart upload, with filesize: 0.
List Files
GET /api/files supports the following query parameters for filtering, sorting, and pagination:
| Param | Type | Description |
|---|---|---|
limit | number | Maximum number of items to return |
offset | number | Number of items to skip (for cursor-based pagination) |
page | number | Page number (alternative to offset) |
search | string | Free-text search across title and filename |
sort | string | Sort field(s), comma-separated. Prefix with - for descending (e.g. -uploaded_on) |
fields | string | Comma-separated list of fields to return (e.g. id,title,filesize) |
filter | string (JSON) | JSON-encoded filter object (e.g. {"folder":{"_eq":"<id>"}}) |
meta | string | Set to filter_count to include metadata in the response |
Example — list with pagination and metadata:
GET /api/files?limit=10&offset=0&meta=filter_count&sort=-uploaded_onResponse (200):
{
"data": [
{ "id": "...", "title": "Image 1", "filesize": 204800, "..." },
{ "id": "...", "title": "Image 2", "filesize": 102400, "..." }
],
"meta": {
"filter_count": 10,
"total_count": 42
}
}When meta=filter_count is not passed, the meta key is omitted from the response.
Get File Metadata
Retrieve a single file’s metadata by ID:
GET /api/files/:id| Param | Type | Description |
|---|---|---|
fields | string | Comma-separated list of fields to return |
meta | string | Set to filter_count to include count metadata |
Response (200):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "My Image",
"filename_download": "image.png",
"type": "image/png",
"filesize": 204800,
"width": null,
"height": null,
"folder": null,
"uploaded_by": "<user_id>",
"uploaded_on": "2024-01-15T10:30:00Z"
}
}Update File Metadata
JSON Body Update
Update metadata fields via JSON:
curl -X PATCH https://your-domain.com/api/files/<id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"title": "Updated Title", "description": "A better description", "tags": ["hero", "blog"]}'Response (200):
{
"data": {
"id": "<id>",
"title": "Updated Title",
"description": "A better description",
"tags": ["hero", "blog"],
"...": "..."
}
}Multipart File Replacement
Replace the file binary for an existing file record using multipart form data:
curl -X PATCH https://your-domain.com/api/files/<id> \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/new-version.png"The response has the same shape as the JSON update. The old file binary is replaced in storage; the file record’s filesize, type, and filename_download are updated automatically from the new file.
Delete a File
curl -X DELETE https://your-domain.com/api/files/<id> \
-H "Authorization: Bearer <token>"Returns 204 No Content on success.
Serving Files
Use the asset endpoint to serve file content. The response is raw binary (not JSON) with appropriate Content-Type and Content-Disposition headers — suitable for <img src>, <video src>, or direct download links.
GET /api/assets/:id| Query param | Description |
|---|---|
download | Forces a file download (Content-Disposition: attachment) |
width | Resize image to this width (Supabase Pro plans only) |
height | Resize image to this height (Supabase Pro plans only) |
Assets are served with Cache-Control: public, max-age=31536000 (1 year). Updated files may not reflect immediately in all clients due to aggressive caching.
<img src="https://your-domain.com/api/assets/550e8400-e29b-41d4-a716-446655440000"
alt="My Image"
<!-- Authorization header must be set via fetch/XHR for protected assets -->
/>All asset requests require a valid Authorization header. There is no unauthenticated asset serving — grant the requesting user’s policy read access on daas_files to allow access.
Download File
Get a signed download URL for a file. The response is JSON containing a temporary URL, not the file content itself.
GET /api/files/:id/download| Query param | Type | Default | Description |
|---|---|---|---|
expires_in | number | 3600 | URL expiration time in seconds |
Response (200):
{
"url": "https://<project>.supabase.co/storage/v1/object/sign/files/<filename_disk>?token=...",
"expires_in": 3600
}Import from URL
Download a remote file and store it in Supabase Storage:
curl -X POST https://your-domain.com/api/files/import \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/image.jpg", "data": {"title": "Imported Image"}}'Returns 200 with the imported file data:
{
"data": {
"id": "550e8400-...",
"filename_download": "image.jpg",
"title": "Imported Image",
"...": "..."
}
}Folder Organization
Files can be organized into hierarchical folders.
Create a Folder
curl -X POST https://your-domain.com/api/folders \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Blog Images", "parent": null}'Response (201):
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Blog Images",
"parent": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}List Folders
GET /api/foldersResponse (200):
{
"data": [
{
"id": "550e8400-...",
"name": "Blog Images",
"parent": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"meta": {
"filter_count": 1,
"total_count": 1
}
}Update a Folder
curl -X PATCH https://your-domain.com/api/folders/<id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Renamed Folder"}'Response (200):
{
"data": {
"id": "550e8400-...",
"name": "Renamed Folder",
"parent": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
}Setting a folder as its own parent (PATCH /api/folders/:id with "parent": "<same_id>") is rejected with a 400 error: A folder cannot be its own parent.
Delete a Folder
curl -X DELETE https://your-domain.com/api/folders/<id> \
-H "Authorization: Bearer <token>"Returns 204 No Content on success.
A folder with subfolders or containing files cannot be deleted. Attempting to delete a non-empty folder returns a 400 error with code FOLDER_NOT_EMPTY. Delete all files and subfolders first.
Upload into a Folder
curl -X POST https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-F "file=@hero.jpg" \
-F "folder=<folder_id>"Batch Operations
Batch Update
Update metadata for multiple files in a single request. Two formats are supported:
Keys format — apply the same update to multiple files:
curl -X PATCH https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"keys": ["id-1", "id-2", "id-3"], "data": {"title": "Bulk Updated"}}'Array format — apply per-file updates:
curl -X PATCH https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '[
{"id": "id-1", "title": "New Title A"},
{"id": "id-2", "title": "New Title B", "description": "Updated desc"}
]'Returns 200 with the updated file data.
Batch Delete
Batch delete accepts a JSON body with an array of file IDs (or an object with a keys array):
# Batch delete by ID list
curl -X DELETE https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '["id-1", "id-2", "id-3"]'
# Alternative: keys object
curl -X DELETE https://your-domain.com/api/files \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"keys": ["id-1", "id-2"]}'Returns 204 No Content.