# CLAUDE.md 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 repositórios / 3 stacks Docker Compose Cada stack é um repositório git separado, com seu próprio `docker-compose.yml` isolado. Os 3 compartilham uma **network Docker externa** chamada `infra-net`. ``` lronetto-main/ Postgres + Redis + MinIO + pgAdmin + Caddy (+ orquestrador) lronetto-gitea/ Gitea + Actions runner lronetto-wedding/ App FastAPI + sidecars de backup (+ deploy CI) ``` No disco os 3 ficam como **diretórios irmãos** (ex.: `C:\Users\lrone\code\lronetto-*`). O `Makefile` orquestrador (neste repo, `lronetto-main`) referencia os outros dois por caminho relativo (`../lronetto-gitea`, `../lronetto-wedding`) — sobrescrevível via `GITEA_DIR=` / `WEDDING_DIR=`. **Por que repos separados**: cada stack tem ciclo de vida, histórico e CI próprios. Subir/derrubar ou versionar uma não afeta as outras. Gitea pode ser desativado sem tocar no app. **`lronetto-main` é o "platform layer"** que os consumidores usam. `gitea` e `wedding` conectam ao Postgres e MinIO de lá pela `infra-net`. > Histórico: nasceu como monorepo `wedding-app` (branch `main`) com tudo em `infra/{main,gitea,wedding_photo}/`. Foi dividido em 3 repos (fresh start, sem histórico herdado — o monorepo original fica arquivado como backup). ### `lronetto-main` é infra base compartilhada (não só do casamento) `lronetto-main` não é infra dedicada ao casamento — é a **infra base** (Postgres + Redis + MinIO + pgAdmin + Caddy + `infra-net`) que **outros serviços independentes** também consomem, além de `gitea` e `wedding`. Todos rodam no mesmo VPS, todos hospedados no mesmo Gitea self-hosted sob o prefixo `lronetto-`: | Repo | URL | Escopo | Orquestrado por este `Makefile`? | |---|---|---|---| | `lronetto-main` | `https://gitea.lronetto.com/lronetto/lronetto-main.git` | Este repo — infra base (platform layer) + orquestrador | — | | `lronetto-gitea` | `https://gitea.lronetto.com/lronetto/lronetto-gitea.git` | Git hosting + Actions, hospeda todos os repos `lronetto-*` (dogfooding) | Sim (`up-gitea`) | | `lronetto-wedding` | `https://gitea.lronetto.com/lronetto/lronetto-wedding.git` | App do casamento (é o que este `CLAUDE.md` documenta em detalhe) | Sim (`up-wedding`) | | `lronetto-finance` | `https://gitea.lronetto.com/lronetto/lronetto-finance.git` | Open Finance (Pluggy) — outro projeto, não relacionado ao casamento | Sim (`up-finance`) | | `lronetto-claudeweb` | `https://gitea.lronetto.com/lronetto/lronetto-claudeweb.git` | Interface web pro Claude Code (Agent SDK), single-user — outro projeto, não relacionado ao casamento | Sim (`up-claudeweb`) | `lronetto-finance` e `lronetto-claudeweb` **já estão provisionados e rodando em produção**, consumindo Postgres/Redis/MinIO/Caddy deste repo pela `infra-net` — assim como wedding e gitea. Cada um segue o mesmo padrão de "app self-contained, sem Postgres/Redis/Caddy próprios" já usado pela wedding: repo com `docker-compose.yml` (ou `deploy/compose.yml`) próprio, `infra-net` declarada como `external`, containers conectados por nome (`postgres`, `redis`, `minio`), sem porta pública (exposição só via Caddy do main). **O que existe hoje neste repo pra suportar os dois:** - `postgres/init/01-create-databases.sh`: cria roles/databases `finance` e `claudeweb` (além de `wedding`/`gitea`), a partir de `FINANCE_DB_*`/`CLAUDEWEB_DB_*` no `.env` - `docker-compose.yml` (serviço `postgres`): repassa `FINANCE_DB_*`/`CLAUDEWEB_DB_*` pro init script - `minio/init.sh`: cria também o bucket `claudeweb-artifacts` (privado, sem download anônimo — usado só se `lronetto-claudeweb` tiver `S3_ENDPOINT` preenchido, pra artefatos de sessão M3/RF-14); `lronetto-finance` não usa MinIO - `caddy/Caddyfile`: roteia `finance.{DOMAIN_BASE}` → `finance_app:8000` e **`code.{DOMAIN_BASE}`** (não `claudeweb.{DOMAIN_BASE}`!) → `claudeweb-api:8300` (rotas `/api/*`, `/healthz`, `/ws/*`) ou `claudeweb-web:80` (resto, SPA estática) - `Makefile`: targets `up-finance`/`down-finance`/`logs-finance`/`pull-finance`/`rebuild-finance` e os equivalentes `-claudeweb`, seguindo o padrão de `WEDDING_DIR=`/`GITEA_DIR=` (agora também `FINANCE_DIR=` / `CLAUDEWEB_DIR=`, default `../lronetto-finance` / `../lronetto-claudeweb`) **Diferença importante**: `make up`/`down`/`restart`/`status` continuam cobrindo só o **core** (main + gitea + wedding — os 3 repos que este documento detalha). `finance` e `claudeweb` têm targets avulsos (`make up-finance`, `make up-claudeweb`, etc.) porque têm ciclo de vida e deploy CI próprios, independentes deste projeto — não entram no `up`/`down` agregado de propósito. `make status` já lista os dois (com `|| true`, então não quebra se não estiverem clonados/de pé). **Gotcha extra do `lronetto-claudeweb`**: ele cria e gerencia por fora uma segunda network Docker, `claudeweb-workers` (não é a `infra-net`), e **conecta manualmente os containers `redis`, `gitea` e (opcionalmente) `minio` deste repo a ela** — é assim que os containers efêmeros de worker (Agent SDK) do claudeweb falam com Redis/Gitea/MinIO sem entrar na `infra-net` (isolamento de egress). Isso significa que `redis`/`gitea`/`minio` ficam com **duas networks anexadas** por fora do `docker-compose.yml` deste repo — um `docker network disconnect` acidental, um recreate de container, ou uma limpeza de networks (`docker network prune`) pode quebrar essa conexão sem avisar aqui. Ver `README.md` do `lronetto-claudeweb` pra recriar (`docker network connect claudeweb-workers `). > Nota sobre backup: o `scripts/vps-backup.sh` (seção 8) já cobre `finance`/`claudeweb` de graça — o `pg_dumpall` do passo 1 dumpa **todos** os bancos da instância, e o script agora também copia o `.env` de `lronetto-finance`/`lronetto-claudeweb` (via `FINANCE_DIR=`/`CLAUDEWEB_DIR=`) se estiverem clonados como irmãos. --- ## 3. Stack técnica | Camada | Escolha | Por quê | |---|---|---| | HTTP framework | **FastAPI 0.115** | Async-first, OpenAPI auto, Pydantic v2 | | ASGI server | **uvicorn** | Padrão de fato | | ORM | **SQLAlchemy 2.0 async + asyncpg** | Tipos modernos, drivers async maduros | | Validação | **Pydantic v2** | Built-in no FastAPI; camelCase no wire | | Env loader | **pydantic-settings** | Falha rápido em var faltando | | Storage SDK | **boto3** wrapped em `asyncio.to_thread` | S3 compatível, lida com MinIO + R2 + B2 idem | | Auth | **pyjwt** + cookie HttpOnly | 7 dias, HS256, constant-time compare | | QR codes | **qrcode** + **reportlab** | PNG/SVG/PDF A6 imprimível | | HEIC | **pillow-heif** | Decode iPhone HEIC → JPEG no `/confirm` | | Frontend | **Vite + React 18 + Tailwind + react-router** | Stack comum, rápida | | DB | **Postgres 16** | Multi-banco em uma instância (wedding + gitea) | | Cache | **Redis 7** | Gitea usa hoje (cache + sessões); wedding pode usar depois | | Storage | **MinIO** | S3 compat self-hosted; código portável pra R2/B2 | | Reverse proxy | **Caddy 2** | TLS auto (Let's Encrypt em prod, interno em `*.localhost` dev) | | Git hosting | **Gitea 1.22** + Actions runner | Self-hosted, leve, compatível com GitHub Actions | | Backup | `prodrigestivill/postgres-backup-local` + `alpine + mc` | Sidecars com cron, rotação dias/semanas/meses | | Package mgmt | **uv** (Python), **pnpm** (Node) | Mais rápidos que pip/npm | --- ## 4. Estrutura de diretórios completa Três repos irmãos no mesmo diretório pai: ``` lronetto-main/ # PLATFORM LAYER + ORQUESTRADOR (este repo) ├── .gitignore ├── Makefile # Orquestra as 3 stacks (refs ../lronetto-{gitea,wedding}) ├── network.sh # Cria infra-net (idempotente) ├── CLAUDE.md # Este arquivo ├── docker-compose.yml ├── .env.example ├── caddy/Caddyfile # hostname-based routing ├── postgres/init/01-create-databases.sh # cria DBs wedding + gitea ├── minio/{init.sh, cors.json} # cria bucket wedding-media ├── pgadmin/servers.json # postgres pré-conectado ├── scripts/vps-backup.sh # backup completo do host (make backup-vps) └── backups/vps/ # destino do vps-backup.sh (gitignored) lronetto-gitea/ # GIT + CI ├── .gitignore ├── docker-compose.yml ├── .env.example └── runner/Dockerfile # act_runner + docker-cli lronetto-wedding/ # APLICAÇÃO ├── .gitignore ├── docker-compose.yml # app + postgres-backup + media-backup ├── .env.example ├── Dockerfile # multi-stage Node(build web) + Python(runtime) ├── Makefile # dev local (uv, pnpm) + ops da stack + deploy ├── .gitea/workflows/deploy.yml # CI: push na main -> SSH no host -> make deploy ├── pyproject.toml, package.json, pnpm-workspace.yaml, ... ├── apps/ │ ├── api/ # FastAPI Python │ │ ├── pyproject.toml │ │ └── app/ │ │ ├── main.py # FastAPI app, lifespan, SPA fallback │ │ ├── config.py # pydantic-settings │ │ ├── db/{base,models,migrate}.py │ │ ├── migrations/ # SQL puro, runner idempotente │ │ ├── lib/ # auth, storage, qrcode, pdf, ids, transcode │ │ ├── schemas/api.py # Pydantic v2 wire models │ │ └── routes/{public,uploads,admin}.py │ └── web/ # Vite + React + Tailwind SPA │ ├── index.html │ ├── vite.config.ts │ ├── tailwind.config.ts │ └── src/ │ ├── main.tsx, App.tsx │ ├── routes/{Home,Upload,Gallery}.tsx + admin/{Login,Dashboard}.tsx │ └── lib/{api,upload}.ts ├── 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. ### Container names fixos - `postgres`, `redis`, `minio`, `pgadmin`, `caddy` (stack main) - `gitea`, `gitea_runner` (stack gitea) - `wedding_app`, `wedding_pg_backup`, `wedding_media_backup` (stack wedding_photo) - `finance_app` (stack `lronetto-finance`, repo à parte) - `claudeweb-api`, `claudeweb-web` (stack `lronetto-claudeweb`, repo à parte) ### URLs (com `DOMAIN_BASE`) | Hostname | Serve | |---|---| | `https://wedding.{DOMAIN_BASE}` | Site dos noivos (uploads + galeria + admin) | | `https://gitea.{DOMAIN_BASE}` | Git hosting + Actions | | `https://pgadmin.{DOMAIN_BASE}` | Web UI dos bancos | | `https://minio.{DOMAIN_BASE}` | Console admin do MinIO | | `https://media.{DOMAIN_BASE}` | S3 API pública do MinIO (uploads/downloads) | | `ssh://git@gitea.{DOMAIN_BASE}:2222` | Git via SSH | | `https://finance.{DOMAIN_BASE}` | `lronetto-finance` (Open Finance/Pluggy) — repo à parte, não relacionado ao casamento | | `https://code.{DOMAIN_BASE}` | `lronetto-claudeweb` (interface web pro Claude Code) — repo à parte, subdomínio é `code`, não `claudeweb` | ### `DOMAIN_BASE` - **Dev local**: `localhost` → Caddy emite cert interno automático pra `*.localhost` - **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 ### Orquestrador (`lronetto-main/Makefile`) `make up`/`down`/`restart`/`status` cobrem só o **core do casamento** (main + gitea + wedding — os 3 repos que este documento detalha). Precisa que `lronetto-gitea` e `lronetto-wedding` estejam como diretórios irmãos (ou ajuste `GITEA_DIR=` / `WEDDING_DIR=`). `finance` e `claudeweb` (seção 2) têm targets avulsos (`up-finance`, `up-claudeweb`, etc.) — de propósito **não** entram no `up`/`down` agregado, porque têm ciclo de vida e deploy CI próprios. Ajuste via `FINANCE_DIR=` / `CLAUDEWEB_DIR=` se não estiverem como diretórios irmãos. ```bash make help # lista tudo # ----- core (main + gitea + wedding) ----- make up # network + main + gitea + wedding (na ordem) make down # inverso make restart # down + up make status # ps do core + finance/claudeweb (se de pé) make up-main # só infra base (este repo) 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} # ----- outros consumidores da infra base (targets avulsos) ----- make up-finance # build + up do app finance make up-claudeweb # build da imagem do worker + up do claudeweb (api+web) make rebuild-{finance,claudeweb} make down-{finance,claudeweb} make logs-{finance,claudeweb} make pull-{finance,claudeweb} make backup-vps # roda scripts/vps-backup.sh (backup completo do host, # inclui finance/claudeweb se clonados como irmãos) ``` ### App wedding (`lronetto-wedding/Makefile`) Repo self-contained — roda standalone, sem o orquestrador. ```bash # Dev local (fora do Docker) make install # pnpm install + uv sync make dev-web # vite na 5173 make dev-api # uvicorn --reload na 3000 make migrate # roda migrations make build # build do front make typecheck / lint # Stack Docker (requer infra-net + platform layer de pé) make up # up -d --build make down / rebuild / logs / ps make deploy # git reset --hard + up (chamado pelo CI) ``` --- ## 7. Convenções ### Variáveis de ambiente - **SCREAMING_SNAKE_CASE** - Cada stack tem seu `.env` (cópia do `.env.example` ao lado) - Variáveis compartilhadas entre stacks (`DOMAIN_BASE`, `MINIO_ROOT_*`, `WEDDING_DB_*`) precisam casar manualmente - Senhas defaults nos `.env.example` são placeholders `troque-essa-senha-forte` — sempre trocar - `SESSION_SECRET` mínimo 16 chars (sugestão: UUID + sufixo) ### IDs - Nanoid 16 chars + prefixo: `up_xxxx...` (upload), `au_xxxx...` (audit) - Implementação em `apps/api/app/lib/ids.py` ### API wire format - **camelCase** (`coupleNames`, `createdAt`, `maxFileMb`, etc.) - Mesmo schema entre Python (Pydantic) e TS (Zod) — campos batem nome-a-nome - Erros: `{"error": "code", "details": ...}` com HTTP status apropriado ### Timestamps - `bigint` em ms desde epoch (não `timestamptz`) - Razões: lida fácil com JS `Date.now()`, sort sem timezone, não precisa de cast - Default no DB: `(EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT` ### Migrations - SQL puro em `apps/api/app/migrations/NNNN_descricao.sql` - Runner em `app/db/migrate.py`: idempotente, valida SHA256 (impede editar migration aplicada) - Roda automaticamente no boot do `wedding_app` (`AUTO_MIGRATE=true`) - Schema sempre em **Postgres puro** (não SQLite syntax) ### Storage keys - Formato: `uploads/{YYYY}/{MM}/{id}.{ext}` - Ex.: `uploads/2026/06/up_abc123def456.jpg` - Extensão vem do filename, fallback no MIME type --- ## 8. Fluxos importantes ### Upload (convidado) 1. `POST /api/uploads/init` → backend valida (tamanho, vídeo permitido, duração), cria row `pending`, retorna URL pré-assinada 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` - `POST /api/admin/uploads/:id/{approve,reject,cover}` — actions individuais - `DELETE /api/admin/uploads/:id` — apaga do banco + storage (incl. thumbnail) - `POST /api/admin/uploads/bulk` — `{action: approve|reject|delete, ids: [...]}` até 200 IDs - `DELETE /api/admin/event/cover` — remove a foto de capa ### Backup **Nível stack (`lronetto-wedding`, sidecars sempre ativos)** - **Postgres**: `prodrigestivill/postgres-backup-local` daily, retenção dias/semanas/meses, escreve em `./backups/postgres/` (bind mount do host) - **MinIO**: alpine + mc cron-driven, `mc mirror` (incremental) pra `./backups/media/` - **Backup remoto opcional**: configurar `BACKUP_REMOTE_*` no `.env` da wedding → espelha pra outro endpoint S3 (R2/B2/etc.) **Nível host (`lronetto-main/scripts/vps-backup.sh`, `make backup-vps`)** - Script único que cobre o que os sidecars da wedding **não** cobrem: dump lógico completo do Postgres (`pg_dumpall`, todos os bancos + roles, não só o da wedding), snapshot RDB do Redis, tar do diretório de dados do Gitea (repos + config), `.env` das 3 stacks e os certs do Caddy (evita reemissão no Let's Encrypt após um restore) - Empacota tudo num único `.tar.gz` com permissão `600` (tem segredo dentro — os `.env`) em `backups/vps/`, com rotação por `BACKUP_RETENTION_DAYS` (default 14 dias) - Envio offsite opcional via `rclone` se `BACKUP_REMOTE` estiver setado (ex.: `r2:meu-bucket/vps-backup`) - Roda **no host**, fora de containers (usa `docker exec`/`docker cp` pra falar com `postgres`/`redis`, e lê os diretórios dos repos irmãos via `GITEA_DIR=`/`WEDDING_DIR=`, mesma convenção do Makefile orquestrador) - Pensado pra cron diário (fora de horário de pico): ```cron 0 3 * * * cd /opt/lronetto-main && ./scripts/vps-backup.sh >> backups/vps/cron.log 2>&1 ``` - Restore é manual (não tem script de restore automático — cada peça volta do seu jeito): `gunzip -c postgres-all.sql.gz | docker exec -i postgres psql -U postgres`, `tar -xzf gitea-data.tar.gz -C ../lronetto-gitea/`, etc. ### Gitea bootstrap (1ª vez) 1. `make up-main` (precisa estar de pé pro postgres + redis) 2. `make up-gitea` (runner falha porque ainda não tem token, ok) 3. Cria user admin via CLI (nome **NÃO pode ser `admin`** — é reservado): ```bash read -s PW docker exec -u git -it gitea gitea admin user create \ --username lronetto --password "$PW" --email lronetto@gmail.com --admin ``` 4. Abre `https://gitea.{DOMAIN_BASE}` → loga 5. Avatar → **Site Administration** → **Actions** → **Runners** → **Create new Runner** → copia token 6. Cola no `.env` do repo `lronetto-gitea`: `GITEA_RUNNER_TOKEN=` 7. `make up-gitea` (runner agora se registra) ### Trocar senha do Gitea ```bash read -s NEWPW docker exec -u git -it gitea gitea admin user change-password \ --username lronetto --password "$NEWPW" ``` ### Deploy automático (CI/CD) Vive no repo **`lronetto-wedding`** (`.gitea/workflows/deploy.yml`). A cada push na `main` — ou disparo manual (`workflow_dispatch`) — o runner conecta por **SSH no host** e roda `make deploy` (git reset --hard + `up` com build no repo wedding), seguido de health check em `/api/health`. **Por que SSH e não docker direto no runner**: o `.env` (segredos do app) fica só no host, fora do git. O host já tem o repo clonado; o deploy só atualiza o código e sobe. Setup (1ª vez): 1. No host, clone `lronetto-wedding` (ex.: `/opt/lronetto-wedding`) com um usuário SSH que rode docker e tenha acesso ao repo. (Stack `main` precisa estar de pé.) 2. Gere um par de chaves SSH e adicione a **pública** no `~/.ssh/authorized_keys` desse usuário. 3. No repo `lronetto-wedding` no Gitea → **Settings → Actions → Secrets**, crie: - `DEPLOY_HOST` — IP/hostname do host - `DEPLOY_USER` — usuário SSH - `DEPLOY_SSH_KEY` — a chave **privada** (conteúdo completo) - `DEPLOY_PATH` — caminho do repo no host (ex.: `/opt/lronetto-wedding`) 4. (Opcional) **Variables**: `DEPLOY_PORT` se o SSH não for 22. `make deploy` (no repo wedding) aceita `BRANCH=` pra sobrescrever a branch. --- ## 9. Gotchas conhecidos ### Postgres init script só roda na 1ª vez `postgres/init/01-create-databases.sh` (neste repo, `lronetto-main`) é executado pelo entrypoint do postgres **apenas quando `PGDATA` está vazio**. Pra "rerodar": ```bash make down-main sudo rm -rf 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 < /tmp/caddy-root.crt # Importa no trust store do OS/browser ``` ### Configurar firewall em produção - VPS firewall (ufw/iptables): liberar 80 e 443 - **Cloud firewall** (security group AWS/DO/Vultr): também liberar 80 e 443 — esse é separado e quase sempre é o esquecido - Caddy ACME challenge precisa de **80 acessível externamente**, senão Let's Encrypt falha ### DNS precisa apontar antes do `up` 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,gitea,wedding_photo}` no monorepo `wedding-app`, compartilhando `infra-net`. Caddy concentrado em `main/`. 6. **Deploy CI + rename main**: workflow Gitea Actions de deploy via SSH; branch renomeada pra `main`. 7. **Split em 3 repos**: `lronetto-main` + `lronetto-gitea` + `lronetto-wedding` (fresh start). Orquestrador no main, deploy CI no wedding. Monorepo `wedding-app` arquivado. Repos / branches: - `lronetto-main`, `lronetto-gitea`, `lronetto-wedding` (atuais; default branch `main`; hospedados no Gitea self-hosted da própria stack, prefixo `lronetto-*`) - Arquivados no monorepo `wedding-app`: `claude/wedding-qrcode-photos-C0PQt` (Cloudflare original), `claude/docker-vps-migration` (1ª migração Docker Node) --- ## 11. Onde olhar pra estender | Quero adicionar... | Olha em | |---|---| | Nova rota pública | `apps/api/app/routes/public.py` | | Nova rota admin | `apps/api/app/routes/admin.py` | | Novo campo no upload | `apps/api/app/db/models.py` + nova migration + `apps/api/app/schemas/api.py` + atualizar `routes/uploads.py` | | Novo bucket no MinIO | `lronetto-main`: `minio/init.sh` + variável no `.env.example` | | Outro DB no postgres | `lronetto-main`: `postgres/init/01-create-databases.sh` + role nova | | Novo subdomínio Caddy | `lronetto-main`: `caddy/Caddyfile` + container_name correspondente | | Nova app na rede | novo repo com `docker-compose.yml`, declarar `infra-net` como external, conectar a `postgres`/`redis`/`minio` por nome, e referenciar no Makefile orquestrador. Exemplos reais já rodando assim: `lronetto-finance` e `lronetto-claudeweb` (seção 2) | | Nova tela no front | `apps/web/src/routes/` + rota em `App.tsx` | | Schema compartilhado front-back | duplica: Zod em `packages/shared/src/schemas.ts` (TS) + Pydantic em `apps/api/app/schemas/api.py` (Python). **Camelo nos dois** | | Workflow CI no Gitea | `.gitea/workflows/*.yml` no repo alvo (sintaxe GitHub Actions) — runner já registrado. Ex.: deploy do app em `lronetto-wedding` | --- ## 12. Decisões deferidas (a fazer se necessário) - **Thumbnails server-side**: galeria carrega imagens full-size. Pra otimizar: `sharp` no upload `/confirm` (ou um job async), salvar `thumbnail_key`. ~2h. - **Export ZIP do admin**: streamed ZIP de todos os uploads. ~1h. - **Rate limit em `/uploads/init`**: hoje sem limite. Vale colocar Redis-based se houver suspeita de abuso. ~1h. - **Email aos noivos quando upload chegar**: Resend ou SMTP. ~2h. - **Slideshow pra projetar na recepção**: tela `/slideshow` com auto-advance. ~30 min. - **Custom domain do bucket público** (em vez de `media.X`): mais "branded". DNS + CNAME pro MinIO. ~15 min. - **Pivot multi-tenant SaaS**: ver seção 1 — 2-4 semanas se valer a pena.