# New Knowledge Base

## 2026-04-15 — PRD hardening (rate limit, RLS, RFC 8058 unsubscribe)

- **Rate limit:** Tiered `express-rate-limit` on `/api`, `/track`, and stricter windows for send, invite, enrich, CSV import, transactional send.
- **RLS:** `003_rls_and_campaign_failed.sql` enables RLS + `authenticated` policies on core tables; `004_rls_enrichment_tables.sql` covers `002` enrichment tables (apply **after** `002` exists).
- **RFC 8058:** Pair `List-Unsubscribe: <https://...>` with `List-Unsubscribe-Post: List-Unsubscribe=One-Click` and implement `POST /track/unsubscribe/:token` (204 on success).
- **Turnstile:** When `VITE_TURNSTILE_SITE_KEY` is set, login/sign-up pass `captchaToken` to Supabase; enable captcha in GoTrue (`GOTRUE_SECURITY_*`) for production.
## 2026-04-20 — `profiles` table vs Docker init order

- The API expects `public.profiles` (`id`, `email`, `role`, `updated_at`) for JWT-backed routes. Migrations live under `supabase/migrations/` and run as Postgres `docker-entrypoint-initdb.d` scripts **before** GoTrue creates `auth.users`, so `003_profiles.sql` does **not** declare `REFERENCES auth.users(id)` (that FK would fail on a fresh volume). After the stack has run once, operators can optionally add the FK + `ON DELETE CASCADE` for referential cleanup (see comment in `003_profiles.sql`).

## 2026-04-15 — GoTrue email rate limit (`over_email_send_rate_limit`)

- Recovery/signup emails are throttled (often about **one request per 60 seconds** per flow). The login UI maps this code to plain language and enforces a matching client cooldown on **Send reset link** so users do not hammer the API.
- Self-hosted: see `GOTRUE_RATE_LIMIT_EMAIL_SENT` in [Supabase Auth (GoTrue) README](https://github.com/supabase/auth/blob/master/README.md) for hourly email caps on `/recover` and related routes.

## 2026-04-15 — Supabase forgot password

- **Flow:** `supabase.auth.resetPasswordForEmail(email, { redirectTo: origin + '/reset-password' })` sends the recovery mail. The link returns to the SPA with hash params including `type=recovery`.
- **UI:** `/reset-password` must stay **outside** `RedirectIfAuth`; a recovery session counts as signed-in and would otherwise bounce to `/` before the user sets a new password.
- **Detection:** Listen for `onAuthStateChange` event `PASSWORD_RECOVERY` and/or treat `type=recovery` in `location.hash` so the new-password form appears even if the async client races the listener.
- **Complete:** `supabase.auth.updateUser({ password })` then navigate to the app; GoTrue redirect URLs must include the full reset URL per environment.

## 2026-04-15 — AI enrichment & reply mailbox

- **OpenAI:** Campaign enrichment uses `openai` npm package with `response_format: { type: 'json_object' }` so the model returns `enriched_subject` and `enriched_body` (HTML fragment).
- **Concurrency:** `runEnrichmentJob` processes contacts with a pool of 5 parallel workers to balance speed and rate limits.
- **IMAP:** `imapflow` searches `{ seen: false }` with `{ uid: true }`, downloads via `download(uid, undefined, { uid: true })`, then marks `\Seen`. `mailparser.simpleParser` accepts the download stream.
- **Reply threading:** Outbound `Message-ID` is stored on `campaign_enriched_emails.outbound_message_id`; inbound `In-Reply-To` / `References` are matched to route auto-replies and forwards.
- **List-Unsubscribe:** When `reply_unsubscribe_enabled` is true, enriched sends omit the `List-Unsubscribe` header (reply-based opt-out instead).
