mcp: project-plan — PBS Membership LOE 1 Phase 1 — Backend Foundation (v3, build-ready)
This commit is contained in:
parent
e7e905dcd7
commit
7d3cc87da9
234
Sources/Dev/pbs-membership-loe1-phase1-version3.md
Normal file
234
Sources/Dev/pbs-membership-loe1-phase1-version3.md
Normal file
@ -0,0 +1,234 @@
|
||||
---
|
||||
created: '2026-06-10'
|
||||
path: Sources/Dev
|
||||
project: pbs-membership-loe1-phase1-version3
|
||||
tags:
|
||||
- pbs
|
||||
- membership
|
||||
- loe1
|
||||
- flask
|
||||
- mysql
|
||||
- wordpress
|
||||
- auth
|
||||
- docker
|
||||
type: project-plan
|
||||
---
|
||||
|
||||
## Supersedes / Source-of-Truth Note
|
||||
|
||||
This is the build-ready, canonical Phase 1 plan. It supersedes both prior docs:
|
||||
|
||||
- **`pbs-membership-loe1-recipe-saving`** — stale on database name (`pbs_hub`), topology (extension of pbs-hub), and auth (Python reimplementation of WordPress cookie HMAC validation). **Architecture is wrong; implementation specifics were correct** and are carried forward here.
|
||||
- **`pbs-membership-loe1-phase1-backend-foundation`** — correct architecture and auth, but written without the original in context, which de-specified the SQL schema and collapsed the API surface to a single vague checkbox. **Architecture is right; build detail was thin** and is restored here.
|
||||
|
||||
Where any of the three disagree, **this doc wins**.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Build the backend foundation for membership: the database, the data layer, and the WordPress-side auth endpoint that everything else (the Phase 2 save button, 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 via WordPress itself, and an authenticated request can write/read/delete an interaction keyed to a WordPress user — testable via curl and verifiable in phpMyAdmin.
|
||||
|
||||
Estimated time: 4–6 hours (carried from original; auth approach is smaller to build than the original's HMAC reimplementation, so this is conservative).
|
||||
|
||||
---
|
||||
|
||||
## Locked Decisions
|
||||
|
||||
### Identity vs. features
|
||||
- **WordPress (Ultimate Member) owns identity** — registration, login, profiles, roles/tiers. The membership backend never manages credentials.
|
||||
- **The membership backend (the Garden) owns features** — saves, and later likes/cooked/planned/rated, preferences.
|
||||
- **The bridge** is the validated WordPress session plus read-only access to WordPress tables.
|
||||
|
||||
### Topology
|
||||
- The membership data layer belongs to a **standalone Flask application (the Garden) in its own container**, separate from `pbs-hub`. `pbs-hub` is a live team/production tool; the Garden 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 Garden **owns a dedicated database, `pbs_garden`**, on the shared MySQL server.
|
||||
- Holds the **generic interactions model** — a single `user_interactions` table serving all interaction types (save / like / cooked / planned / rated) rather than one table per feature, plus `user_preferences`.
|
||||
- `wp_user_id` (the WordPress user ID, returned by the auth contract) is the foreign key on interaction rows.
|
||||
- **Read-only, one-directional bridge to WordPress tables** (`wp_posts` for recipe title/slug).
|
||||
- Scoped MySQL user: full control on `pbs_garden`, read-only on the specific WordPress tables needed, nothing else. No root for application access.
|
||||
|
||||
#### Concrete schema (restored from original; confirm post type before first migration — see Open Items)
|
||||
|
||||
```sql
|
||||
CREATE DATABASE pbs_garden;
|
||||
|
||||
CREATE TABLE pbs_garden.user_interactions (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
wp_user_id INT UNSIGNED NOT NULL,
|
||||
wp_post_id BIGINT UNSIGNED NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
metadata JSON NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_action (wp_user_id, wp_post_id, action)
|
||||
);
|
||||
|
||||
CREATE TABLE pbs_garden.user_preferences (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
wp_user_id INT UNSIGNED NOT NULL,
|
||||
pref_key VARCHAR(100) NOT NULL,
|
||||
pref_value JSON NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_pref (wp_user_id, pref_key)
|
||||
);
|
||||
```
|
||||
|
||||
`wp_post_id` is `BIGINT UNSIGNED` to match WordPress's post ID type. (Instagram-style IDs are not relevant here — that VARCHAR concern is from the content-hub work, not this.)
|
||||
|
||||
#### Scoped MySQL user (restored from original; database name updated)
|
||||
|
||||
```sql
|
||||
CREATE USER 'pbsgarden_app'@'%' IDENTIFIED BY 'strong_password_here';
|
||||
|
||||
-- Read-only on WordPress (auth + recipe display)
|
||||
GRANT SELECT ON wordpress.wp_posts TO 'pbsgarden_app'@'%';
|
||||
|
||||
-- Full control on pbs_garden
|
||||
GRANT ALL ON pbs_garden.* TO 'pbsgarden_app'@'%';
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
Note: the app no longer needs `SELECT` on `wp_users` / `wp_usermeta` for auth — WordPress validates its own cookie and returns the user. The only WordPress read the app needs is `wp_posts` for recipe title/slug. (Audit existing MySQL users — pbs-api, n8n — and scope them off root while you're in here.)
|
||||
|
||||
### Auth — mechanism (Option 2: WordPress validates its own cookie)
|
||||
- On an authenticated request, the Garden 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 Garden) and **reliability** (WordPress cannot drift out of sync with its own session format; survives WP/plugin/security-stack changes). The Garden already depends on WordPress at request time for read-only recipe data, so no runtime-independence is lost.
|
||||
- Endpoint is **internal-network only** (server-to-server on the Docker network), **never exposed via Traefik/Cloudflare.** This constraint is load-bearing for security — an exposed endpoint would be an unauthenticated "who is this cookie" oracle.
|
||||
- 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 → Garden)
|
||||
|
||||
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 (maps to `wp_user_id`).
|
||||
- `roles` — **array**; membership **tiers are expressed as WordPress roles** (WordPress/Ultimate Member assigns them; the Garden 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).
|
||||
|
||||
### API endpoint surface (restored from original; auth row reflects new mechanism)
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| POST | `/api/interactions` | Create interaction (save, like, etc.) | Required |
|
||||
| DELETE | `/api/interactions` | Remove interaction | Required |
|
||||
| GET | `/api/interactions?action=save` | List user's interactions by action type | Required |
|
||||
| GET | `/api/interactions/check?post_id=123&action=save` | Check if a specific interaction exists | Required |
|
||||
|
||||
The **check endpoint is not optional** — Phase 2's save button needs it to render a recipe as already-saved on page load.
|
||||
|
||||
**Request body (POST):**
|
||||
```json
|
||||
{ "post_id": 123, "action": "save", "metadata": null }
|
||||
```
|
||||
|
||||
**Response (GET list):** joins `pbs_garden.user_interactions` with `wordpress.wp_posts` to return title and slug:
|
||||
```json
|
||||
{
|
||||
"interactions": [
|
||||
{
|
||||
"post_id": 123,
|
||||
"action": "save",
|
||||
"post_title": "Southern Black-Eyed Pea Stew",
|
||||
"post_slug": "southern-black-eyed-pea-stew",
|
||||
"saved_at": "2026-04-06T12:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**`wp_posts` join filter (restored from original — load-bearing):** filter on `post_status = 'publish'` and `post_type IN ('post', 'wprm_recipe')` to exclude revisions, drafts, autosaves, and other post types. Forgetting this returns garbage rows.
|
||||
|
||||
### Tier authorization — future-proofed, not built
|
||||
Tier **gating** is out of scope for Phase 1 (everything is free). The pattern, for when tiers exist:
|
||||
|
||||
```python
|
||||
TIER_PERMISSIONS = {
|
||||
"free": ["save", "like"],
|
||||
"basic": ["save", "like", "planned", "cooked"],
|
||||
"premium": ["save", "like", "planned", "cooked", "collections", "ai_suggest"],
|
||||
}
|
||||
```
|
||||
|
||||
Adding paid tiers later is a code change, not a schema change. The contract already carries `roles[]`, so the source of truth is in place; only the enforcement layer is deferred.
|
||||
|
||||
---
|
||||
|
||||
## Open Items
|
||||
|
||||
- [ ] **Recipe post type:** are recipes the `wprm_recipe` post type or regular WordPress `post` with recipe metadata? Determines the `post_type IN (...)` filter on the `wp_posts` join. Confirm before first migration.
|
||||
- [ ] **`user_interactions` final columns:** schema above is the intended shape; confirm column set and the `wp_post_id` type matches WordPress (`BIGINT UNSIGNED`) before the first migration.
|
||||
- [ ] **Ultimate Member tier roles:** confirm UM can assign tier roles/capabilities the way tiering will need. Contract is unaffected either way; flag if awkward.
|
||||
- [ ] **WordPress endpoint form:** mu-plugin vs. custom REST route for the internal validation endpoint — pick at build, both satisfy the contract.
|
||||
|
||||
---
|
||||
|
||||
## Build Phases
|
||||
|
||||
### Phase 1 deliverables checklist
|
||||
- [ ] Create `pbs_garden` database on staging MySQL
|
||||
- [ ] Create `user_interactions` table
|
||||
- [ ] Create `user_preferences` table
|
||||
- [ ] Create `pbsgarden_app` MySQL user with scoped permissions (full on `pbs_garden`, read-only on `wp_posts`)
|
||||
- [ ] Audit and scope existing MySQL users (pbs-api, n8n) off root
|
||||
- [ ] Standalone Garden Flask app skeleton (its own container) with data layer / models
|
||||
- [ ] WordPress-side internal validation endpoint (mu-plugin or REST route) returning the contract
|
||||
- [ ] App-side auth: forward cookie → call endpoint → cache per-session (~5 min TTL) → attach `user_id` to request
|
||||
- [ ] `POST /api/interactions` endpoint
|
||||
- [ ] `DELETE /api/interactions` endpoint
|
||||
- [ ] `GET /api/interactions` endpoint (with `wp_posts` join + publish/post-type filter)
|
||||
- [ ] `GET /api/interactions/check` endpoint
|
||||
- [ ] Test all endpoints via curl
|
||||
- [ ] Verify data in phpMyAdmin
|
||||
- [ ] Verify validation endpoint is unreachable from the public internet (internal-network only)
|
||||
- [ ] Error handling and logging
|
||||
|
||||
### Out of scope
|
||||
- Save button UI on WordPress (Phase 2)
|
||||
- Member area / browsing UI (later)
|
||||
- Tier *gating* enforcement (the contract carries roles; enforcing comes when tiers exist)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### LOE 1 phase roadmap (orienting context)
|
||||
- **Phase 0** — Ultimate Member setup (registration, login, roles, basic profile)
|
||||
- **Phase 1** — Backend foundation ← THIS DOC
|
||||
- **Phase 2** — Save button on WordPress recipe pages (JS widget; needs the check endpoint)
|
||||
- **Phase 3** — Saved recipes member area (the Garden frontend; see phase3a-garden-skeleton)
|
||||
- **Phase 4** — Collections (named groups)
|
||||
- **Phase 5** — Polish (mobile, loading states, error handling)
|
||||
|
||||
### The four Lines of Effort
|
||||
| LOE | Project | Status |
|
||||
|-----|---------|--------|
|
||||
| 1 | Recipe Saving | Active — Phase 1 |
|
||||
| 2 | Weekly Meal Plan Viewer | Queued |
|
||||
| 3 | Whole Food Diversity Tracker | Queued |
|
||||
| 4 | User Profiles & Preferences | Queued |
|
||||
|
||||
### Why the auth approach changed (for future readers)
|
||||
The original doc had the Garden reimplementing WordPress's cookie HMAC validation in Python, which required copying `LOGGED_IN_KEY`/`LOGGED_IN_SALT` into the Garden and reproducing WP's crypto. That spreads auth secrets to a second app (security risk) and couples the Garden to WP's internal cookie format (breaks silently on WP/security-stack changes). Option 2 — WordPress validates its own cookie — keeps secrets sealed in hardened WordPress and cannot drift out of sync with itself. Chosen against the criteria "always works, always secure."
|
||||
|
||||
### What this doc fixed relative to the two it supersedes
|
||||
- From recipe-saving: corrected db name, topology, auth.
|
||||
- From phase1-backend-foundation: restored the concrete SQL, the four-endpoint API surface (including the DELETE and check endpoints), and the `wp_posts` publish/post-type filter — all of which had been de-specified in the rewrite.
|
||||
Loading…
Reference in New Issue
Block a user