- README rewritten for the real Next.js + Payload + Postgres stack (old one described the dead static prototype) - Logo + 4 live screenshots (home, articles, post, subscription) - Arabic flow explanations with mermaid diagrams: visitor journey, content pipeline, request path - Architecture table, quick start, API reference, security summary, deploy commands - AGENT.md: orientation for future AI agents (data model, trust rules, rate limits, routes, env, deploy, house rules) - GLOSSARY.md and docs/adr committed (linked from README)
8.1 KiB
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) |
| 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. Onlyapprovedis public. Authors cannot edit/delete their own comments (moderated model, docs/adr/0002). Display name is a snapshot offullNameat submit time. - Media / Files — uploads (files served via
/api/files/[id]). - PostLikes — one like per member per post.
Trust model (critical — do not break)
- Roles are server-forced.
POST /api/usersand any client write cannot setrole,emailVerified,stripeCustomerId, orverificationToken— thebeforeChangehook inUsers.tsdeletes them for untrusted operations. Onlyreq.user.role === 'admin'orcontext.systemActivation === true(payment activation from server code) counts as trusted. - 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). - Comments require verification (
emailVerified), admins exempt. - Client requests can never mint VIP (free
/payment-successvisits 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/users5/h,POST /api/users/login10/15min,POST /api/users/forgot-password5/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.tsbeforeOperationlogin hook runs a dummy PBKDF2 (same params, saltanwarpy-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
pagelimit60/min burst 120 +connlimit50 in-flight per IP; APIapilimit90/min burst 180. Strict-Transport-Security: max-age=63072000; includeSubDomainson all routes (web/next.config.mjs). Do not addpreload.
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
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.tsautomatically on build (payload generate:types). - Schema changes require a migration (
payload migrate:create); the container CMD runspayload migrateon 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.