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.

DataDefault limitNotes
Post JSON body1MBThe JSON passed to uploadPost or updatePost, excluding file bytes.
User data JSON1MBThe data object used by sign up and updateUserData.
Auth JSON body1MBSign up, sign in, email verification, and password reset style requests.
Username64 bytesApplies where username is accepted.
Email320 bytesApplies where email is accepted.
Name180 bytesApplies where name is accepted.
Password fields1024 bytesApplies to password, current password, and new password fields.
File upload1GBFRL maxSize can make this smaller for a field.
Video upload1GBVideos are also file uploads and can be restricted with FRL.
Video duration6 hoursFRL maxDurationSeconds can make this smaller for a video field. Longer videos are prepared up to the allowed duration.
Resume window24 hoursResume interrupted uploads while the upload session is active.
Image resize output width/height2400pxLarger 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.