8.3 KiB
project: pbs-membership-loe1-recipe-saving type: project-plan status: active tags:
- pbs
- flask
- docker
- mysql
- membership
- automation created: 2026-04-06 updated: 2026-04-06 path: PBS/Tech/Projects/
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:
- Reads
wordpress_logged_in_{hash}cookie from request - Extracts user ID and token from cookie value
- Validates HMAC against wp_users password hash + WordPress secret keys
- Sets
g.current_user(user ID) andg.user_tier(role) for the request - 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