Files
anwar-pi-website/DATABASE.md
T

5.5 KiB
Raw Permalink Blame History

DATABASE — anwarpy.ai

The schema is Payload-first: collections in web/src/collections/ are the source of truth; SQL in Postgres is a generated artifact. This document maps the logical schema; never edit SQL by hand.

1. Adapter & migrations

  • @payloadcms/db-postgres (Drizzle). Connection = DATABASE_URI (prod: composed by docker-compose.vps.yml from DB_PASSWORD; local: web/.env → LAN Postgres 192.168.88.36).
  • Migrations live in src/migrations/ (timestamped YYYYMMDD_HHMMSS.ts + paired .json). Created with npx payload migrate:create; applied on container boot in prod, or npx payload migrate locally.
  • Rules: never edit an applied migration (history: the 20261003_111451 duplicate-full_name fix was done in place before prod applied it — that's the exception that proves the risk). Data backfills are standalone scripts under web/scripts/ (backfill-reading-minutes.ts pattern), not migrations. payload_migrations is bookkeeping — if it says applied, it is.

2. Collections

posts — guides (the core table)

Field Type Notes
title, slug text slug unique, URL identity, 409 on conflict
dek text standfirst/summary, feeds metadata + JSON-LD
category select وكلاء / نماذج / استرجاع / سلامة / أدوات / أعمال
topics select/array registry in src/lib/topics.ts (agents, models, automation, foundations)
access select free | vip intended tier — data, not state; never rewritten to simulate launch states
content lexical (richtext) serialized {root:{children:[…]}}; field-level read ACL (ARCHITECTURE §3)
contentType select note / tutorial / case_study / comparison — badge
authorName text visible byline; also JSON-LD author Person
sourceId text agent-pipeline idempotency; dedup before slug-409
cover upload → media og:image source (1200×630)
readingMinutes number computed at write (countLexicalWords ÷ READING_WORDS_PER_MINUTE=160); stripped from agent payloads; backfilled honestly
publishedAt date feeds «حُدّث» + JSON-LD
status select draft | published public visibility gate; publish: true agent opt-in
derived/rels — views, likes counters (see routes)

users

Payload auth: email (unique), password (hash), fullName, interest/level (onboarding), emailVerified flag + verification_token (written at signup, ADR-compliant), stripeCustomerId (Phase 3; hidden/removed when empty — never exposed to frontend). Roles: admin vs member (see SECURITY §3). Note: 20261003_111451 removed a duplicate full_name column.

comments

post (rel → posts), author (rel → users, forced server-side — the API ignores any client-sent author, ADR-0002), authorName (display snapshot), body (textarea), status (moderation: pending → published / rejected). Anonymous requests are rejected at the route (PRD §3.5). admin.defaultColumns: post, authorName, status, createdAt.

launch_notifications

email, topic, timestamps. Written by /api/notify on every submission (even when mail transport is down — the honest-503 still records the row). Duplicate email → no new row, alreadySubscribed: true. Zero rows during the 2026-10-10 prod smoke cleanup is the expected state.

post_likes

user (rel), post (rel) — one per user per post (uniqueness enforced in the like route before insert); counter incremented on posts. Auth required (401 otherwise).

media

Payload uploads (covers + pipeline images) → volume media-uploads; sharp for sizes. Public URL pattern /api/media/file/....

files

Guide attachments → volume files-uploads; per-file access mirrors its post (download gate GET /api/files/[id]); ingested via agent-files.ts (never accepts access from the payload).

3. Relationships & integrity

posts 1─* comments *─1 users
posts 1─* post_likes *─1 users   (unique(user,post))
posts *─1 media (cover)
posts 1─* files                  (each file carries its own access)

Enforcement lives in collection hooks/route handlers (Payload has no SQL-level FK constraints across its tables for these patterns) — hence the "check then insert" pattern in like (query where user+post before create). Keep that pattern for any new per-user-once resource.

4. Indexing

  • Unique: posts.slug, users.email, posts.sourceId (dedup lookup), verification_token.
  • Hot lookups: comments.post (+ status), post_likes.post, launch_notifications.email (dup check).
  • Add indexes via migrations when a query pattern shows up in slow logs — not speculatively.

5. Backups & restore

  • web/scripts/backup-db.sh → pg_dump gzip; prod cron/backup policy is an owner action.
  • Rehearsed restore (2026-10-10): restore into a temp DB (anwarpi_restore_test), verify counts (posts, users, payload_migrations), then drop. Never restore over the live DB without owner approval.

6. Data rules that are also PRD rules

  • access and readingMinutes are stripped from agent payloads (pickFields) — the pipeline cannot set them.
  • Reading minutes are computed, never trusted from input; if a script touches them, it must recompute from content and report scanned/updated counts.
  • Personal data in launch_notifications/users emails: never rendered into docs, videos, or evidence (the smoke test uses @example.com rows and deletes them).