5.9 KiB
5.9 KiB
| created | path | project | tags | type | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-06-09 | Sources/Dev | pbs-membership-loe1-phase1-backend-foundation |
|
project-plan |
Supersedes / Source-of-Truth Note
This plan replaces the earlier pbs-membership-loe1-recipe-saving doc as the current truth for Phase 1. Where they disagree, this plan wins. The earlier doc's body is immutable and remains stale on three points it got differently:
- Database name/ownership: was
pbs_hub; nowpbs_garden, owned by the standalone Garden app (see Topology). - Topology: the membership data layer is not an extension of the
pbs-hubcontainer; it belongs to a separate Garden application. - Auth approach: the earlier doc sketched the membership app reimplementing WordPress cookie validation in Python. That is replaced by WordPress validating its own cookie via an internal endpoint (see Auth).
What Phase 1 Is
The backend foundation for membership: the database, the data layer, and the WordPress-side auth endpoint that everything else (the save button in Phase 2, 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, and an authenticated request can write/read an interaction keyed to a WordPress user.
Architectural Foundation (settled)
Identity vs. features
- WordPress (Ultimate Member) owns identity. Registration, login, profiles, roles/tiers. The membership backend never manages credentials.
- The membership backend owns features. Saves, and later likes/cooked/planned/rated, preferences.
- The bridge between them is the validated WordPress session plus read-only access to WordPress tables.
Topology
- The membership data layer belongs to a standalone Flask application in its own container, separate from
pbs-hub.pbs-hubis a live team/production tool; the membership app 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 membership app owns a dedicated database,
pbs_garden, on the shared MySQL server. - Holds the generic interactions model — a single
user_interactionstable designed to serve all interaction types (save / like / cooked / planned / rated) rather than one table per feature, plususer_preferencesas needed. user_id(the WordPress user ID) is the foreign key on interaction rows.- Read-only bridge to WordPress tables (e.g.
wp_postsfor recipe title/slug) — one-directional, read-only. - Scoped MySQL user: full control on
pbs_garden, read-only on the specific WordPress tables needed, nothing else.
Auth — mechanism
- WordPress validates its own cookie. On an authenticated request, the membership app 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 membership app) and reliability (WordPress cannot drift out of sync with its own session format; survives WP/plugin/security-stack changes). The app already depends on WordPress at request time for read-only recipe data, so there is no runtime-independence benefit lost by this choice.
- Endpoint is internal-network only (server-to-server on the Docker network), never exposed via Traefik/Cloudflare.
- 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 → membership app)
Authenticated:
{
"authenticated": true,
"user_id": 47,
"username": "janedoe",
"display_name": "Jane Doe",
"email": "jane@example.com",
"roles": ["subscriber"]
}
Unauthenticated:
{ "authenticated": false }
user_id— critical; foreign key intopbs_gardeninteraction rows.roles— array; membership tiers are expressed as WordPress roles (WordPress/Ultimate Member assigns them; the app is a pure reader). No separate tier field, no second source of truth. Multi-tier supported from day one.emailincluded now (cheap; wanted for later account/notification features).
Build Surface (Phase 1 deliverables)
pbs_gardendatabase +user_interactions(generic interaction model) and anyuser_preferencestable- Scoped MySQL user (full on
pbs_garden, read-only on needed WP tables) - Standalone Flask app skeleton (its own container) with the data layer / models
- WordPress-side internal validation endpoint (mu-plugin or REST route) returning the contract above
- App-side auth: forward cookie → call endpoint → cache per-session → attach
user_idto request - Interaction read/write endpoints (create/list a saved interaction for the authenticated user)
Open Questions (carried from original Phase 1; confirm at build)
- Recipe post type: are recipes the
wprm_recipepost type or regular WordPress posts? Determines what the read-only WP bridge joins against. user_interactionsschema specifics: confirm column set (interaction type enum, target post ID as the right type, timestamps) before first migration. (Note: Instagram-style IDs are not relevant here, but confirm the recipe/post ID type matches WordPress.)- Ultimate Member tier roles: confirm UM can assign tier roles/capabilities the way tiering will need (contract is unaffected either way; flag if awkward).
Out of Scope
- Save button UI on WordPress (Phase 2).
- Member area / browsing UI (later).
- Tier gating logic (the contract carries roles; enforcing tier access comes when tiers exist).