# Pattern: Document Library

How portal-platform's `Document` works end-to-end.

## Roles

- **Owner** (`Document.ownerId`) — created the document; default editor.
- **Reader** — has `documents.read`; can list + download via signed URL.
- **Writer** — has `documents.write`; can replace versions + edit metadata.
- **Admin** — has `documents.delete`; can soft-delete.

## Upload (signed-URL direct-to-S3)

1. Client `POST /api/documents/intent { fileName, mimeType, sizeBytes }`.
2. Server validates mime/size, generates a `storageKey` = `${tenantId}/documents/${cuid()}/${fileName}`.
3. Server returns `{ signedUploadUrl, storageKey }`.
4. Client `PUT signedUploadUrl` with the file (15-min expiry).
5. Client `POST /api/documents { storageKey, title, description?, tags? }` → server inserts `Document` row.
6. Audit: `document.upload`.

## Download

`GET /api/documents/:id/url` → returns a `signedDownloadUrl` (default 15-min expiry). Audit: `document.download`.

## Versioning

A new version uses the same `Document.id` and increments `version`. Old versions are archived under `${storageKey}.v${n}` — never overwritten in place.

## Soft delete

`DELETE /api/documents/:id` sets `deletedAt`. The blob stays in S3 for the retention window (`docs/compliance/data-retention.md`) then a scheduled job removes it.

## Tags

`tags String[]` — keep them tenant-local; no central tag table.

## Search

- Phase 1 search: `WHERE title ILIKE '%q%'` (Postgres `pg_trgm` index).
- Phase 2 (future): OpenSearch / Meilisearch sync from the `documents` table.

## Anti-patterns

- Don't proxy the file through the API host (use signed URLs).
- Don't store the blob in Postgres.
- Don't trust client-reported `mimeType`/`sizeBytes` — re-validate from the uploaded object's metadata.

## Related

- `.claude/skills/file-upload/SKILL.md`
- `enterprise-modules/file-storage/README.md`
- `prisma/schema.prisma` (`Document`)
