wiki-vault/Sources/Dev/2026-06-19-pbs-membership-phases-1-2-3a-deploy.md

12 KiB

created path project tags type
2026-06-19 Sources/Dev pbs-membership-platform-overview
pbs
membership
loe1
phase-1
phase-2
phase-3a
flask
wordpress
docker
ansible
garden
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:

  1. Phase 1 finish (member-garden functional tier + WP mu-plugin + DB apply + integration deploy) — extends thread #597
  2. Phase 2 (save-button WPCode snippet + member-garden public Traefik routing) — thread #600
  3. 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-garden with 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 header X-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/interactions and /the-garden to 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 (commits 1820943, edf13ea) — 34 tests, 99% coverage
    • feat/phase-3a-garden-skeleton off the above (commits 7094076, c2097fa) — 44 tests, 99% coverage
  • petal-power/wordpress-install:
    • feat/wp-mu-plugin-garden-auth (commit 116a72d)
    • integration/garden-phase1-deploy (tip ca2bd35) — merges fix-pbs-init-sql-syntax + mu-plugin branch + role bumps
    • feat/save-button-phase2 (tips 0ca8c1d89428c64f2d105) — 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-Auth was 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 AuthResolver ABC so a future React Native client can swap a token-based resolver in without touching endpoints.
  • pbs_garden DB applied to herbylab MySQL via the existing scoped-user SQL pattern; grants verified: full on pbs_garden.*, SELECT on wordpress.wp_posts, USAGE elsewhere.
  • Traefik routing strategy for member-garden — switched the originally-planned garden.<domain> subdomain to same-host path-prefixes (/api/interactions priority 110, /the-garden priority 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 from wordpress_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.html macro, before_request auth gate returns 302 (not 401) to UM login with redirect_to=. Static assets and /logout//login are 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 /api Traefik collision was a near-miss. pbs-api already owned Host && PathPrefix(/api) at priority 100. A literal /api member-garden router would have non-deterministically intercepted pbs-api's /api/recipes, /api/health, etc. Scoping to /api/interactions at 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_v2 doesn'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 adding container_compose_build: always for 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.php via docker exec wordpress php -r '...' and calling wp_generate_auth_cookie directly. 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 install to 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/interactions rely on ambient cookie auth with no nonce. Robust fix: add X-WP-Nonce validation 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 automatedflask db upgrade had to be run manually via docker cp of the migrations/ dir + container exec. Bake migrations into the image OR add an ansible task that runs them on deploy.
  • X-Forwarded-Proto not honored — Traefik terminates TLS; Flask sees http:// and builds the unauth 302 redirect_to with the wrong scheme. Fix with werkzeug.middleware.proxy_fix.ProxyFix in the app factory.
  • WP logout confirmation noncewp-login.php?action=logout without _wpnonce shows 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 herbylabpbs_recipes FK type mismatch (instagram_posts_ibfk_1 references pbs_recipes(post_id) with incompatible pbs_post_id column type → MySQL OperationalError 3780). Does NOT touch pbs_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) and feat/wp-mu-plugin-garden-auth (this session). Both currently included transitively via integration/garden-phase1-deploy and feat/save-button-phase2. Eventual merge order: fix-pbs-init-sql-syntax → main, then feat/wp-mu-plugin-garden-auth (rebased) → main, then integration/garden-phase1-deploy → main, then feat/save-button-phase2 → main.
  • member-garden branches awaiting merge to main: feat/phase-1-functional-tier first, then feat/phase-3a-garden-skeleton on top.
  • WP admin password on herbylab is ephemeral. Document or store in herbylab vault if persistence is wanted.
  • /login vs auth-gate URL consistency — Garden's convenience /login route hardcodes wp-login.php per the W5 brief, while the gate uses UM_LOGIN_URL. Unify on UM_LOGIN_URL in Phase 3b.

Open questions for Jenny

  • Final brand palette hex values (--garden-bg, --garden-fg, --garden-accent, --garden-muted, --garden-card, --pbs-brand for 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_interactions joined with wordpress.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.
  • AuthResolver ABC pattern. Future React Native app slots in a TokenResolver (bearer token) without changing endpoints; the protocol returns the same AuthIdentity regardless 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/... and pbs.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-deploy merges 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.