# Phase 5: Files - Context

**Gathered:** 2026-05-15
**Status:** Ready for planning

<domain>
## Phase Boundary

A tablo owner can attach files to a tablo — upload via server-proxied multipart POST, list with filename/size/date, download via signed time-limited URLs, and delete. File bytes stored in S3-compatible object storage (Cloudflare R2 in production, MinIO for local dev). Metadata stored in Postgres. All routes scoped to tablo owner.

Delivers FILE-01..06. **Not in scope:** file previews/thumbnails, file versioning, file attachments on tasks (tasks only have title/description in v1), background job for orphan-file cleanup (Phase 6), per-user storage quotas.

This phase also introduces **tab navigation** on the tablo detail page: Overview / Tasks / Files — previously deferred from Phase 4.

</domain>

<decisions>
## Implementation Decisions

### Upload Method
- **D-01:** Server-proxied upload — client submits a multipart form POST to the Go server; Go streams bytes to S3-compatible storage. No presigned PUT URLs for v1. Simpler auth story, easier server-side enforcement of size limits and content-type detection.
- **D-02:** Storage backend is fully configurable via env vars (`S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`). Same Go code works with Cloudflare R2 (production) or self-hosted MinIO (local dev) — just a config change.
- **D-03:** Add a **MinIO service** to `compose.yaml` for local dev. Include an `mc` (MinIO client) init container that creates the bucket on first start. Developers can run the full file upload flow locally without any external credentials.

### Storage Schema
- **D-04:** S3 object keys use `files/{tablo_id}/{uuid}` format (tablo-scoped with UUID). Tablo ownership is visible in the key; per-tablo listing and bulk-delete are trivial. No key collisions possible.
- **D-05:** Content-type is **server-detected** from the first 512 bytes using `net/http.DetectContentType`. The browser-reported MIME type is ignored — prevents spoofing. Detected content-type stored in `tablo_files.content_type` and set as the S3 object's `ContentType` metadata.
- **D-06:** **Allow duplicates** — no dedup logic for v1. Each upload gets its own UUID, DB row, and S3 object. User can upload the same file twice; both appear in the list with their own delete buttons.

### Page Layout — Tab Navigation
- **D-07:** The tablo detail page introduces **3 tabs: Overview / Tasks / Files**. Overview contains the tablo title and description (inline editable, as in Phase 3). Tasks contains the kanban board (Phase 4). Files contains the file list + upload form (this phase).
- **D-08:** Tab switching uses **HTMX + `hx-push-url`**. Each tab is a server-rendered sub-route:
  - `GET /tablos/{id}` → redirects to or renders Overview tab
  - `GET /tablos/{id}/tasks` → Tasks tab fragment
  - `GET /tablos/{id}/files` → Files tab fragment
  - Tab clicks fire `hx-get` + `hx-push-url` so the URL updates and browser back-button navigates between tabs. No JS tab state.

### File Restrictions
- **D-09:** Default max upload size is **25 MB**, configurable via `MAX_UPLOAD_SIZE_MB` env var. Enforced server-side via `http.MaxBytesReader` wrapping the request body before streaming begins, and verified again before writing to S3. Friendly error message surfaced above the upload form on violation (ROADMAP success criterion #6).
- **D-10:** **All file types accepted** — no allowlist for v1. Content-type is server-detected and stored for future use but not used to gate uploads. Users can attach any file type.

### Claude's Discretion
- Exact Tailwind styling for the file list rows (compact vs. card layout, icon per content-type, hover states).
- Tab active/inactive visual treatment (underline vs. filled tab, border style).
- Which tab is the default landing when visiting `/tablos/{id}` for the first time (Overview is the natural default).
- Whether the upload form is always visible at the top of the Files tab or collapsed behind an "Attach file" button.
- HTTP verb for file upload: `POST /tablos/{id}/files` (form-compatible).
- Whether file delete uses inline confirmation (same D-07 pattern from Phase 3) or a modal.
- Signed URL TTL for downloads (ROADMAP suggests "e.g. 5 minutes" — planner's call within that range).
- Whether downloading redirects to the signed URL (302) or opens in a new tab (link with `target="_blank"`).

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Requirements
- `.planning/REQUIREMENTS.md` §Files (FILE-01..06) — The 6 file requirements this phase delivers
- `.planning/REQUIREMENTS.md` §Tasks (TASK-01..07) — Task requirements; Phase 5 must not regress kanban behavior when adding tabs
- `.planning/PROJECT.md` — Core value statement and constraints (HTMX-first, no JS framework, single binary, S3-compatible storage)
- `.planning/ROADMAP.md` §Phase 5 — Success criteria and user-in-loop decisions

### Prior Phase Context (locked decisions that constrain this phase)
- `.planning/phases/04-tasks-kanban/04-CONTEXT.md` — D-10 (kanban is embedded in tablo detail, tabs deferred to Phase 5), D-08 (inline edit pattern), D-03 (hard-delete only)
- `.planning/phases/03-tablos-crud/03-CONTEXT.md` — D-05 (inline create pattern), D-06 (inline edit pattern), D-07 (inline delete confirmation) — reused in Files UI
- `.planning/phases/02-authentication/02-CONTEXT.md` — D-23/D-24 (chi route groups, middleware order), RequireAuth middleware
- `.planning/phases/01-foundation/01-CONTEXT.md` — chi router + templ + sqlc + goose migration conventions

### Codebase Patterns (Go backend)
- `backend/internal/web/router.go` — Router structure, protected group, route ordering convention (static before parametric)
- `backend/internal/web/handlers_tablos.go` — HTMX-aware handler pattern (HX-Request detection, fragment vs full-page response) — reuse verbatim for file handlers
- `backend/internal/web/ui/` — Existing Card, Button, Badge, CSRFField components — reuse in Files tab UI
- `backend/templates/` — Base layout, templ component patterns
- `backend/internal/db/queries/tablos.sql` — sqlc query style reference
- `backend/migrations/0004_tasks.sql` — Migration style reference (most recent); Phase 5 adds `0005_files.sql`
- `backend/internal/files/doc.go` — Placeholder package; Phase 5 implementation lands here
- `backend/cmd/worker/main.go` — Exists but Phase 6 wires it; Phase 5 does not touch worker

### Infrastructure
- `compose.yaml` — Add MinIO service here; follow existing postgres service pattern for naming/health checks

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `ui.Card`, `ui.Button`, `ui.Badge`, `ui.CSRFField` — available in `backend/internal/web/ui/`. Files tab rows and upload form should use these.
- `auth.RequireAuth` middleware — already wraps the tablo route group; file routes nest inside the same group under `/tablos/{id}/files*`.
- `auth.Authed(ctx)` — extracts `*auth.User` from context; file handlers verify `tablo.user_id == authed_user.id` (FILE-06).
- HTMX fragment pattern from Phase 2/3/4 (`HX-Request` header detection, 200+fragment vs 303+redirect) — reuse verbatim for all file mutations.
- Inline delete confirmation pattern (Phase 3 D-07) — reuse for file delete.
- `templates.Layout` — base layout; Files tab fragment extends tablo detail page.

### Established Patterns
- sqlc queries in `backend/internal/db/queries/<domain>.sql`, generated output in `backend/internal/db/sqlc/`. Phase 5 adds `files.sql`.
- Handler constructors return `http.HandlerFunc` with a `FilesDeps` struct (mirrors `TasksDeps` from Phase 4).
- Middleware order locked by D-24: `RequestID → RealIP → SlogLogger → Recoverer → ResolveSession → csrf.Protect → [route groups]`. File routes add to the existing `RequireAuth` group.
- goose migrations numbered sequentially: `0005_files.sql` is next.
- `templ generate` must run after any `.templ` file change.
- Static route segments declared BEFORE parametric in chi (e.g., `/tablos/{id}/files/new` before `/tablos/{id}/files/{file_id}`).

### Integration Points
- `backend/internal/web/router.go` — Add tab sub-routes (`/tablos/{id}`, `/tablos/{id}/tasks`, `/tablos/{id}/files`) and file CRUD routes inside the existing `RequireAuth` group.
- `backend/cmd/web/main.go` — Pass `FilesDeps` (db pool + S3 client) to `NewRouter` alongside existing deps. S3 client constructed from env vars at startup.
- `backend/templates/tablos/detail.templ` — Restructure into tab layout (tab bar + content area). Tasks tab content = existing kanban; Overview tab = existing title/description; Files tab = new.
- `compose.yaml` — Add MinIO service + mc init container for local dev bucket creation.

</code_context>

<specifics>
## Specific Ideas

- S3 key format: `files/{tablo_id}/{uuid}` — e.g., `files/a1b2c3d4-…/f5e6c7b8-….pdf`
- The original filename (from the multipart header) is stored in `tablo_files.filename` for display; it is NOT used as the S3 key (prevents path traversal, collision, and special-character issues).
- MinIO in `compose.yaml` should expose port 9000 (S3 API) and 9001 (console UI) — the mc init container creates the bucket (`xtablo-dev` or similar) and sets it to private.
- `MAX_UPLOAD_SIZE_MB` env var default: 25. `http.MaxBytesReader` wraps the body before any reads begin.
- Signed download URLs: short TTL (5 minutes per ROADMAP suggestion) generated via `s3:GetObject` presigned URL from the Go server on each download request. Client is redirected to the signed URL (302) — no proxying of download bytes.
- Tab sub-routes pattern: `GET /tablos/{id}` renders the full page with Overview tab active; `GET /tablos/{id}/tasks` and `GET /tablos/{id}/files` serve the full page OR a fragment (depending on HX-Request header), consistent with the existing HTMX handler pattern.

</specifics>

<deferred>
## Deferred Ideas

- **Self-hosted MinIO in production** — user asked about this during discussion. For v1, production can point at R2 or any S3-compatible endpoint via env. Hosting MinIO in production on the same VPS as the Go binary is viable (add MinIO to the production Dockerfile or compose stack) but deferred — worth revisiting in the Deploy phase (Phase 7) or v2.
- **File type restriction / allowlist** — deferred to a later phase if security requirements change. Content-type is stored in DB, so adding enforcement later requires only a middleware/validation check, not a schema change.
- **Per-user storage quota** — out of v1 scope per PROJECT.md.
- **File preview / thumbnail generation** — image/PDF previews deferred to v2. Content-type is stored so preview rendering can be added later.
- **Orphan file cleanup job** — files whose tablo is deleted leave orphan S3 objects. Cleanup deferred to Phase 6 (background worker), which will implement a job that reconciles DB rows with S3 objects.

</deferred>

---

*Phase: 5-Files*
*Context gathered: 2026-05-15*
