Docs: new README (Arabic flows, screenshots, architecture) + AGENT.md for AI agents
- 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)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user