diff --git a/Sources/Dev/pbs-membership-loe1-phase1-backend-foundation.md b/Sources/Dev/pbs-membership-loe1-phase1-backend-foundation.md new file mode 100644 index 0000000..617e902 --- /dev/null +++ b/Sources/Dev/pbs-membership-loe1-phase1-backend-foundation.md @@ -0,0 +1,93 @@ +--- +created: '2026-06-09' +path: Sources/Dev +project: pbs-membership-loe1-phase1-backend-foundation +tags: +- pbs +- membership +- loe1 +- flask +- mysql +- wordpress +- auth +- backend +type: project-plan +--- + +## Supersedes / Source-of-Truth Note + +This plan replaces the earlier `pbs-membership-loe1-recipe-saving` doc as the current truth for Phase 1. Where they disagree, **this plan wins**. The earlier doc's body is immutable and remains stale on three points it got differently: + +- **Database name/ownership:** was `pbs_hub`; now **`pbs_garden`**, owned by the standalone Garden app (see Topology). +- **Topology:** the membership data layer is **not** an extension of the `pbs-hub` container; it belongs to a separate Garden application. +- **Auth approach:** the earlier doc sketched the membership app **reimplementing** WordPress cookie validation in Python. That is replaced by **WordPress validating its own cookie** via an internal endpoint (see Auth). + +--- + +## What Phase 1 Is + +The backend foundation for membership: the database, the data layer, and the WordPress-side auth endpoint that everything else (the save button in Phase 2, the member area later) builds on. No member-facing UI in this phase. Success = the data layer exists, a member's WordPress login can be validated server-side, and an authenticated request can write/read an interaction keyed to a WordPress user. + +## Architectural Foundation (settled) + +### Identity vs. features +- **WordPress (Ultimate Member) owns identity.** Registration, login, profiles, roles/tiers. The membership backend never manages credentials. +- **The membership backend owns features.** Saves, and later likes/cooked/planned/rated, preferences. +- **The bridge between them** is the validated WordPress session plus read-only access to WordPress tables. + +### Topology +- The membership data layer belongs to a **standalone Flask application in its own container**, separate from `pbs-hub`. `pbs-hub` is a live team/production tool; the membership app must not share its deploy lifecycle or blast radius. +- Shared **MySQL server**, but **no shared application tables** and **no shared application logic** with `pbs-hub`. + +### Database — `pbs_garden` +- The membership app **owns a dedicated database, `pbs_garden`**, on the shared MySQL server. +- Holds the generic interactions model — a single `user_interactions` table designed to serve all interaction types (save / like / cooked / planned / rated) rather than one table per feature, plus `user_preferences` as needed. +- `user_id` (the WordPress user ID) is the foreign key on interaction rows. +- **Read-only bridge to WordPress tables** (e.g. `wp_posts` for recipe title/slug) — one-directional, read-only. +- Scoped MySQL user: full control on `pbs_garden`, read-only on the specific WordPress tables needed, nothing else. + +### Auth — mechanism +- **WordPress validates its own cookie.** On an authenticated request, the membership app forwards the incoming `wordpress_logged_in_*` cookie to a small **internal** WordPress endpoint (mu-plugin / REST route). WordPress reads its own session server-side and returns the user. +- Chosen for **security** (WP secret keys and auth crypto stay inside hardened WordPress — never copied into the membership app) and **reliability** (WordPress cannot drift out of sync with its own session format; survives WP/plugin/security-stack changes). The app already depends on WordPress at request time for read-only recipe data, so there is no runtime-independence benefit lost by this choice. +- Endpoint is **internal-network only** (server-to-server on the Docker network), never exposed via Traefik/Cloudflare. +- Validation responses are **cached per-session, short TTL (~5 min)**, so WordPress isn't queried on every request. Logout/expiry still works because the cookie itself becomes invalid. + +### Auth — contract (WordPress endpoint → membership app) + +Authenticated: +```json +{ + "authenticated": true, + "user_id": 47, + "username": "janedoe", + "display_name": "Jane Doe", + "email": "jane@example.com", + "roles": ["subscriber"] +} +``` +Unauthenticated: +```json +{ "authenticated": false } +``` + +- `user_id` — **critical**; foreign key into `pbs_garden` interaction rows. +- `roles` — **array**; membership **tiers are expressed as WordPress roles** (WordPress/Ultimate Member assigns them; the app is a pure reader). No separate tier field, no second source of truth. Multi-tier supported from day one. +- `email` included now (cheap; wanted for later account/notification features). + +## Build Surface (Phase 1 deliverables) +- [ ] `pbs_garden` database + `user_interactions` (generic interaction model) and any `user_preferences` table +- [ ] Scoped MySQL user (full on `pbs_garden`, read-only on needed WP tables) +- [ ] Standalone Flask app skeleton (its own container) with the data layer / models +- [ ] WordPress-side internal validation endpoint (mu-plugin or REST route) returning the contract above +- [ ] App-side auth: forward cookie → call endpoint → cache per-session → attach `user_id` to request +- [ ] Interaction read/write endpoints (create/list a saved interaction for the authenticated user) + +## Open Questions (carried from original Phase 1; confirm at build) +- [ ] **Recipe post type:** are recipes the `wprm_recipe` post type or regular WordPress posts? Determines what the read-only WP bridge joins against. +- [ ] **`user_interactions` schema specifics:** confirm column set (interaction type enum, target post ID as the right type, timestamps) before first migration. *(Note: Instagram-style IDs are not relevant here, but confirm the recipe/post ID type matches WordPress.)* +- [ ] **Ultimate Member tier roles:** confirm UM can assign tier roles/capabilities the way tiering will need (contract is unaffected either way; flag if awkward). + +## Out of Scope +- Save button UI on WordPress (Phase 2). +- Member area / browsing UI (later). +- Tier *gating* logic (the contract carries roles; enforcing tier access comes when tiers exist). \ No newline at end of file