[@layout]: ../docs.layout.md

# Attachments

Let a reader upload a project file, download it later, or remove it. The
`@pathmx/uppy` package adds the upload controls and stores an attachment record
and binary file in the reader's writable Home.

---

## Install

Start with the [local account and writable Home setup](./auth.reference.md#local-actor)
in an initialized project. Then install the uploader:

```sh
bun run pmx plugins add uppy
```

The default policy accepts JPEG, PNG, WebP, GIF, PDF, and ZIP files, up to
25 MiB per file and five selected files per declaration. Configure the
deployment policy through a repository Plugin when needed. Use this configured
entrypoint instead of also activating the default package in `pathmx.plugins`:


```ts
// plugins/attachment.plugin.ts
import { UppyAttachmentPlugin } from "@pathmx/uppy"

export default UppyAttachmentPlugin({
  limits: {
    maxFileSize: 50 * 1024 * 1024,
    maxFiles: 5,
    allowedFileTypes: ["image/*", ".pdf", ".zip"],
  },
})
```

---

## Author an uploader

Create `paths/project.page.md`:

```md
# Share your project

<x-auth-session />

<x-attachment-upload
  name="project"
  label="Upload project PDF"
  accept=".pdf"
  max-files="1"
  max-size="25 MiB"
/>
```

Run `bun run dev`, open `/project.page`, and sign in as Ada. Select a PDF smaller
than 25 MiB and upload it. The saved selection offers its file for download and
removal; reload and download it to verify persistence. Try a PNG: this declaration
only accepts PDF, so choose an allowed file and retry. Its limits cannot widen
the deployment policy. If uploads are unavailable, check admission and Home write
access. Production needs [configured credentials](./auth.reference.md#magic-link-setup)
and persistent writable hosting.

---

## Verify a browser upload

In Playwright, select the file through the real file input with `setInputFiles`,
then submit the uploader's normal control. For the native multipart form, submit
that form rather than posting a hand-built replacement request. Verify the saved
file survives reload and downloads with the expected bytes, and that another
Actor cannot retrieve it. A hung automation upload command alone does not prove
that the application's multipart route failed.

---

## Hosting and persistence

The native multipart form works without JavaScript on HTTP hosts. With JavaScript,
Uppy Dashboard uses the document's active View connection for uploads; the same
Component works with HTTP and browser-worker hosts. Removal forms and download
links use that connection too.

Browser hosts preserve binary files and their media types in IndexedDB, alongside
the readable Attachment State Source. Upload success confirms that the file and
its record were saved. Retrying the same request after a lost response reuses the
record. Worker transfers buffer file bodies; streaming and byte-by-byte upload
progress are not supported.

The file write precedes the Source commit. An ordinary failed Source commit removes
the new file; a crash between these writes can leave an unreferenced file. The pair
is not a single atomic transaction.

---

## Consume a selection

A domain Plugin can use the reader returned by `createUppyAttachmentPlugin()`
from `@pathmx/uppy`. It resolves validated records for one Actor, Source, and named
selection. Ordinary Actions consume saved record ids or hrefs rather than raw
browser file objects; Attachment State retains binary authority and lifecycle
metadata. The uploader above is complete without a custom domain Plugin.

See [Actions](../extend/actions.reference.md) for transaction and authorization
setup when attaching those saved records to domain work.
