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.