Feed
Overview
The CommonPlace feed is designed as a deliberate counterpoint to the infinite-scroll, algorithmically-ranked feeds found on mainstream social platforms. Instead of continuously loading content to maximize time-on-site, the feed operates on a "session" model: when a user opens their feed, they see a fixed batch of new posts (since their last check-in) and can optionally reveal previous posts. There is no automatic loading of more content.
The feed supports three pacing modes -- slow (6-hour refresh cooldown), normal (1-hour cooldown), and live (no restriction). These modes control how frequently a user can check for new posts, enforcing intentional engagement rather than compulsive refreshing. The feed also provides an explicit "I'm done for now" button that commits the session and shows a calm farewell message.
Content filtering happens client-side with support for filtering by all posts, friends only, or specific circles. Posts from "stepped back" friends (a de-escalation feature) are automatically hidden. The feed uses a masonry grid layout and skeleton loaders for the initial load.
Relevant Invariants
- Invariant #2: "Calm Is the Default State" -- No urgency cues, badges, or auto-refresh. The feed refreshes only on explicit user action.
- Invariant #8: "Feed Is a View, Not a Goal" -- The feed is not a reward surface. Users see what is available and can leave without guilt.
- Invariant #12: "Slowness Is Baked In" -- Pacing intervals enforce deliberate engagement. The "Check for new posts" button respects cooldowns.
- Invariant #13: "Absence Is First-Class" -- An empty feed displays "Nothing here yet. Check back later or explore other areas." rather than suggesting the user is missing out.
User Experience
User Flow
- On load, the feed fetches the user's
user_feed_stateto determine thelast_checked_attimestamp. - Posts created after
last_checked_atappear in the "new posts" section at the top. - A divider separates new posts from previous posts. The divider shows "Nothing new right now" when appropriate.
- Below the divider, a collapsible "Previous posts" section shows older posts with a toggle to expand.
- The user can click "I'm done for now" to commit their check-in (updating
last_checked_atto the current session cutoff). - The user can click "Check for new posts" to fetch posts created since the session started, subject to pacing cooldowns.
- A filter dropdown at the top allows switching between "All Posts", "Friends Only", or any of the user's circles.
Pacing Modes
| Mode | Refresh Interval | Description |
|---|---|---|
slow | 6 hours | Deep focus mode; check-ins are infrequent |
normal | 1 hour | Balanced engagement |
live | No restriction | Real-time subscription for new posts via Supabase Realtime |
Technical Implementation
Key Files
| File | Purpose |
|---|---|
src/components/Feed.jsx | Main feed component (1081 lines). Handles session management, post fetching, filtering, pacing, and rendering |
src/hooks/useCircleFeedState.js | Per-circle feed pacing hooks: useCircleFeedState, useCircleNewPostCount, useMarkCircleChecked, useAllCirclesNewPostCounts |
src/hooks/useShareToCircle.js | Share-to-circle modal integration within the feed |
src/hooks/useSteppedBack.js | Provides IDs of friends the user has "stepped back" from, used to filter feed |
Architecture
The feed maintains two separate post arrays:
newPosts: Posts created afterlast_checked_atand beforesessionCutoff(the timestamp when the feed was loaded).previousPosts: Posts created before or atlast_checked_at, paginated in batches of 20.
Both arrays are filtered client-side through filterPosts() which applies audience visibility rules (public, friends, circles) and the current filter selection.
Post Query Structure
Each post query selects related data in a single Supabase query:
profiles:user_id-- Author profilecomments-- Comment count and datapost_images-- Attached images sorted by positionpost_circles > circles-- Circle associationsshared_post:shared_post_id-- Embedded shared post data
Real-time (Live Mode)
In live mode, a Supabase Realtime subscription on the posts table listens for INSERT events. New posts are fetched with full relational data and appended to newPosts. The subscription is torn down when switching away from live mode.
Database Tables
| Table | Purpose |
|---|---|
user_feed_state | Stores last_checked_at, feed_mode, and last_refresh_at per user |
circle_feed_state | Per-user, per-circle "caught up" state with last_checked_at |
posts | All posts with content, intent, audience, editor_type, etc. |
saved_posts | Tracks which posts the current user has saved (for bookmark state in feed) |
shelves | User's organizational shelves for saved posts |
Circle Feed State Hooks
useCircleFeedState(circleId): Fetches feed state for a specific circle.useCircleNewPostCount(circleId): Callsget_circle_new_post_countRPC to get the count of new posts since last check.useMarkCircleChecked(): Callsmark_circle_checkedRPC to update the last-checked timestamp for a circle.useAllCirclesNewPostCounts(): Callsget_all_circles_new_post_countsRPC for the feed filter badge counts. Refreshes on tab visibility change (not polling).
Edge Cases
| Scenario | Behavior |
|---|---|
| No posts at all | Shows "Nothing here yet. Check back later or explore other areas." |
| Friends filter with no friends | Shows "No friends yet. When you add friends, their posts will appear here." |
| Refresh during cooldown | Button is disabled and shows remaining time (e.g., "Check again in 4h 23m") |
| Post created by current user | Immediately added to new posts section with highlight animation |
| Post deleted | Removed from both new and previous post arrays via handlePostDeleted |
| Stepped-back friend's posts | Filtered out client-side before rendering |
| Circle filter selected | Only shows posts with audience: 'circles' that are associated with the selected circle |
Related
- Posts -- Individual post rendering within the feed
- Intent System -- Per-author intent styles are fetched and applied in the feed
- Circles -- Circle-based filtering and per-circle feed state
- Saved & Shelves -- Save state is tracked per-post in the feed
Last updated: 2026-02-07