12 KiB
12 KiB
| created | path | project | tags | type | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-06-19 | Sources/Dev | pbs-membership-platform-overview |
|
session-notes |
Outcome
Three phases of the PBS Sunflower Garden are now coded, pushed to gitea, and deployed end-to-end on the herbylab sandbox VM (pbs.herbylab.dev, PVE 202, IP 10.0.21.101). The full auth chain (browser cookie → Traefik → member-garden → GardenAuth resolver → WordPress mu-plugin → identity) is verified working over the public route. The Garden frontend renders, the API gates correctly, the deploy is repeatable.
Phases executed in one session (2026-06-19), Lovebug supervising 6 workers via petal-dispatch:
- Phase 1 finish (member-garden functional tier + WP mu-plugin + DB apply + integration deploy) — extends thread #597
- Phase 2 (save-button WPCode snippet + member-garden public Traefik routing) — thread #600
- Phase 3a (Garden skeleton at
/the-garden/) — thread #601
What ships:
- member-garden Flask app with four interaction endpoints (POST/DELETE/GET/check), pluggable
AuthResolver(WP-cookie impl, 300s bounded TTL cache, fail-closed on missing shared secret), Garden blueprint at/the-gardenwith branded shell (header/hero/card-grid/footer), logout + login routes. - WordPress mu-plugin (
/wp-json/garden/v1/validate) protected by two-belt defense: shared-secret headerX-Garden-Auth+ Traefik path deny for/wp-json/garden/*. - wordpress-install ansible role wiring: clone member-garden from gitea (configurable branch via
member_garden_repo_version), bind-mount mu-plugin into WP, template the shared secret into both containers' env, force-rebuild member-garden image on every clone, route/api/interactionsand/the-gardento member-garden via Traefik. - WPCode Lite snippet source-of-truth in
wordpress-install/wpcode-snippets/save-button/(manual paste into WP admin). - Vault keys generated for the herbylab sandbox:
vault_garden_auth_shared_secret,vault_pbsgarden_db_password. Staging/prod vaults untouched.
Branches landed (all gitea-only, never GitHub):
petal-power/member-garden:feat/phase-1-functional-tier(commits1820943,edf13ea) — 34 tests, 99% coveragefeat/phase-3a-garden-skeletonoff the above (commits7094076,c2097fa) — 44 tests, 99% coverage
petal-power/wordpress-install:feat/wp-mu-plugin-garden-auth(commit116a72d)integration/garden-phase1-deploy(tipca2bd35) — merges fix-pbs-init-sql-syntax + mu-plugin branch + role bumpsfeat/save-button-phase2(tips0ca8c1d→89428c6→4f2d105) — save-button source + Traefik labels + Phase 3a inventory bump
Final smoke test on https://pbs.herbylab.dev/: 6/6 (deploy worker) + 4/4 (Phase 2 deploy worker) + 7/7 (Phase 3a deploy worker) = 17/17 checks ✅.
Topics Covered
- Auth contract implementation — both sides of the WP-validates-its-own-cookie design (vault 247/294 Option 2). Shared-secret header
X-Garden-Authwas added during implementation as a server-to-server access control on top of the cookie validation, fails closed both sides on missing secret. Cache is 300s TTL, sha256-keyed, size-capped at 10k, positive-only (negative responses not cached). - Pluggable
AuthResolverABC so a future React Native client can swap a token-based resolver in without touching endpoints. pbs_gardenDB applied to herbylab MySQL via the existing scoped-user SQL pattern; grants verified: full onpbs_garden.*, SELECT onwordpress.wp_posts, USAGE elsewhere.- Traefik routing strategy for member-garden — switched the originally-planned
garden.<domain>subdomain to same-host path-prefixes (/api/interactionspriority 110,/the-gardenpriority 100). Same-host is the only way the browser auto-sends the WP cookie to member-garden. - WPCode Lite save-button widget — JS+CSS+README in repo as source of truth; copy/paste into WP admin. Reads post_id from
body.postid-{N}(canonical WP, not the WPRM recipe id), detects login fromwordpress_logged_in_cookie name (render hint; API 401 is authoritative), two injection points (top-of-post next to Jump/Print, inside recipe card next to Print). Logged-out → modal/bottom-sheet signup funnel. - Garden frontend at
/the-garden/: branded chrome with garden-owned palette, welcome hero with display-name greeting, 4-card grid (Saved Recipes active + 3 coming-soon), Jinja_card.htmlmacro,before_requestauth gate returns 302 (not 401) to UM login withredirect_to=. Static assets and/logout//loginare auth-exempt. UM_LOGIN_URL defaults to${WP_BASE_URL}/login/.
Key Learnings
- a-review is paying dividends. Each worker invoked a-review (Gemini via
agy) after zero-check. It caught: a TTL cache memory-leak/DoS vector on the resolver (size-capped + negative-not-cached), static assets behind the Garden auth gate (login loop), env-var default loaded before config overrides (silent prod-wrong default), button race during init API call (visible UI bug), and a cookie-name substring false-positive in the save-button JS. All real bugs the workers fixed before reporting done. - The
/apiTraefik collision was a near-miss. pbs-api already ownedHost && PathPrefix(/api)at priority 100. A literal/apimember-garden router would have non-deterministically intercepted pbs-api's/api/recipes,/api/health, etc. Scoping to/api/interactionsat priority 110 sidesteps it cleanly. Rule worth codifying: any new container claiming a path prefix on a shared host must check existing routers first; never claim a parent prefix of something already in use. docker_compose_v2doesn't rebuild on source changes by default — the first deploy of the functional tier silently shipped foundation-tier code because the existing image was cached. Fixed by addingcontainer_compose_build: alwaysfor member-garden. Worth checking whether pbs-api has the same latent gap.- WP-CLI is not in the wordpress image. Both deploy workers (W3, W6) had to mint test cookies by bootstrapping
wp-load.phpviadocker exec wordpress php -r '...'and callingwp_generate_auth_cookiedirectly. Worth a follow-up to bake wp-cli into the image (or document the bootstrap pattern). - Same-host cookie routing is load-bearing. The original spec (vault 247) mentioned a
garden.<domain>subdomain as one option; that path silently breaks the browser sending the WP cookie to the Garden. Same-host path-prefix routing is the only working topology and should be documented as required, not optional. - WordPress on herbylab was never fully installed before this session — the install wizard was reachable but no admin user existed. W3 ran
wp core installto enable end-to-end auth verification. The admin password is ephemeral (not persisted to vault).
Follow-ons
Production-readiness gaps (must address before live cutover)
- CSRF nonce on save/unsave — flagged CRITICAL by a-review on W4. POST/DELETE to
/api/interactionsrely on ambient cookie auth with no nonce. Robust fix: addX-WP-Noncevalidation to the member-garden API (wp_create_nonce('wp_rest')injected via WPCode into the page, JS sends it in header, Flask validates against WP). Current mitigations (same-origin + JSON content-type) are partial only. - member-garden migrations not automated —
flask db upgradehad to be run manually viadocker cpof the migrations/ dir + container exec. Bake migrations into the image OR add an ansible task that runs them on deploy. X-Forwarded-Protonot honored — Traefik terminates TLS; Flask seeshttp://and builds the unauth 302redirect_towith the wrong scheme. Fix withwerkzeug.middleware.proxy_fix.ProxyFixin the app factory.- WP logout confirmation nonce —
wp-login.php?action=logoutwithout_wpnonceshows the "Do you really want to log out?" interstitial. Seamless logout needs a WP-side endpoint that accepts authenticated logout without a nonce, or a mu-plugin route. - WPCode save-button browser test — sandbox WP has no recipe content; W4 documented a manual test plan in the README but a real browser test (Chrome on a real recipe page) hasn't been done.
- Brand color hex + Sunnie SVG assets for both the save-button (filled saved state) and the Garden hero/card placeholders — Jenny input needed before launch.
Carryover / side-issues (named, not fixed)
- pbs-api crash-loop on herbylab —
pbs_recipesFK type mismatch (instagram_posts_ibfk_1referencespbs_recipes(post_id)with incompatiblepbs_post_idcolumn type → MySQLOperationalError 3780). Does NOT touchpbs_garden. Travis to backfill (he mentioned this during the session). - Two unmerged branches in wordpress-install awaiting Travis's merge to main:
fix-pbs-init-sql-syntax(from thread #582) andfeat/wp-mu-plugin-garden-auth(this session). Both currently included transitively viaintegration/garden-phase1-deployandfeat/save-button-phase2. Eventual merge order:fix-pbs-init-sql-syntax→ main, thenfeat/wp-mu-plugin-garden-auth(rebased) → main, thenintegration/garden-phase1-deploy→ main, thenfeat/save-button-phase2→ main. - member-garden branches awaiting merge to main:
feat/phase-1-functional-tierfirst, thenfeat/phase-3a-garden-skeletonon top. - WP admin password on herbylab is ephemeral. Document or store in herbylab vault if persistence is wanted.
/loginvs auth-gate URL consistency — Garden's convenience/loginroute hardcodeswp-login.phpper the W5 brief, while the gate usesUM_LOGIN_URL. Unify onUM_LOGIN_URLin Phase 3b.
Open questions for Jenny
- Final brand palette hex values (
--garden-bg,--garden-fg,--garden-accent,--garden-muted,--garden-card,--pbs-brandfor the save button). - Real chef Sunnie SVG (hero placeholder), seed-packet active + coming-soon SVGs (card placeholders), filled Sunnie save-state SVG (button placeholder).
- Hero copy: "Welcome to the Sunflower Garden, {name}." / "Here's what's growing today." — keep or revise?
- Card lineup for launch: 3a ships Saved Recipes active + Meal Plans / Diversity Tracker / Profile coming-soon. Right set/labels?
- Top-nav placeholders: which LOE landing pages get permanent top-nav slots in Phase 3a vs. later.
- Pinterest button — decide whether Pin sits alongside Save (deferred per original spec).
Phase 3b candidates (next session)
- Wire Saved Recipes card to real data: query
pbs_garden.user_interactionsjoined withwordpress.wp_posts, show count + clickable. - Build the saved-recipes detail page (3c) at
/the-garden/saved(currently the 3b stub). - "Recently added to your garden" strip on the home (Phase 3d).
Architecture decisions worth keeping
- Auth contract is the seam. Mu-plugin returns
{authenticated, user_id, username, display_name, email, roles[]}. Tiers ride in as additional WP role values (Path A — no second source of truth). Multi-tier supported from day one without a contract change. - Two-belt internal-only on the mu-plugin. Shared-secret header (PHP
permission_callback+hash_equals) + Traefik deny router (PathPrefix(/wp-json/garden)→ 403). Belt #1 alone would suffice, but belt #2 protects against env-var leak scenarios. AuthResolverABC pattern. Future React Native app slots in aTokenResolver(bearer token) without changing endpoints; the protocol returns the sameAuthIdentityregardless of source.- 300s in-process TTL cache on the resolver. Keyed by
sha256(cookie_value), size-capped at 10k, positive-only. Cookie itself invalidates on WP logout (no manual cache busting needed). Per-process is fine — sessions don't need to be coherent across replicas at the auth layer. - Same-host path-prefix routing.
pbs.herbylab.dev/api/interactions/...andpbs.herbylab.dev/the-garden/...route to member-garden; the browser auto-sends the WP cookie. Avoids the subdomain trap. - Integration branch as the deploy target.
integration/garden-phase1-deploymerges in-flight feature branches before they're merged to main, so the test VM can run them while Travis reviews PRs at his pace. Trade-off: integration branch needs to be kept current as new features land.