wiki-vault/Sources/Dev/pbs-membership-loe1-phase1-backend-foundation.md

93 lines
5.9 KiB
Markdown

---
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).