Notas de projeto pro Claude Code (e qualquer dev que entre depois). Resume todas as decisões tomadas ao longo das conversas. Linguagem do projeto: **PT-BR**.
---
## 1. Visão geral
App web pro casamento de **Stefanie & Leandro**. Convidados escaneiam um QR Code na mesa, abrem o site, mandam fotos/vídeos + uma mensagem. Os noivos administram tudo num painel `/admin`.
**Escopo**: single-tenant (um casamento). Multi-tenant (SaaS) foi avaliado — fica pra um pivot futuro se houver demanda real (3+ pedidos).
---
## 2. Arquitetura: 3 stacks Docker Compose
Cada stack é um `docker-compose.yml` isolado. Os 3 compartilham uma **network Docker externa** chamada `infra-net`.
└── wedding_photo/ App FastAPI + sidecars de backup
```
**Por que separados**: cada stack tem seu próprio ciclo de vida (`make up-main`, `make up-gitea`, `make up-wedding`). Subir/derrubar uma não afeta as outras. Gitea pode ser desativado sem tocar no app.
**`main/` é o "platform layer"** que os consumidores usam. `gitea/` e `wedding_photo/` conectam ao Postgres e MinIO de lá.
├── packages/shared/ # Zod schemas TS (usado pelo web)
├── infra/backup/ # entrypoint + script do media-backup
└── backups/ # destino dos sidecars (gitignored)
├── postgres/
└── media/
```
---
## 5. Network e roteamento
### Network
`infra-net` é **external**. Criada pelo `network.sh` (chamada por `make network`). Containers de stacks diferentes se enxergam por nome via DNS interno do Docker.
- **Produção**: domínio real (ex.: `lronetto.com`) → Let's Encrypt automático
- Mudar `DOMAIN_BASE` requer **down + up** das 3 stacks (envs lidos no boot)
### Hairpin / `extra_hosts`
`wedding_app` tem `extra_hosts: media.{DOMAIN_BASE}:host-gateway` e `wedding.{DOMAIN_BASE}:host-gateway` pra que, mesmo dentro do container, ele resolva esses hostnames pro Docker host gateway → Caddy. Assim assinaturas S3 fecham (signing host == host que o browser usa pra PUT).
---
## 6. Comandos
### Root `Makefile`
```bash
make help # lista tudo
make up # network + main + gitea + wedding (na ordem)
make down # inverso
make restart # down + up
make status # ps das 3 stacks
make up-main # só infra base
make up-gitea
make up-wedding # rebuilda imagem do app
make rebuild-wedding # build --no-cache + up
make down-{main,gitea,wedding}
make logs-{main,gitea,wedding}
make pull-{main,gitea,wedding}
```
### Wedding `Makefile` (dev local, fora do Docker)
```bash
make install # pnpm install + uv sync
make dev-web # vite na 5173
make dev-api # uvicorn --reload na 3000
make migrate # roda migrations do D1
make build # build do front
make typecheck
make lint
```
---
## 7. Convenções
### Variáveis de ambiente
- **SCREAMING_SNAKE_CASE**
- Cada stack tem seu `.env` (cópia do `.env.example` ao lado)
2. Browser faz **`PUT` direto no MinIO** (não passa pelo backend) com a URL pré-assinada
3. Decisão **single vs multipart**:
-`<= 50 MB`: single PUT
-`> 50 MB`: multipart 10 MB chunks
4.`POST /api/uploads/:id/confirm` → backend faz HEAD pra verificar o objeto, completa multipart se aplicável, **se for HEIC**: transcoda pra JPEG e substitui o storage_key, marca `approved` (ou `pending` se moderation=`pre`)
5. Galeria pública lista só `status=approved`
### HEIC transcoding
- Detecção: `mime_type in {"image/heic", "image/heif"}`
- Em `/confirm` após HEAD: baixa, decoda com pillow-heif, aplica EXIF rotation, salva JPEG quality 88 progressive, escreve com `.jpg`, deleta HEIC
- **Falha não bloqueia o upload**: log + mantém HEIC original (gallery mostra placeholder)
- Razão: browsers (especialmente Android) não renderizam HEIC nativamente
### Multipart upload (cliente)
- Em `apps/web/src/lib/upload.ts`
- Sequential (não paralelo pra MVP) com `XMLHttpRequest` (precisa de progress event)
- ETag de cada chunk via header `etag` na resposta — exige CORS `ExposeHeaders: ["ETag"]` no bucket
### Admin login
-`POST /api/admin/login` body `{email, password}`
- Valida email contra `ALLOWED_ADMIN_EMAILS` (separados por vírgula no env)
- Compara senha com `ADMIN_PASSWORD` via `hmac.compare_digest` (constant-time)
- Issue cookie HttpOnly JWT HS256 com email + exp 7 dias
-`GET /api/admin/*` decora com `Depends(get_admin_email)` que verifica o cookie
### Admin: gestão de uploads
-`GET /api/admin/uploads?status=...&kind=photo|video&q=...&cursor=...` — paginação por timestamp DESC
-`PATCH /api/admin/uploads/:id` — edita `authorName` + `message`
`infra/main/postgres/init/01-create-databases.sh` é executado pelo entrypoint do postgres **apenas quando `PGDATA` está vazio**. Pra "rerodar":
```bash
make down-main
sudo rm -rf infra/main/postgres/data
make up-main
```
Alternativa: criar manualmente via `docker exec -i postgres psql`.
### `docker exec -it` com heredoc
`-t` aloca TTY e conflita com stdin redirecionado. Usar **`-i` só**:
```bash
docker exec -i postgres psql -U postgres <<EOF
CREATE ROLE gitea WITH LOGIN PASSWORD 'xxx';
EOF
```
### Gitea reservou nomes
`admin`, `api`, `user`, `org`, `explore`, `install`, `repo` etc. são reservados. Use outro nome (ex.: `lronetto`, `gitadmin`). O role admin vem do flag `--admin`, não do username.
Usamos a **regular** com `INSTALL_LOCK=true` pra pular o wizard `/install` no primeiro boot.
### `docker exec` por padrão entra como root
Pra Gitea, isso quebra (`Gitea is not supposed to be run as root`). Sempre `docker exec -u git ...` quando for chamar binário do gitea.
### CORS no MinIO
Bucket precisa de `ExposeHeaders: ["ETag"]` pra multipart funcionar (cliente lê ETag de cada PUT). Aplicado pelo `infra/main/minio/init.sh` via `mc cors set`.
### Postgres path-style URLs
`S3_FORCE_PATH_STYLE=true` é necessário pra MinIO. Sem isso, boto3 monta URL virtual-hosted (`bucket.endpoint/key`) que MinIO single-instance não suporta.
### Mudança em `.env` exige restart
Compose só lê env vars no boot do container. `make down-X && make up-X` após editar.
### TLS local em `*.localhost`
Caddy emite cert auto-assinado pelo root local. Browser pede pra aceitar (1x por subdomínio). Pra evitar prompts:
Pra produção, registros A pros subdomínios (`wedding`, `gitea`, `media`, `pgadmin`, `minio`) precisam estar propagados antes do Caddy tentar emitir cert. Senão ele entra em backoff e demora.
### `ACME_EMAIL` obrigatório em prod
Let's Encrypt requer email pra contato de renovação. Em dev pode ficar vazio (Caddy usa internal CA).
### Hairpin DNS no `wedding_app`
O `extra_hosts: host-gateway` pra `media.{DOMAIN_BASE}` faz o container resolver o subdomínio pro host. Sem isso, requests server-side (HEAD/DELETE/multipart complete) vão pra IP público → roteador → host → Caddy (lentidão). Com host-gateway: container → host → Caddy (rápido).
---
## 10. Histórico de iterações (sem detalhe — referência rápida)
1.**MVP Cloudflare**: Workers + D1 + R2 + Pages + Access. Funcionou mas Access não rola em `*.workers.dev`.
2.**Migração 1**: full Docker (Node/Hono + Postgres + MinIO + Caddy). Single compose, deploy num VPS.
3.**Migração 2**: backend reescrito em Python (FastAPI + SQLAlchemy + boto3 + pyjwt). Mesmo contrato de API. Frontend não mexeu.
4.**HEIC + backup**: pillow-heif no `/confirm`, 2 sidecars de backup (Postgres + MinIO mirror).
5.**Reorganização em 3 stacks**: `infra/main` + `infra/gitea` + `infra/wedding_photo` compartilhando `infra-net`. Caddy concentrado em `main/`.