- 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)
161 lines
8.1 KiB
Markdown
161 lines
8.1 KiB
Markdown
# 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.
|