Upload File
Frontbacked lets theme code put File or Blob values directly inside post data. The runtime finds them anywhere in the post object, uploads them with a resumable flow, and saves normal file metadata back into the post.
Use the same API for files, images, PDFs, audio, videos, and nested file arrays. You do not need a separate upload endpoint in your theme.
Upload a Post with Files
<input id="title" type="text">
<input id="hero" type="file" accept="image/*">
<input id="deck" type="file" accept="application/pdf">
<button id="save">Save</button>
document.querySelector("#save").addEventListener("click", async () => {
const result = await Frontbacked.uploadPost({
type: "articles",
post: {
title: document.querySelector("#title").value,
heroImage: document.querySelector("#hero").files[0],
resources: {
deck: document.querySelector("#deck").files[0]
}
},
onUploadProgress(progress) {
console.log(progress.fieldPath, progress.percent, progress.status);
}
});
if (!result.ok) {
Frontbacked.alert(result.error || "Could not save article.");
}
});
uploadPost and updatePost scan nested objects and arrays, so all of these work:
await Frontbacked.uploadPost({
type: "gallery",
post: {
title: "Launch day",
cover: coverFile,
attachments: [pressKitFile, brochureFile],
sections: [
{ title: "Intro", image: introImageFile },
{ title: "Demo", video: demoVideoFile }
]
}
});
Update a Post with Files
await Frontbacked.updatePost({
id: articleId,
type: "articles",
post: {
title: "Updated title",
heroImage: document.querySelector("#hero").files[0]
},
merge: true,
onUploadProgress(progress) {
updateProgressBar(progress.fieldPath, progress.percent);
}
});
When a post has files, the post data is still sent as JSON. Frontbacked uploads each file separately before attaching the final file metadata to the post.
Upload Progress
onUploadProgress receives one event per file. It includes the post id, field path, file identity, status, bytes, percentage, and whether the upload can be resumed.
await Frontbacked.uploadPost({
type: "videos",
post: { title, video: file },
onUploadProgress(progress) {
Frontbacked.setState(`uploads.${progress.sessionId}`, progress);
}
});
Progress events look like this:
{
id: "upload_session_id",
sessionId: "upload_session_id",
postId: "post_id",
postType: "videos",
fieldPath: "post[video]",
path: ["video"],
fileId: "file_id",
mediaId: "media_id_or_null",
fileName: "intro.mp4",
mimeType: "video/mp4",
uploaded: 33554432,
total: 94371840,
percent: 35.6,
phase: "uploading",
status: "uploading",
resumable: true,
error: null
}
Frontbacked also emits browser events:
window.addEventListener("frontbacked:upload-progress", (event) => {
console.log(event.detail);
});
window.addEventListener("frontbacked:video-upload-progress", (event) => {
console.log(event.detail);
});
The video-specific event is only emitted for video files. The general upload event is emitted for every file type.
Stored File Shape
After upload, file fields become metadata objects:
{
url: "uploads/...",
size: 18492320,
mimeType: "image/jpeg",
fileName: "cover.jpg",
uploadTime: "2026-07-20T12:00:00.000Z",
upload: {
id: "upload_session_id",
sessionId: "upload_session_id",
fileId: "file_id",
mediaId: null,
status: "completed",
progress: 100,
uploaded: 18492320,
total: 18492320,
resumable: false,
error: null
}
}
Video files also receive a media object when they are read back. See Adaptive Video for the playback fields.
Resume Interrupted Uploads
Use one API: Frontbacked.uploads.resume().
The simplest version takes a post object or post id:
await Frontbacked.uploads.resume({
post
});
If the original file is not already available in the current page, Frontbacked opens a small resume modal. The user chooses the same file, gets a preview when possible, and the upload continues from the uploaded parts instead of starting over.
You can also supply files yourself:
await Frontbacked.uploads.resume({
post: postId,
files: document.querySelector("#resumeFiles").files,
onProgress(progress) {
renderResumeProgress(progress);
}
});
For fully custom UI, return the file from onFile:
await Frontbacked.uploads.resume({
post,
async onFile(pendingFile) {
return await askUserForOriginalFile(pendingFile);
},
onProgress(progress) {
console.log(progress.percent);
}
});
Frontbacked.uploads.pending(post) returns only unfinished files discovered in a post:
const pending = Frontbacked.uploads.pending(post);
if (pending.count) {
showResumeButton();
}
The result shape is:
{
ok: true,
postId: "post_id",
count: 1,
files: [
{
sessionId: "upload_session_id",
postId: "post_id",
fieldPath: "post[sections][0][video]",
path: ["sections", "0", "video"],
fileName: "demo.mp4",
mimeType: "video/mp4",
size: 120000000,
uploadedBytes: 90000000,
progress: 75,
status: "uploading",
resumable: true
}
]
}
Live Upload State
Use Frontbacked.uploads.status() when you want upload state for files being uploaded in the current page session.
const allActiveUploads = Frontbacked.uploads.status();
const oneUpload = Frontbacked.uploads.status(sessionId);
Request Limits
Theme FRL can set stricter rules such as maxSize, mimeTypes, and maxDurationSeconds. These Frontbacked limits still apply even when FRL allows more.
| Data | Default limit | Notes |
|---|---|---|
| Post JSON body | 1MB | The JSON passed to uploadPost or updatePost, excluding file bytes. |
| User data JSON | 1MB | The data object used by sign up and updateUserData. |
| Auth JSON body | 1MB | Sign up, sign in, email verification, and password reset style requests. |
| Username | 64 bytes | Applies where username is accepted. |
| 320 bytes | Applies where email is accepted. | |
| Name | 180 bytes | Applies where name is accepted. |
| Password fields | 1024 bytes | Applies to password, current password, and new password fields. |
| File upload | 1GB | FRL maxSize can make this smaller for a field. |
| Video upload | 1GB | Videos are also file uploads and can be restricted with FRL. |
| Video duration | 6 hours | FRL maxDurationSeconds can make this smaller for a video field. Longer videos are prepared up to the allowed duration. |
| Resume window | 24 hours | Resume interrupted uploads while the upload session is active. |
| Image resize output width/height | 2400px | Larger resize requests are rejected or normalized by the image helper. |
These limits keep theme requests predictable and help users get clear validation errors when submitted data is too large.