Iwf Guide

Iwf Guide

File Uploads and Storage

Iwf separates multipart field parsing from file storage. Route and form decoding can read ordinary multipart fields directly; applications provide file-aware decoders only when they need uploaded file contents.

1. Multipart Data

The upload layer exposes:

record UploadedFile where
  constructor MkUploadedFile
  fieldName : String
  fileName : String
  contentType : String
  contents : String

record MultipartFormData where
  constructor MkMultipartFormData
  formFields : List (String, String)
  uploadedFiles : List UploadedFile

Use a MultipartFileDecodeHook when a route needs uploaded files:

decodeMultipartFormData : MultipartFileDecodeHook -> Request -> Either String MultipartFormData

Iwf also includes a framework parser for standard in-memory multipart uploads:

formData : Either String MultipartFormData
formData =
  parseMultipartFormData request

Because parsed UploadedFile values keep contents in memory, set explicit limits for production endpoints:

limits : UploadLimits
limits =
  MkUploadLimits 10485760 52428800

formData : Either String MultipartFormData
formData =
  parseMultipartFormDataWithLimits limits request

UploadLimits checks the maximum bytes for each file and the maximum total bytes across all parsed files. Use defaultUploadLimits when the endpoint does not need a custom policy.

When a typed form needs only normal fields for route decoding, no hook is required. multipartFieldsOnlyHook remains useful when you already have a file-aware decoder and want to feed only its form fields into route decoding:

multipartFieldsOnlyHook fileAwareHook

Then read files from the full multipart data:

avatar : Maybe UploadedFile
avatar =
  multipartFile "avatar" formData

attachments : List UploadedFile
attachments =
  multipartFilesFor "attachments" formData

2. Local Storage

Store under a local directory:

avatarStorage : FileStorage
avatarStorage =
  localFileStorage (MkLocalFileStorage "uploads" "/uploads")

saveAvatar : UploadedFile -> IO (Either String StoredFile)
saveAvatar file =
  storeUploadedFile avatarStorage file

Local storage sanitizes filenames and returns a StoredFile descriptor with:

  • backend
  • key
  • original filename
  • content type
  • byte length
  • storage location
  • public URL

Storage keys are collision-safe by default. localStorageKey and s3StorageKey use uploadCollisionSafeKey, which combines a sanitized field name, deterministic content hash, and sanitized original filename.

Persist metadata through the explicit descriptor helper:

metadata : StoredFileMetadata
metadata =
  storedFileMetadata storedFile

Use local storage for development, tests, or deployments where the upload directory is persistent and backed up.

3. S3-Compatible Storage

Build an S3-compatible storage value:

s3Storage : S3FileStorage
s3Storage =
  MkS3FileStorage
    "https://s3.example.test"
    "my-bucket"
    "uploads"
    "https://cdn.example.test/uploads"

Iwf builds the PUT object request:

request : S3PutObject
request =
  s3PutObjectRequest s3Storage file

The app provides transport/signing:

saveToS3 : UploadedFile -> IO (Either String StoredFile)
saveToS3 file =
  storeUploadedFileS3 s3Storage transport file

This keeps cloud credentials and provider-specific request signing in the application boundary. Use signedS3Transport when the app wants Iwf to run a signing hook before the transport:

signer : S3RequestSigner
signer request =
  pure (Right ({ s3Headers := ("Authorization", "...") :: request.s3Headers } request))

transport : S3Transport
transport =
  signedS3Transport signer unsignedTransport

4. Private And Signed URLs

For private files, clear the public URL before persisting or returning the descriptor:

privateFile : StoredFile
privateFile =
  privateStoredFile storedFile

For temporary signed URLs, provide an app-owned hook:

signer : SignedUrlHook
signer stored =
  pure (Right (stored.storedPublicUrl ++ "?signature=app-owned"))

signed : IO (Either String StoredFile)
signed =
  signStoredFileUrl signer storedFile

Iwf defines the hook boundary and updates the StoredFile descriptor. The app owns signing algorithms, credentials, expiry, and access-control policy.

5. Forms

File forms should use direct HTML with a multipart route action. The route body can include UploadedFile, Maybe UploadedFile, or List UploadedFile fields:

uploadForm : ControllerContext -> Html
uploadForm context =
  <form method="post" action={pathTo uploadAvatarRoute} enctype="multipart/form-data">
    <label class="field">
      <span>Avatar</span>
      <input name={avatarField} type="file" />
    </label>
    <button type="submit" class="btn-primary">Upload</button>
  </form>

Next

Read Integrations, I18n, and Payments.