Proxy pra Open Food Facts com cache real (Redis) e rate-limit. Fica fora do repo do
app (app.macro-tracker) porque não é bundlado pelo Metro. O app hoje não
está conectado a esse proxy — FoodApiClient (lib/food-api.ts no app) já está
isolado atrás de uma interface, então a troca é só implementar um client que
aponte pra cá, quando/se isso for decidido.
cp .env.example .env
docker compose up -d # sobe Redis em :6379
npm install
npm run dev # servidor em :3000 (ou $PORT)GET /search?q=<query>&page=<n>— busca produtos, cache 1h.GET /barcode/:code— busca por código de barras, cache 24h (misses cacheiam por 60s).GET /health— healthcheck.POST /auth/google—{ idToken }(ID token obtido no client via Google Sign-In) → valida contra o Google, cria/atualiza usuário no SQLite, retorna{ token, userId, email }(JWT próprio, 30 dias).GET /sync(autenticado,Authorization: Bearer <token>) — devolve o estado mesclado completo:{ goals, settings, dayLogs, favorites, savedMeals }.POST /sync(autenticado) — changeset parcial no mesmo formato (só as entidades que mudaram). Merge é por registro, não no blob inteiro — ver seção abaixo.
- Upstream throttle (
src/off-client.ts): serializa requests pra OFF com gap mínimo de 350ms + retry com backoff em 503/429 — protege a OFF de todos os clientes do proxy combinados. - Rate-limit de entrada (
src/index.ts,express-rate-limit): 60 req/min por IP — protege o proxy em si de abuso.
Login via Google (ID token verificado no servidor com google-auth-library,
audience = GOOGLE_CLIENT_ID). Dados ficam em SQLite (better-sqlite3, arquivo
em SQLITE_PATH):
goalsesettings— singletons por usuário, last-write-wins no registro inteiro.day_logs(chavedate),favorites(chavefood_id),saved_meals(chavemeal_id) — coleções, last-write-wins por item. Editar o dia de hoje em dois devices offline não derruba os outros dias já sincronizados.
Exclusão usa tombstone (deletedAt no registro, não DELETE de linha) — assim
outros devices aprendem da exclusão em vez de ressuscitar uma cópia local antiga.
src/sync-merge.ts tem a decisão pura de merge (resolveWinner, testável sem
DB); src/sync-store.ts é a camada SQLite que aplica isso por registro.
O app RN ainda não chama /auth/google nem /sync — esse wiring (telas de
login, chamada de sync no macro-context.tsx, e adaptar o reducer local pra
trackear updatedAt por dia/favorito/meal) é uma tarefa separada.
- Auth/API key na busca de alimento — só espelha dados públicos da OFF, sem dado sensível.
- Apple Sign In — só Google por agora; Apple entra se/quando publicar na App Store.
- Deploy/hosting — só ambiente de dev local (docker compose + SQLite) por agora.