# Uploads and storage

Last updated: 2026-07-12

Web page: https://fillo.so/docs/uploads
Docs overview: https://fillo.so/docs.md
Quickstart: https://fillo.so/docs/embed.md
Agent setup: https://fillo.so/agents.md

Send respondent files directly to Google Drive, S3-compatible storage, or Box.

## Choose a storage provider

Fillo does not turn uploads into a separate file silo. Connect storage once at the workspace level, then choose a destination per form. The browser sends bytes to that provider while Fillo keeps the response-safe file reference and verifies completion.

| Provider | Use it when | Transfer path |
| --- | --- | --- |
| Google Drive | Your team already works from Drive and wants OAuth-managed access. | Browser-direct, resumable Drive upload. |
| Amazon S3 / Cloudflare R2 | You want bucket ownership, lifecycle rules, and infrastructure-level control. | Browser-direct multipart upload with scoped signed requests. |
| Box | Your organization keeps operational files in Box. | Browser-direct upload; larger files use Box upload sessions. |

> **Connection and destination are separate:** A workspace connection makes a provider available. A form's **File storage** setting decides which connected provider receives that form's files.

## Prepare an S3 or R2 bucket for recoverable deletion

Fillo accepts an S3-compatible connection only after it proves the permissions and provider behavior needed for browser-direct multipart uploads, crash recovery, and permanent file deletion. The same erasure capability is checked again before respondent bytes are accepted or materialized.

- Use a network-reachable HTTPS endpoint and keep the bucket private. Scope credentials to the `fillo/` prefix with object read, write, and delete; multipart create, list, and abort; bucket-version listing; and lifecycle-read permissions.
- Allow browser `PUT` requests from every hosted or embedded form origin in the bucket CORS policy.
- For AWS S3, use a bucket that has never had Versioning enabled. Enabled and suspended versioning are rejected. MFA Delete and Object Lock must also be off because they can prevent immediate erasure.
- Cloudflare R2 does not implement object versioning. Fillo accepts that behavior only for recognized Cloudflare R2 endpoints; an unknown S3-compatible endpoint that skips the check fails closed.
- Add an enabled `AbortIncompleteMultipartUpload` lifecycle rule covering `fillo/` with 7 days or fewer.
- Connection verification creates an empty multipart upload, proves it can be rediscovered and aborted, then creates empty sentinel objects and proves permanent deletion. It never uploads respondent content during the check.

## Connect, assign, and publish

1. **Connect the provider:** Open **Settings → Connections**. Connect Google, Box, or an S3-compatible bucket. Keep OAuth grants and bucket credentials limited to the workspace that needs them.
2. **Assign it to the form:** Open the form's **Settings → File storage** and select the destination. A connection can serve several forms, but each form chooses its own active provider.
3. **Add file fields and publish:** Use the builder or a code-defined `file_upload` field (`<Fillo.FileUpload>` in JSX). A form that collects files cannot be published without a supported destination. A live file form also cannot remove its destination; unpublish it or select another destination first.
4. **Test the real path:** Submit a representative file from the hosted form or embed, confirm it appears in the provider, then open it from the response drawer as a workspace member.

## What happens during an upload

- The SDK asks Fillo for an upload session bound to the form, field, provider, filename, MIME type, and expected size.
- The browser transfers bytes to Drive, S3/R2, or Box. Provider credentials and OAuth refresh tokens stay server-side; the browser receives only the scoped capability needed for that upload.
- The SDK reports completion to Fillo. The storage adapter verifies the provider result before the file reference can be submitted with the response.
- The response stores file metadata and a provider reference, not a public provider credential. Opening a response file through Fillo requires signed-in workspace access.

> **Do not treat a client callback as proof:** A custom uploader still needs Fillo's completion call. The server-side adapter verifies the object or provider file before accepting the reference.

## Custom upload UI and resume handles

The default React and DOM renderers already run the upload protocol. If your headless screen owns the file picker, call `client.uploadFile()` and store the returned file value in the form answer.

For reload-safe recovery, persist the `sessionId` and `uploadToken` supplied to `onSession`, then pass them back with the same file. Resume behavior follows the provider protocol; do not reuse a handle for a different file or field.

```ts
const fileValue = await client.uploadFile(formId, file, {
  fieldId: "attachment",
  onProgress: ({ fraction }) => setProgress(fraction),
  onSession: ({ sessionId, uploadToken }) => {
    saveResumeHandle({ sessionId, uploadToken });
  },
});
```

## Switch or disconnect storage

Changing a form's destination affects new upload sessions. Existing responses keep references to the provider that accepted their files, so switching providers does not migrate historical objects.

- Let active uploads finish or ask respondents to restart after the change; completion rejects a session if the form's storage changed mid-upload.
- Unpublish a live form with file fields before removing its destination, or choose another connected destination in the same settings panel.
- Keep the old provider connection while historical files still need to open or be erased. If access is revoked externally, reconnect the same account or bucket before attempting cleanup.
- Provider lifecycle rules are yours. For S3/R2, retain Fillo's object prefix and incomplete-multipart cleanup requirements when changing bucket policy.

## Troubleshoot uploads

| Symptom | Check |
| --- | --- |
| The form cannot publish | A file field exists but the form has no connected storage destination. |
| S3/R2 fails before transfer | Reopen the connection and verify endpoint, region, bucket permissions, CORS, and multipart lifecycle access. |
| Completion says storage changed | The form switched providers during the session. Restart the upload against the current destination. |
| A historical file will not open | Confirm the viewer is a workspace member and the original provider connection can still read that object. |
| A resumed upload is rejected | Use the original file, field, session id, and upload token; expired or completed sessions must restart. |

## Next steps

- [Add a file field](https://fillo.so/docs/authoring#components): Use the schema and JSX field catalog for code-defined forms.
- [Build custom UI](https://fillo.so/docs/custom-ui): Own the picker and progress experience without replacing the upload protocol.
- [Review responses](https://fillo.so/docs/responses): Find uploaded files alongside the answers and respondent context.
