Skip to Content
ConnectFiles API

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 folder

File Object

The File entity has the following fields. Fields marked * are always returned; others are null when not applicable.

FieldTypeDescription
id *stringUnique identifier (UUID)
storage *stringStorage bucket name (default: "files")
filename_diskstring | nullFilename as stored on disk
filename_download *stringOriginal filename for downloads
titlestring | nullDisplay title
typestring | nullMIME type
filesize *numberFile size in bytes (0 for metadata-only records)
widthnumber | nullImage width in pixels (not computed by server — set client-side)
heightnumber | nullImage height in pixels (not computed by server — set client-side)
durationnumber | nullDuration in seconds (video/audio)
charsetstring | nullCharacter encoding for text files
folderstring | nullParent folder ID
descriptionstring | nullDescription of the file
locationstring | nullLocation metadata
tagsstring[]Array of tags
metadataobjectArbitrary JSON metadata (EXIF, etc.)
embedstring | nullEmbed code/URL for external media
focal_point_xnumber | nullFocal point X coordinate for cropping
focal_point_ynumber | nullFocal point Y coordinate for cropping
tus_idstring | nullTUS resumable upload identifier
tus_dataobject | nullTUS upload data
uploaded_bystring | nullUser who uploaded the file
uploaded_onstring | nullUpload timestamp
created_on *stringRecord creation timestamp
modified_bystring | nullUser who last modified the file
modified_on *stringLast modification timestamp

Upload a File

Multipart Upload

Send a multipart/form-data request with the file binary and optional metadata:

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 fieldRequiredDescription
fileYes (multipart)The file binary
titleNoDisplay title
descriptionNoDescription text
folderNoParent folder ID
storageNoStorage 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.

ConstraintWhere it is configuredFallback
Maximum upload sizeBucket File size limit (Studio)FILES_MAX_UPLOAD_SIZE, default 100 MB
Allowed MIME typesBucket Allowed MIME types (Studio)FILES_MIME_TYPE_ALLOW_LIST, default all types
Buckets signed uploads may targetFILES_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:

ParamTypeDescription
limitnumberMaximum number of items to return
offsetnumberNumber of items to skip (for cursor-based pagination)
pagenumberPage number (alternative to offset)
searchstringFree-text search across title and filename
sortstringSort field(s), comma-separated. Prefix with - for descending (e.g. -uploaded_on)
fieldsstringComma-separated list of fields to return (e.g. id,title,filesize)
filterstring (JSON)JSON-encoded filter object (e.g. {"folder":{"_eq":"<id>"}})
metastringSet 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_on

Response (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
ParamTypeDescription
fieldsstringComma-separated list of fields to return
metastringSet 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 paramDescription
downloadForces a file download (Content-Disposition: attachment)
widthResize image to this width (Supabase Pro plans only)
heightResize 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 paramTypeDefaultDescription
expires_innumber3600URL 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/folders

Response (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.