--- 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: 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