---
name: file-upload
description: S3-compatible upload with signed URLs via @loma-portal/em-file-storage. Use for any file handling.
---

# Skill: File Upload

## When to use

Document library, avatars, attachments — anything stored as a blob.

## Pattern: signed-URL direct upload (preferred)

1. Client asks the API for a signed upload URL (`getSignedUploadUrl(key)`).
2. Client PUTs the file directly to S3 — the API host stays out of the data path.
3. Client notifies the API; the API records `Document` metadata (key, mime, size).
4. Downloads use `getSignedDownloadUrl(key)` (default 15-min expiry).

## Steps

1. Key convention: `<tenantId>/<entity>/<id>/<filename>`.
2. Validate mime + size **server-side** before issuing the upload URL.
3. Store metadata in the `documents` table; never store the blob in Postgres.

## Gotchas

- Objects are **private**; share only via signed URLs.
- For MinIO/R2, set `endpoint` + `forcePathStyle: true` (auto when endpoint is set).
- Virus-scan hook belongs between upload-complete and metadata-commit (Phase 8+).
- Enforce a max size at the signed-URL step (S3 conditions) AND validate after.

## Validation

Upload → download round-trips; private without a signed URL; tenant-prefixed key; size/mime validated.
