# How wishlist sharing actually works *May 15, 2026* A wishlist starts as the most private thing in the app: *my* list of products I might buy. Sharing it means deliberately opening a hole in that privacy — and the engineering question is how to open exactly the hole you intend, and no more. ## One public endpoint, on purpose Every wishlist route lives inside an `authenticate :user` block. Sharing adds *one* exception: ``` GET /shared_wishlist/:token # public, no login ``` That is the entire public surface. Not "wishlists are public if a flag is set" — one route, one purpose, easy to audit. The `:token` is a per-wishlist `share_token` carried by a `Wishlist::Sharable` concern, so the URL is unguessable and revocable without touching the wishlist's real ID. Keeping it to a single endpoint matters more than it looks. Every public route is something a security review has to reason about. One is reasonable. A sharing feature spread across "public if X, or Y, or the owner set Z" is not. ## Collaboration is a separate idea from sharing It's tempting to model sharing and collaboration as one spectrum. They aren't. *Sharing* is "anyone with the link can look." *Collaboration* is "these specific people can do these specific things." So they get separate tables: | Table | What it expresses | |-------|-------------------| | `wishlist_collaborators` | Named people, with a role: owner / editor / viewer | | `wishlist_follows` | "Notify me when this shared list changes" | | `gift_purchases` | "I bought this — hide it from the owner" | Each role maps to a Pundit policy. A viewer who was invited to *read* a list literally cannot reach the code path that mutates it — the policy refuses before the controller acts. Roles aren't a UI suggestion; they're an authorization boundary. ## The nicest bug we designed out `gift_purchases` exists for one reason: surprise. If a friend buys something off your wishlist, and your wishlist then cheerfully shows it as "purchased," the surprise is gone. So a gift purchase is recorded against the *buyer* and hidden from the *owner's* view of their own list. The owner sees their wishlist unchanged; the giver sees what they've handled. It's a small thing. But it's the kind of small thing that's invisible when it works and glaring when it doesn't — and it only works because "who bought this" was modelled as its own fact, not a boolean on the wishlist item. ## The takeaway Sharing features fail by accreting special cases. This one stayed small by keeping three ideas genuinely separate: a single public endpoint for *sharing*, role rows for *collaboration*, and a distinct record for *gift purchases*. Three small models beat one clever one.