wiki-vault/Sources/Dev/pbs-membership-loe1-phase4-collections.md

9.6 KiB

created path project tags type
2026-07-24 Sources/Dev pbs-membership-loe1-phase4-collections
pbs
membership
loe1
garden
collections
flask
mysql
data-model
project-plan

PBS Membership LOE 1 Phase 4 — Collections

Constitution

n/a — phase within an existing LOE. Inherits the platform's locked architecture (WordPress owns identity, Garden owns features, pbs_garden DB, WP-cookie auth, Option D chrome).


Spec

Goal

Named, shareable groups of saved recipes. A member organizes their saves into meaningful collections; Jenny publishes collections members can subscribe to.

Problem

3c gives members one flat, save-date-ordered list. That doesn't scale past a few dozen saves and offers no way to group by occasion, season, or theme. Jenny already publishes recipe roundups as posts — collections are the member-side equivalent of something the audience already understands.

Why separate from meal planning (LOE 2)

Different verbs. A collection is a taxonomy — these recipes belong together. A meal plan is a schedule — this recipe on Tuesday. Collections have no time dimension; LOE 2's model is entirely time (meal_slots, calendars). Merging them means one inherits a model it doesn't use. They will want to talk to each other later ("add this collection to next week's plan"), which is an integration between two clean things — cheap. Untangling one merged thing is not.

Design constraint: collections must be handable to a meal planner later. Stable collection IDs, clean "list the recipes in this collection" read.

Out of scope

  • Meal-plan integration (LOE 2)
  • editor role (schema leaves room; only owner and subscriber built)
  • Drag-and-drop reordering — carried from the April sketch, unresolved, may not survive contact with reality

User journeys

Own: Member with saves creates "Weeknight Dinners," adds recipes to it, views it, removes one, deletes it.

Subscribe: Jenny publishes "Holiday Sides." Member subscribes. It appears alongside their own collections, marked as not theirs. Jenny adds a recipe next week; the member sees it.

Cascade: Member unsaves a recipe that sits in three of their collections. They're told which three before it goes.

Requirements

  • FR-001 — WHEN a member creates a collection, THE SYSTEM SHALL write the collection and an owner membership row in a single transaction.
  • FR-002 — WHEN a member views their collections, THE SYSTEM SHALL return all collections where a membership row exists for that member, with the role available to the UI.
  • FR-003 — WHEN a member adds a recipe to a collection they own, THE SYSTEM SHALL create a save for that member if one does not already exist.
  • FR-004 — WHEN a member unsaves a recipe, THE SYSTEM SHALL remove it from all collections that member owns.
  • FR-005 — WHEN an unsave would remove a recipe from one or more collections, THE SYSTEM SHALL name those collections in a confirmation before proceeding.
  • FR-006 — WHEN a member deletes a collection, THE SYSTEM SHALL leave their saves untouched.
  • FR-007 — THE SYSTEM SHALL permit a recipe to belong to any number of a member's collections.
  • FR-008 — THE SYSTEM SHALL permit a collection to be empty.
  • FR-009 — WHEN a member subscribes to a collection, THE SYSTEM SHALL write a subscriber membership row without copying the collection or its items.
  • FR-010 — WHEN an owner modifies a collection, THE SYSTEM SHALL reflect that change for all subscribers.
  • FR-011 — WHEN a member without an owner role views a collection, THE SYSTEM SHALL present it read-only.
  • FR-012 — THE SYSTEM SHALL permit more than one owner membership row per collection.

Success criteria

All FRs observable in-browser. A member can organize saves into overlapping collections, and a Jenny-published collection reaches subscribers with edits propagating.

Edge cases

  • Recipe already saved when added to a collection → no duplicate save
  • Recipe absent from WordPress → inherits 3c's skip-and-log
  • Last owner leaves a collection with subscribers → unresolved (see Unresolved questions)
  • Subscriber unsubscribes → membership row removed; collection and other members untouched

Plan

Decisions

Decision Chosen Rejected Reversibility
Relationship to LOE 2 Separate feature Collections as part of meal planning — different verbs, forces an unused time model Two-way, but merging later is expensive
Item pointer collection_items → recipe post ID Point at user_interactions row — collides with sharing (would reference another user's rows or manufacture phantom saves); sharing requires dereferencing to the recipe anyway One-way once data exists
Item polymorphism item_type + item_id, only recipe used Recipe-only column — cheap to leave room, no cost now Two-way
Collections per recipe Many One (folder model) — forces a false choice; roundups are genuinely multi-category Two-way
Control model collection_members rows with role Single user_id owner column + separate subscriptions table — can't express multiple owners; garden is intended as community-oriented One-way in practice
Sharing mechanism Subscribe — one collection, edits propagate Copy — cheaper and satisfies the invariant naturally, but Jenny's later edits never reach members Two-way
Save/collection coupling Adding to an owned collection creates the save Independent — would break the must-be-saved invariant Two-way
Unsave behavior Cascades out of the member's owned collections Independent — leaves collections holding unsaved recipes, contradicting the invariant Two-way
Collection deletion Does not touch saves Cascade to saves — destructive and surprising Two-way

Data model

Two new tables in pbs_garden:

collections — id, name, timestamps. No owner column; ownership is a membership row.

collection_members — collection_id, user_id, role (owner | subscriber; editor reserved), timestamp. Unique on (collection_id, user_id).

collection_items — collection_id, item_type (recipe), item_id (WP post ID), timestamp. Unique on (collection_id, item_type, item_id).

No changes to user_interactions. The must-be-saved invariant is enforced in application logic, not schema.

Known consequence: with ownership in membership rows, nothing at the schema level guarantees a collection has an owner at all. Creation transaction handles the happy path; the owner-departure case is unresolved below.

Interfaces / contracts

JSON endpoints under the garden blueprint, inheriting 3c's CSRF pattern for all writes — token in page, sent as header, validated server-side. Reads need no token.

Writes: create collection, rename, delete, add item, remove item, subscribe, unsubscribe.

Reads: list my collections (owned + subscribed, role included), list items in a collection.

Deployment

Inherits 3a — existing pbs-garden container, Traefik routing. New tables require a migration.

Tests

Deferred to the end-of-LOE hardening pass, consistent with 3c. Two worth writing then: the cascade (unsave removes from all owned collections) and the creation transaction (collection without owner row must not persist).

NFRs

The "which collections is this recipe in" read on the flat saved list is an extra query per row if done naively — batch it. Otherwise n/a at this scale.


Tasks

Phase 4 scope

  • Migration for three tables
  • Create/rename/delete collection + owner membership transaction (FR-001, FR-006)
  • Add/remove item with save-creation (FR-003, FR-007, FR-008)
  • Collections list read with role (FR-002, FR-011)
  • Unsave cascade + confirmation (FR-004, FR-005)
  • Subscribe/unsubscribe (FR-009, FR-010, FR-012)

Iterate-shaped

Collection UI and the cascade confirmation wording — converge in-browser.

Human-required handoffs

  • Jenny's collection cards may want their own seed-packet treatment; asset decision
  • Jenny defines the first published collections

Cross-cutting

Codebase assumptions

3c shipped: the saved-recipes page, the JS-write-plus-CSRF pattern, hard delete on unsave with optimistic undo, WP featured image as canonical thumbnail with fallback, WP-absence skip-and-log.

Unresolved questions

Must-resolve before code:

  • Does the must-be-saved invariant apply to subscribed collections? Either propagation auto-saves into every subscriber's account when an owner adds a recipe, or the invariant is scoped to owned collections and subscribed ones are view-only lists. Travis is thinking on it. The read-only reading is consistent with FR-011. Note: this decision impacts the relational data structure — it is not purely an application-logic or UX choice.

Defer and tag:

  • Last owner leaving a collection that has subscribers
  • Whether the flat saved list displays collection membership

Soft — revisit in testing:

  • The must-be-saved invariant generally. If it proves brittle, drop it and let collections hold unsaved recipes.

Pre-mortem

Most likely failure is the invariant. It's enforced in application logic across four separate paths — add item, unsave, subscribe, and collection delete — and a miss in any one leaves collections holding unsaved recipes. Second: the creation transaction — a collection whose owner row failed to write is invisible and unrecoverable through the UI. Third: the cascade confirmation is the first destructive multi-object action in the garden, and getting it wrong means silently destroying curation a member spent real effort on.