
# Form File Uploads

Forms can collect files — resumes, invoices, photos, documents — and persist them to your own storage instead of a third-party CDN. Attachment fields render a file picker (and optional drag-and-drop zone), validate type and size before upload, show progress and previews, and submit a canonical `{ url, name, size, mimeType }` metadata object. Files land in a configured `app.buckets[]` destination on your S3, local disk, or database storage.

```yaml
buckets:
  - name: applications
    default: true
    backend: s3
    config: { bucket: $env.S3_BUCKET, region: us-east-1 }

forms:
  - id: 1
    name: apply
    title: Job Application
    path: /apply
    submitTo: { table: applications }
    fields:
      - { kind: standalone, name: full_name, inputType: single-line-text, required: true }
      - kind: standalone
        name: resume
        inputType: attachment
        label: Resume
        required: true
        accept: 'application/pdf,.doc,.docx'
        maxFileSize: 10485760 # 10 MB
        dropZone: true
```

## Attachment Field Configuration

Attachment behavior is configured the same way on both field kinds: a `kind: standalone` field with `inputType: attachment`, or a `kind: table-field` whose underlying column is `single-attachment` / `multiple-attachments`. The upload props are shared.

| Property      | Description                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `accept`      | Comma-separated MIME types or extensions restricting the file dialog (e.g. `image/*`, `application/pdf`, `.doc,.docx`). |
| `maxFileSize` | Maximum size in bytes per file. Larger files are rejected with an inline error before upload starts.                    |
| `maxFiles`    | Maximum number of files (multiple-file fields only). Selecting more surfaces an inline error.                           |
| `dropZone`    | Render a drag-and-drop area alongside the picker, accepting the same files.                                             |

:::callout
**Single vs multiple.** A single-file attachment submits one `FileMetadata` object (or `null` when empty); a multiple-file attachment submits a `FileMetadata[]` array (or `[]`). For table-bound fields the multiplicity is inferred from the column type (`single-attachment` vs `multiple-attachments`); for standalone fields it follows the upload UI presented by `inputType: attachment` and `maxFiles`.
:::

## Bucket Binding

The storage destination comes from `app.buckets[]`. A form inherits a **default bucket**: the bucket whose `default: true` flag is set, or the only bucket when exactly one is defined. Files are uploaded there on submit, and the form payload carries canonical metadata for each file. Per-field bucket overrides are reserved for a follow-up release.

```yaml
buckets:
  - name: documents
    default: true
    backend: local
    config: { path: ./storage/documents }

forms:
  - id: 2
    name: invoice-upload
    title: Upload Invoice
    submitTo: { automation: process-invoice }
    fields:
      - { kind: standalone, name: vendor, inputType: single-line-text, required: true }
      - kind: standalone
        name: document
        inputType: attachment
        accept: 'application/pdf,image/*'
        maxFileSize: 20971520 # 20 MB
        dropZone: true
```

## Upload UX

The attachment field renders a complete upload experience:

| Behavior             | Description                                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Type filter          | `accept` restricts both the browser dialog and drag-and-drop; mismatched files surface an inline error.             |
| Size validation      | `maxFileSize` rejects oversized files before upload begins.                                                         |
| Progress indicator   | A progress bar or spinner is shown while each file uploads.                                                         |
| Preview              | Image files render a thumbnail after upload; non-image files render filename + size.                                |
| Remove               | Selected files can be removed before submit (keyboard-accessible); removing one of many does not affect the others. |
| Required enforcement | A `required` attachment field with no file blocks submission with an inline error.                                  |

## File Metadata Shape

When the form submits, each attachment value is normalized to a canonical metadata object — the same shape consumed by `submitTo.table`, `submitTo.automation` triggers, and the `form_submissions` ledger `data` payload.

```typescript
type FileMetadata = {
  url: string // canonical URL into the bucket (signed when the bucket is private)
  name: string // original filename
  size: number // bytes
  mimeType: string // detected MIME type
}
```

A single-file field submits `FileMetadata | null`; a multiple-file field submits `FileMetadata[]`. Routing the upload to an automation makes the same metadata available in the trigger payload — for example to run an AI extraction over `{{trigger.data.document.url}}`.

## Related Pages

- [Form Fields](/en/docs/form-fields) — the field kinds attachment uploads extend.
- [Forms Overview](/en/docs/forms-overview) — `submitTo` targets that receive file metadata.
- [Submissions](/en/docs/form-submissions) — file metadata stored in the ledger `data` payload.
- [Buckets](/en/docs/buckets-overview) — configuring S3, local, and database storage backends.
