wiki-vault/Sources/Dev/pbs-membership-loe1-recipe-saving.md
Travis Herbranson 34a268d8dc migration: copy 62 notes from pbs-projects and homelab-projects
Two-layer structure: Sources (raw notes) + Wiki (compile output)
Four domains: Dev (40), Venture (3), Homelab (23), Reference (0)
Includes CLAUDE.md spec, index pages at all levels, compile log

Co-Authored-By: Lovebug <lovebug@herbylab.dev>
2026-05-08 17:21:13 -04:00

260 lines
8.3 KiB
Markdown

---
created: 2026-04-06
path: Sources/Dev
project: pbs-membership-loe1-recipe-saving
status: active
tags:
- pbs
- flask
- docker
- mysql
- membership
- automation
type: project-plan
updated: 2026-04-06
---
# PBS Membership Platform — LOE 1: Recipe Saving
## Vision
The PBS Membership Platform is a long-term initiative to transform
plantbasedsoutherner.com from a content site into a full lifestyle
platform. The end-state vision includes personalized recipe
recommendations, a whole food diversity tracker, meal planning, a companion
app, and a custom LLM trained on plant-based cooking knowledge.
Recipe Saving is LOE 1 — the foundational project that establishes the
membership infrastructure all other LOEs build on.
---
## LOE 1 Phases Overview
- **Phase 0** — Ultimate Member setup (registration, login, user roles,
basic profile page)
- **Phase 1** — Backend foundation (auth middleware, database, API
endpoints) ← THIS DOC
- **Phase 2** — Save button on WordPress recipe pages (JavaScript widget)
- **Phase 3** — Saved recipes page in pbs-hub (frontend UI)
- **Phase 4** — Collections (named groups, organize saved recipes)
- **Phase 5** — Polish (mobile optimization, loading states, error handling)
---
## All 4 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 |
---
## Phase 1: Backend Foundation
### Goal
Build the API layer and database schema that powers recipe saving. No
frontend — just a working API testable via curl and verifiable in
phpMyAdmin.
### Estimated Time: 4-6 hours
---
### Architecture
Sunnie browser
→ WordPress cookie (set by Ultimate Member login)
→ Request to pbs-hub API
→ Flask auth middleware validates cookie against wp_users (read-only)
→ API endpoint processes request
→ Writes to pbs_hub database
→ Returns JSON response
**Key principle:** WordPress owns identity. pbs-hub owns membership
features. The only bridge is the session cookie and read-only MySQL access.
---
### Database Design
#### New database: pbs_hub
**user_interactions** — generic table for all post-related user actions
(save, like, cooked, planned, rated). Serves LOE 1 now and LOEs 2-4 later.
sql
CREATE DATABASE pbs_hub;
CREATE TABLE pbs_hub.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)
);
**user_preferences** — key-value store for platform-specific user settings
(LOE 4, but created now for completeness).
sql
CREATE TABLE pbs_hub.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)
);
#### MySQL user (scoped permissions)
sql
CREATE USER 'pbshub_app'@'%' IDENTIFIED BY 'strong_password_here';
-- Read-only on WordPress (auth + recipe display)
GRANT SELECT ON wordpress.wp_users TO 'pbshub_app'@'%';
GRANT SELECT ON wordpress.wp_usermeta TO 'pbshub_app'@'%';
GRANT SELECT ON wordpress.wp_posts TO 'pbshub_app'@'%';
-- Full control on pbs_hub
GRANT ALL ON pbs_hub.* TO 'pbshub_app'@'%';
FLUSH PRIVILEGES;
**Note:** Also audit existing MySQL users (pbs-api, n8n) and scope their
permissions. Stop using root for application access.
---
### API Endpoints
| Method | Path | Description | Auth | Tier |
|--------|------|-------------|------|------|
| POST | /api/interactions | Create interaction (save, like, etc.) |
Required | Free |
| DELETE | /api/interactions | Remove interaction | Required | Free |
| GET | /api/interactions?action=save | List user's interactions by action
type | Required | Free |
| GET | /api/interactions/check?post_id=123&action=save | Check if specific
interaction exists | Required | Free |
**Request body (POST):**
json
{
"post_id": 123,
"action": "save",
"metadata": null
}
**Response (GET list):**
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"
}
]
}
**Note:** The GET list endpoint joins pbs_hub.user_interactions with
wordpress.wp_posts to return recipe title and slug. Query filters on
`post_status = 'publish'` and `post_type IN ('post', 'wprm_recipe')` to
exclude revisions, drafts, and other post types.
---
### Auth Middleware
Flask `before_request` middleware that:
1. Reads `wordpress_logged_in_{hash}` cookie from request
2. Extracts user ID and token from cookie value
3. Validates HMAC against wp_users password hash + WordPress secret keys
4. Sets `g.current_user` (user ID) and `g.user_tier` (role) for the request
5. Returns 401 if missing or invalid
**WordPress secret keys** needed from wp-config.php:
- LOGGED_IN_KEY
- LOGGED_IN_SALT
**Security considerations:**
- HTTPS enforced everywhere (Traefik + Cloudflare)
- Read-only MySQL user for WordPress tables
- CSRF protection via Flask-WTF (for future form submissions)
- Cookie is httponly and secure (WordPress default)
---
### Tier Authorization (Future-Proofed)
Not needed for Phase 1 (everything is free), but the pattern is established:
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.
---
### Phase 1 Checklist
- [ ] Create pbs_hub database on staging MySQL
- [ ] Create user_interactions table
- [ ] Create user_preferences table
- [ ] Create pbshub_app MySQL user with scoped permissions
- [ ] Audit and scope existing MySQL users (root cleanup)
- [ ] Extract WordPress secret keys for cookie validation
- [ ] Build Flask auth middleware (cookie → user ID)
- [ ] Build POST /api/interactions endpoint
- [ ] Build DELETE /api/interactions endpoint
- [ ] Build GET /api/interactions endpoint (with wp_posts join)
- [ ] Build GET /api/interactions/check endpoint
- [ ] Test all endpoints via curl
- [ ] Verify data in phpMyAdmin
- [ ] Error handling and logging
---
### Key Decisions Made
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Backend language | Python / Flask | Travis's strongest language. Python
handles PBS scale easily. Revisit Go/Rust only if a specific bottleneck
appears. |
| Auth approach | WordPress cookie validation | Ultimate Member handles
registration/login (Phase 0). pbs-hub validates the cookie for API access.
Plugin is treated as disposable scaffolding. |
| Database | MySQL (pbs_hub database) | Same server, cross-database
read-only joins to WordPress. No data duplication. |
| Interaction table | Generic user_interactions | Single table serves
saves, likes, cooked, planned, rated across all LOEs. Action type is a
column, not a separate table. |
| Recipe data source | WordPress wp_posts (read-only join) | Title and slug
come from wp_posts directly, filtered by post_status and post_type to
exclude revisions. No dependency on pbs_recipes (Instagram automation stays
independent). |
| Paid tier authorization | Code-level, not schema-level | TIER_PERMISSIONS
dictionary gates actions in the API layer. Database doesn't know tiers
exist. |
| MySQL user | Scoped pbshub_app | Read-only on WordPress tables, full
access on pbs_hub. No more root for applications. |
---
### Open Questions
- **WordPress cookie hash suffix:** Need to check wp-config.php for the
exact cookie name (wordpress_logged_in_{hash} where hash is derived from
site URL)
- **WPRM post type:** Confirm whether recipes are stored as 'wprm_recipe'
post type or as regular 'post' with recipe metadata
- **pbs-hub deployment:** Does this extend the existing pbs-hub container
or is Phase 1 a standalone Flask app for testing?
---
*Next Step: Create pbs_hub database and tables on staging, then build the
auth middleware*
...sent from Jenny & Travis