Custom File Uploads

Start of documentation content

Unlike other form builders Formally allows for complete control of the files uploaded in forms, allowing you to send them to your own servers (bonus privacy!)

Other cloud form providers insist on hosting the files your users upload. We offer that feature, but we don't insist on it. If you want to keep the files to yourself that's fine. This concept is commonly associated with Data Sovereignty.

You'll need to set up your own file host with an upload API. Eg, an AWS S3 bucket using signed URLs, or any file host that you can upload files to via JavaScript). Setting up your own file host server and upload API is outside of the scope of this documentation.


In React the <Formally> component can be given an onUpload prop to handle LiveUploads.

The function is async and it is responsible for uploading the files and returning an array of file upload successes or errors. LiveUploads can allow multiple files to be uploaded in a single <input type="file"> so each file is an item in this array.

Because the function is async you can await the upload of each user file to your custom file host. For performance reasons you might want to use FormData, await Promise.all, or similar to simultaneously upload all files at once.

While uploads are occuring you can optionally inform the user of upload progress by repeatedly calling the setProgressRatio function with a number between 0 and 1. 0 is 0% complete, and 1 is 100% complete. If there are multiple files uploaded at once be sure to set a fraction of that ratio per file.

Once files are uploaded the function's response should be an array of either successes or errors:

For success:

export type LiveUploadFileResponseSuccess = {
  type: 'success';
  originalFilename: string;
  key: string;

The key is a unique file upload id (ie, an S3 key or equivalent).

And for errors:

export type LiveUploadFileResponseError = {
  type: 'error';
  localisedMessage: string;

This return value of an array of file upload successes or errors becomes the field value submitted to the server when the user hits the submit button. So the binary file data itself isn't submitted with the form, but the success's key string will identify the file.

File host server

As noted setting up your own file host server and upload API is outside of the scope of this documentation.

However there are some considerations that are worth mentioning.

LiveUpload files are uploaded immediately when a user chooses the file — long before a user submits the form. This is done to optimise for user bandwidth (uploading earlier means it will complete faster), and to give user feedback nearer to where they can fix any errors (on the same page of the form). It also means required fields can prevent page navigation until a file is provided.

File host upload garbage collection

It's possible for a user to repeatedly change their mind about the files they wish to upload. As such, they might upload more files than those that are eventually submitted in the LiveUpload value array of file upload successes and errors. To handle this any file host server should periodically 'garbage collect' files that meet these two conditions:

  • are a few hours old, AND
  • have keys that weren't in any form submission.

The amount of time ("a few hours old") should reflect a worst-case long duration of time for a user to complete a form, so feel free to instead choose any duration time you think is appropriate.

If a file host's key isn't in any form submission then one of either of these scenarions occurred:

  1. the user hasn't submit the form yet, or
  2. the user changed their mind and instead uploaded a different file.

Because file upload hosts can't distinguish between these scenarios the necessity of the 2 conditions becomes clear. Garbage collect only those files that are a few hours old and have an unknown key.

File host server legal obligations

Operating any file host comes with legal obligations to takedown files that violate laws (copyright, abuse images, etc). Be sure your custom file host has staff to available to comply.

Use of onUpload

import { Formally } from 'formally';

  onUpload={async ({
    // Can be called multiple times to indicate upload progress.
    // Set the value between 0 and 1.
    abortControllerSignal, // See MDN abortControllerSignal
  }) => {
    const { files } =;
    if (!files) return [];

    // If you were to upload files it might look like...

    const formData = new FormData(); // See MDN FormData
    Array.from(files).forEach((file, index) => {
      formData.append(`file${index}`, file);
    const response = await fetch(``, {
      method: 'POST',
      body: formData,
    const uploadResults = await response.json();

    return uploadResults;