# Phase 5: Files - Discussion Log

> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.

**Date:** 2026-05-15
**Phase:** 5-files
**Areas discussed:** Upload method, Storage schema, Page layout, File restrictions

---

## Upload method

| Option | Description | Selected |
|--------|-------------|----------|
| Server-proxied | Client POSTs multipart form to Go server; Go streams to S3. Simpler auth, easier server-side enforcement. | ✓ |
| Presigned PUT | Server generates signed URL; client PUTs directly to R2/MinIO. Offloads bandwidth but requires CORS config and two-step flow. | |

**User's choice:** Server-proxied

| Option | Description | Selected |
|--------|-------------|----------|
| Cloudflare R2 | Use R2 as target for v1, same as JS stack. | |
| Self-hosted MinIO | Run MinIO in compose stack. Fully self-contained. | |
| Configurable via env | S3_ENDPOINT/S3_BUCKET/etc as env vars. R2 in prod, MinIO in dev. Zero code difference. | ✓ |

**User's choice:** Configurable via env (support both R2 and MinIO)
**Notes:** User asked about self-hosting S3 storage before selecting — clarified that MinIO is a drop-in S3-compatible option, same Go SDK, just different endpoint URL.

| Option | Description | Selected |
|--------|-------------|----------|
| Yes — add MinIO to compose.yaml | Real S3-compatible bucket locally; no external credentials needed. mc init container creates the bucket. | ✓ |
| No — skip MinIO in compose.yaml | Developers point at R2 or external bucket. Simpler compose.yaml. | |

**User's choice:** Yes — add MinIO to compose.yaml

---

## Storage schema

| Option | Description | Selected |
|--------|-------------|----------|
| Tablo-scoped with UUID: files/{tablo_id}/{uuid} | Tablo ownership visible in key; per-tablo listing trivial; no collisions. | ✓ |
| Flat UUID only: files/{uuid} | Simpler key; tablo grouping only in DB. | |
| Content hash (dedup): files/{sha256} | Dedup via hash; complicates delete if shared across tablos. | |

**User's choice:** Tablo-scoped with UUID

| Option | Description | Selected |
|--------|-------------|----------|
| Server-detect from file bytes | net/http.DetectContentType on first 512 bytes. Ignores browser MIME, prevents spoofing. | ✓ |
| Trust client MIME + extension fallback | Use browser Content-Type, fall back to mime.TypeByExtension. Simpler but spoofable. | |

**User's choice:** Server-detect from file bytes

| Option | Description | Selected |
|--------|-------------|----------|
| Allow duplicates | Each upload gets own UUID, row, and S3 object. No dedup logic. | ✓ |
| Reject exact duplicates by filename | Error if same filename already exists in tablo. | |
| Reject by content hash | Store SHA-256; reject if hash + tablo_id already exists. | |

**User's choice:** Allow duplicates (no dedup for v1)

---

## Page layout

| Option | Description | Selected |
|--------|-------------|----------|
| Single scroll — files section below kanban | Append files section below kanban on same page. No tabs. HTMX-friendly. | |
| Tabs — Overview / Tasks / Files | Introduce tab navigation. Server-rendered, HTMX tab switching. | ✓ |

**User's choice:** Tabs

| Option | Description | Selected |
|--------|-------------|----------|
| Tasks / Files (2 tabs) | Minimal. Tablo title/description as header above tabs. | |
| Overview / Tasks / Files (3 tabs) | Overview = tablo metadata (editable). Tasks = kanban. Files = files. | ✓ |

**User's choice:** Overview / Tasks / Files (3 tabs)

| Option | Description | Selected |
|--------|-------------|----------|
| HTMX + URL push | Each tab is a server sub-route; hx-push-url; back-button works; shareable URLs. | ✓ |
| HTMX swap only (no URL change) | Simpler; no sub-routes; back-button doesn't navigate between tabs. | |

**User's choice:** HTMX + URL push

---

## File restrictions

| Option | Description | Selected |
|--------|-------------|----------|
| 10 MB default | Covers most docs/images. Configurable via MAX_UPLOAD_SIZE_MB. | |
| 25 MB default | Covers larger exports and presentations. Reasonable for single-VPS proxy. | ✓ |
| 50 MB default | Generous; for design files or videos. | |

**User's choice:** 25 MB

| Option | Description | Selected |
|--------|-------------|----------|
| Allow all types | No allowlist. Content-type stored but not used to gate. | ✓ |
| Restrict to safe allowlist | Only images, documents, archives. Reject executables. | |

**User's choice:** Allow all types for v1

---

## Claude's Discretion

- Exact Tailwind styling for file list rows and upload form
- Tab active/inactive visual treatment (underline vs. filled)
- Default tab when visiting `/tablos/{id}` (Overview is natural default)
- Whether upload form is always visible or collapsed behind an "Attach file" button
- HTTP verb for file upload: `POST /tablos/{id}/files`
- File delete UX: inline confirmation (Phase 3 D-07 pattern) vs. modal
- Signed URL TTL for downloads (ROADMAP suggests ~5 minutes)
- Download behavior: 302 redirect to signed URL vs. `target="_blank"` link

## Deferred Ideas

- **Self-hosted MinIO in production** — viable on a single VPS but deferred to Phase 7 (Deploy) or v2
- **File type allowlist / restriction** — content-type stored in DB so adding enforcement later is a validation check, not a schema change
- **Per-user storage quota** — out of v1 scope
- **File previews / thumbnails** — image/PDF preview deferred to v2
- **Orphan file cleanup job** — deferred to Phase 6 (background worker)
