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 in an initialized project. Then install the uploader:

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:

// 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:

# 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 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 for transaction and authorization setup when attaching those saved records to domain work.