Saved Posts & Shelves
Overview
The Saved Posts system allows users to bookmark posts for later reference, organizing them into named shelves. This provides a personal, private collection mechanism that respects the platform's philosophy: saving is a quiet, individual action with no public signal (no "X people saved this" counter, no trending bookmarks).
Each user has at least one shelf (a "Default" shelf created automatically on first save). Users can create additional shelves for organizing saved posts by topic, mood, or any personal taxonomy. Posts can be saved with an optional personal note, and shelves can be renamed or deleted (with posts moved to the default shelf on deletion).
The save interaction has two paths: a quick save that instantly saves to the default shelf, and a detailed save that opens a popover for shelf selection and note addition. On the Saved page, users can browse all saved posts or filter by shelf, add/edit/delete notes, and unsave posts.
Relevant Invariants
- Invariant #6: "No Public Comparative Metrics" -- Save counts are never shown to anyone. Saving is entirely private.
- Invariant #3: "Silence Is a Valid Outcome" -- Having no saved posts is presented as a calm empty state, not a deficiency.
- Invariant #14: "Privacy Is Infrastructure" -- Saved posts and notes are per-user and protected by RLS policies.
User Experience
Saving a Post
- On any post card, the bookmark icon in the action bar provides two interaction zones:
- Click the bookmark: Quick save to default shelf (or unsave if already saved).
- Click the dropdown arrow: Opens the SavePopover for detailed save options.
- The SavePopover (rendered as a portal) shows:
- Shelf selector dropdown with all user shelves and a "New shelf" inline creation option.
- Expandable note input for adding a personal note.
- Save and Cancel buttons.
- Saved posts show a filled bookmark icon (
BookmarkCheck).
Browsing Saved Posts
- Navigate to the Saved page (accessible from navigation).
- Shelf tabs at the top allow filtering: "All" or any specific shelf.
- Posts render in a masonry grid with the same post card component used in the feed, plus any attached note below the card.
- Notes can be added, edited, or deleted inline.
- A "Manage Shelves" button opens a modal for renaming, deleting, and creating shelves.
Managing Shelves
- Click the settings icon next to the shelf tabs to open the shelf manager modal.
- Each shelf shows its name with rename and delete buttons.
- The default shelf cannot be deleted (marked with an asterisk).
- Deleting a shelf moves its saved posts to the default shelf.
- New shelves can be created with a name input at the bottom of the modal.
Technical Implementation
Key Files
| File | Purpose |
|---|---|
src/components/SavedPage.jsx | Main saved posts page (524 lines). Handles shelf tabs, shelf management modal, saved post listing with pagination, and note editing |
src/components/post-parts/SavePopover.jsx | Portal-based popover for detailed save options: shelf selection, inline shelf creation, note input (175 lines) |
src/components/Post.jsx | Contains save/unsave logic, popover positioning, shelf management, and note editing within post cards |
Save Flow (in Post.jsx)
Quick Save (handleQuickSave):
- If already saved: delete from
saved_posts, update local state. - If not saved: ensure a default shelf exists (
ensureDefaultShelf), insert intosaved_postswith default shelf and no note.
Detailed Save (handleShowSaveOptions + handleConfirmSave):
- Opens the SavePopover positioned relative to the save button.
- User selects a shelf and optionally adds a note.
- On confirm: inserts into
saved_postswith selected shelf_id and note.
SavePopover positioning:
- Calculated based on save button's bounding rect.
- Checks space below vs. above viewport to decide popover direction.
- Rendered via
createPortaltodocument.bodyto escape overflow constraints.
SavedPage Architecture
The SavedPage fetches saved posts via a Supabase query that joins saved_posts with posts, profiles, and post_circles. It maintains:
savedPosts-- Array of post objectssavedPostsData-- Map ofpostId -> { note, shelf_id, saved_at }for the saved metadatauserShelves-- Array of shelf objectsauthorIntentStyles-- Per-author intent styles for rendering post cards
Pagination uses PREVIOUS_POSTS_PAGE_SIZE (20 per page) with a "Load more" button.
Note Editing (in Post.jsx)
On the saved posts page, posts render with note management:
- Add note: Button appears when
hideSaveButtonis true and no note exists. - Edit note: Inline input with Enter to save, Escape to cancel.
- Delete note: Confirmation prompt before clearing.
Database Tables
| Table | Purpose |
|---|---|
saved_posts | Saves: user_id, post_id, shelf_id, note (text), saved_at (timestamp) |
shelves | User shelves: user_id, name, emoji, position, is_default (boolean) |
Shelf Defaults
When a user first saves a post, ensureDefaultShelf checks for existing shelves. If none exist, it creates a "Default" shelf with emoji "pushpin", position 0, and is_default: true.
Edge Cases
| Scenario | Behavior |
|---|---|
| First-ever save | Default shelf auto-created with name "Default" and is_default flag |
| Save same post twice | Database unique constraint prevents duplicates |
| Delete a shelf | Posts moved to default shelf before deletion; if viewing deleted shelf, switches to "All" |
| Delete default shelf | Not allowed; delete button hidden for default shelves |
| Shelf name too long | Capped at 30 characters (MAX_SHELF_NAME_LENGTH) in both shelf manager and inline creation |
| Empty saved page | Shows "Nothing saved yet. Tap the bookmark on any post to save it here." with bookmark icon |
| Empty shelf | Shows "No posts in this shelf yet. Save posts to [shelf name] to see them here." |
| Escape key pressed in save popover | Popover closes, state resets |
| Click outside save popover | Popover closes via overlay click handler |
Related
- Posts -- Save button is part of the post action bar
- Feed -- Saved state is tracked per-post in the feed for bookmark icon rendering
Last updated: 2026-02-07