Skip to main content

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

  1. On load, the feed fetches the user's user_feed_state to determine the last_checked_at timestamp.
  2. Posts created after last_checked_at appear in the "new posts" section at the top.
  3. A divider separates new posts from previous posts. The divider shows "Nothing new right now" when appropriate.
  4. Below the divider, a collapsible "Previous posts" section shows older posts with a toggle to expand.
  5. The user can click "I'm done for now" to commit their check-in (updating last_checked_at to the current session cutoff).
  6. The user can click "Check for new posts" to fetch posts created since the session started, subject to pacing cooldowns.
  7. A filter dropdown at the top allows switching between "All Posts", "Friends Only", or any of the user's circles.

Pacing Modes

ModeRefresh IntervalDescription
slow6 hoursDeep focus mode; check-ins are infrequent
normal1 hourBalanced engagement
liveNo restrictionReal-time subscription for new posts via Supabase Realtime

Technical Implementation

Key Files

FilePurpose
src/components/Feed.jsxMain feed component (1081 lines). Handles session management, post fetching, filtering, pacing, and rendering
src/hooks/useCircleFeedState.jsPer-circle feed pacing hooks: useCircleFeedState, useCircleNewPostCount, useMarkCircleChecked, useAllCirclesNewPostCounts
src/hooks/useShareToCircle.jsShare-to-circle modal integration within the feed
src/hooks/useSteppedBack.jsProvides IDs of friends the user has "stepped back" from, used to filter feed

Architecture

The feed maintains two separate post arrays:

  • newPosts: Posts created after last_checked_at and before sessionCutoff (the timestamp when the feed was loaded).
  • previousPosts: Posts created before or at last_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 profile
  • comments -- Comment count and data
  • post_images -- Attached images sorted by position
  • post_circles > circles -- Circle associations
  • shared_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

TablePurpose
user_feed_stateStores last_checked_at, feed_mode, and last_refresh_at per user
circle_feed_statePer-user, per-circle "caught up" state with last_checked_at
postsAll posts with content, intent, audience, editor_type, etc.
saved_postsTracks which posts the current user has saved (for bookmark state in feed)
shelvesUser's organizational shelves for saved posts

Circle Feed State Hooks

  • useCircleFeedState(circleId): Fetches feed state for a specific circle.
  • useCircleNewPostCount(circleId): Calls get_circle_new_post_count RPC to get the count of new posts since last check.
  • useMarkCircleChecked(): Calls mark_circle_checked RPC to update the last-checked timestamp for a circle.
  • useAllCirclesNewPostCounts(): Calls get_all_circles_new_post_counts RPC for the feed filter badge counts. Refreshes on tab visibility change (not polling).

Edge Cases

ScenarioBehavior
No posts at allShows "Nothing here yet. Check back later or explore other areas."
Friends filter with no friendsShows "No friends yet. When you add friends, their posts will appear here."
Refresh during cooldownButton is disabled and shows remaining time (e.g., "Check again in 4h 23m")
Post created by current userImmediately added to new posts section with highlight animation
Post deletedRemoved from both new and previous post arrays via handlePostDeleted
Stepped-back friend's postsFiltered out client-side before rendering
Circle filter selectedOnly shows posts with audience: 'circles' that are associated with the selected circle
  • 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