# AGENT.md — AnwarPi for AI agents Orientation doc for AI agents (coding assistants, autonomous maintainers, content pipelines) working on this repository. Read this before changing anything. ## What this is **AnwarPi (أنور بي آي)** — an Arabic-first, RTL platform for learning practical AI at **https://anwarpy.ai**. Free guides for everyone, VIP-gated deep-dives, a searchable library/catalog, moderated comments, member accounts, and a Stripe subscription that is fully wired but public-facing "coming soon" until keys are set. Content is also published programmatically by an external agent pipeline through a key-authenticated API. Canonical domain: `anwarpy.ai` (`anwarpi.ai` is legacy). Owner: Ahmed (ahmedalkhair.sa@gmail.com). Owner constraint: **never change site flow/UX without explicit approval** (e.g. no CAPTCHA, no aggressive limits). ## Stack | Component | Choice | |---|---| | Framework | Next.js 15 (App Router, `output: 'standalone'`), React 19 | | CMS/Auth | Payload 3.90 (`@payloadcms/next`), Lexical editor | | Database | PostgreSQL 16 via `@payloadcms/db-postgres` | | Styling | Plain CSS files per page (see `web/src/app/(frontend)/styles/`), dark-first brand (see `brand-spec.md`) | | Email | Cloudflare Email Routing (inbound) + Cloudflare Email Sending REST (outbound) — **not SMTP** (docs/adr/0003) | | Payments | Stripe SDK, webhook at `/api/stripe/webhook` | | Edge | Traefik 2/3 with Docker provider (labels in `web/docker-compose.vps.yml`) | ## Repo layout ``` web/ ← the entire application (deploy unit) src/ payload.config.ts ← Payload config: collections, postgres adapter seed.ts ← `npm run seed` middleware.ts ← Next middleware: auth rate limits (F2) collections/ ← Users, Posts, Media, Files, PostLikes, Comments migrations/ ← Payload migrations (`payload migrate` runs on boot) lib/ ← viewer.ts (session helper), mail.ts, format.ts, topics.ts, agent-* app/ (frontend)/ ← public Arabic site (page.tsx per route, styles/ css) (payload)/ ← Payload admin + API catch-all api/ ← custom REST routes (posts, auth, subscribe, stripe, agent, files) public/ ← brand/, covers/, og-default.png Dockerfile ← standalone build; CMD runs `npx payload migrate` first docker-compose.vps.yml ← prod: web + postgres, Traefik labels (rate limits) GLOSSARY.md ← canonical domain vocabulary — USE THESE TERMS docs/adr/ ← architecture decision records — do not contradict brand-spec.md ← visual identity: dark blue-black, silver beams, cyan-teal accent ``` ## Data model (Payload collections) - **Users** — auth collection. `role: 'free' | 'vip' | 'admin'` (server-forced), `emailVerified`, `onboarded`, `termsVersion`. Login lockout: 5 attempts / 15 min; admin accounts never lock (hook clears lock before login). - **Posts** — guides. `access: 'free' | 'vip'`, `slug`, category (وكلاء، نماذج، استرجاع، سلامة، أدوات، أعمال), cover image (1200×630), Lexical body, likes/views. - **Comments** — moderated: `pending → approved | rejected`. Only `approved` is public. Authors cannot edit/delete their own comments (moderated model, docs/adr/0002). Display name is a snapshot of `fullName` at submit time. - **Media / Files** — uploads (files served via `/api/files/[id]`). - **PostLikes** — one like per member per post. ## Trust model (critical — do not break) 1. **Roles are server-forced.** `POST /api/users` and any client write cannot set `role`, `emailVerified`, `stripeCustomerId`, or `verificationToken` — the `beforeChange` hook in `Users.ts` deletes them for untrusted operations. Only `req.user.role === 'admin'` or `context.systemActivation === true` (payment activation from server code) counts as trusted. 2. **Entitlement is derived, never stored on the user**: viewer may read a post's body iff `post.access === 'free' || role in ('vip','admin')`. Gating happens server-side (locked body is stripped from API responses). 3. **Comments require verification** (`emailVerified`), admins exempt. 4. Client requests can never mint VIP (free `/payment-success` visits redirect to `/subscription`; docs/adr/0004, 0005). ## Auth & rate limiting - Passwords: Payload local auth → **PBKDF2-SHA256, 600 000 iterations, 32 bytes** (NOT bcrypt — verified in payload's `generatePasswordSaltHash`). - `web/src/middleware.ts` (F2): per-IP in-memory buckets — `POST /api/users` 5/h, `POST /api/users/login` 10/15min, `POST /api/users/forgot-password` 5/h. Over limit → 429 JSON `{"error":"محاولات كثيرة جدًا. حاول بعد قليل."}` + `Retry-After`. **In-memory is correct for the single-container deployment**; moving to horizontal scaling requires a shared store (Redis) — do not silently assume one replica. - Timing equalization (F7): `Users.ts` `beforeOperation` login hook runs a dummy PBKDF2 (same params, salt `anwarpy-dummy-salt-v1`) when no user matches, so ghost emails are not enumerable by response time. Keep this when touching the hook. - Traefik edge (F1, VPS only): pages `pagelimit` 60/min burst 120 + `connlimit` 50 in-flight per IP; API `apilimit` 90/min burst 180. - `Strict-Transport-Security: max-age=63072000; includeSubDomains` on all routes (`web/next.config.mjs`). Do not add `preload`. ## Key routes Frontend: `/` (home), `/articles`, `/catalog`, `/posts/[slug]` (gated body, engagement, comments), `/signup`, `/login`, `/forgot-password`, `/verify-email`, `/subscription`, `/payment-success`, `/payment-cancelled`, `/vip-onboarding`, `/onboarding`, `/account-settings`, `/about`, `/privacy`, `/terms`. Admin UI: `/admin`. Custom API (see `web/src/app/api/`): `posts` (list) + `posts/[slug]` (detail — strips locked bodies), `posts/[slug]/{like,view,comments}`, `auth/{verify-email,resend-verification}`, `users` REST is Payload's own, `subscribe`, `stripe/webhook` (returns 503 until `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET` exist; verifies `stripe-signature` once configured), `agent/posts` (POST/PUT with `x-agent-key` header — external publishing), and `files/[id]`. ## Environment variables `DATABASE_URI`, `PAYLOAD_SECRET`, `NEXT_PUBLIC_SITE_URL`, `AGENT_API_KEY`, `MAIL_FROM`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, plus optional `STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET`. Prod values live in `/root/anwarpi/.env.production` on the VPS — never commit `.env*`. ## Development & verification ```bash cd web && npm install npm run dev # localhost:3000, /admin for CMS npm run build # MUST exit 0 — typescript.ignoreBuildErrors is false npm run seed ``` - Regenerates `payload-types.ts` automatically on build (`payload generate:types`). - Schema changes require a migration (`payload migrate:create`); the container CMD runs `payload migrate` on every deploy — no manual DB steps. - Auth endpoints are rate-limited in dev too; restart the dev server to reset buckets when testing. ## Deployment Single VPS `147.79.102.201`, app at `/root/anwarpi`, managed by `docker compose -f docker-compose.vps.yml` (web + postgres + Traefik proxy network). Update flow: rsync `web/` → `/root/anwarpi/` excluding `node_modules .next .git media files-private '.env*' '*.tsbuildinfo'`, then `docker compose -f docker-compose.vps.yml up -d --build` (label changes force recreate; migration runs on boot). Traefik labels only affect the VPS. ## House rules - Follow the **reuse ladder**: existing helper → stdlib → platform feature → installed dependency → then write the complete version. - **Do not add UX changes** (new limits, captchas, gates) without owner approval; thresholds are deliberately generous for humans. - New decisions → add an ADR in `docs/adr/` (next number, `NNNN-slug.md`). - Use GLOSSARY terms exactly (عضوية/entitlement, مفتوح/unlocked, موثّق/ verified, كتابة موثوقة/trusted write). - **Pentest context**: the 2026-10-09 report items F1–F9 are closed; see PR #1 and release v1.0.0 on Gitea for what was done and verified. Do not re-implement.