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

12 KiB
Raw Blame History

created path project tags type
2026-06-10 Sources/Dev pbs-membership-loe1-phase1-version3
pbs
membership
loe1
flask
mysql
wordpress
auth
docker
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: 46 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)

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)

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

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

{
  "authenticated": true,
  "user_id": 47,
  "username": "janedoe",
  "display_name": "Jane Doe",
  "email": "jane@example.com",
  "roles": ["subscriber"]
}

Unauthenticated:

{ "authenticated": false }
  • user_idcritical; foreign key into pbs_garden interaction rows (maps to wp_user_id).
  • rolesarray; 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):

{ "post_id": 123, "action": "save", "metadata": null }

Response (GET list): joins pbs_garden.user_interactions with wordpress.wp_posts to return title and slug:

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

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.