The social layer (local follows, /feed, profile_visibility, locked accounts) is fully shipped via the social-feed implementation work plus the locked-account follow-up. Closing out the change. Pre-archive ticks: - 1.4: schema migrated on prod via cd-apps drizzle-kit push; column + table verified on the running DB. - 3.3: follower/following counts on /users/:username shipped in #310. - 7.1: cd-apps drizzle-kit push --force ran; verified post-deploy. - 7.2: smoke inputs verified (bruno is public on prod, has 17 public activities, /users/bruno returns 200). Live click-through is operator-discretion; the listSocialFeed query correctness is proven by integration tests. - 7.3 / 7.4: forward-pointers, not deliverables for this change. One task explicitly deferred: - 6.2: full activity-creation E2E for the /feed assertion. Equivalent coverage at the integration level + the e2e Follow-button + visibility tests; not worth wiring an e2e activity-creation helper just for this one path. Spec sync: + journal-landing: 1 added ~ public-profiles: 1 added, 1 modified + social-follows: new spec (5 added) Move to openspec/changes/archive/2026-04-25-social-feed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 KiB
Why
trails.cool has public activities and public profiles, but no way for users to build a network. A signed-in user's home today shows only their own activities; the visitor-facing home shows every instance's public activity indiscriminately. There's no "activities from people I care about" view, which is table stakes for a social outdoor journal and the differentiator that gives users a reason to sign up over just using the Planner.
This change ships the local social layer: follows between users on the same Journal instance and a personal /feed aggregating their public activities. ActivityPub federation (cross-instance follows, inbox/outbox dispatch, Fedify integration) is deliberately deferred to a follow-up change social-federation so this lands in days rather than weeks. The schema is forward-compatible: when federation arrives, remote follows slot in without migration.
What Changes
- Add an explicit
users.profile_visibilitysetting ('public' | 'private'). New accounts default to'private'(matches trails.cool's privacy-first content defaults); existing accounts are backfilled to'public'so deploy doesn't lock anyone down.privateis functionally Mastodon's "locked" account: profile renders a stub with a Request-to-follow button for non-followers; only accepted followers see content; follows land Pending until the owner approves. - Users SHALL be able to follow another local user (same instance) by clicking a Follow / Request-to-follow button on the user's profile page. Public targets auto-accept; private targets create a Pending row that the owner approves or rejects from
/follows/requests. - A signed-in user SHALL have a
/follows/requestspage listing all incoming Pending requests with Approve / Reject actions; the navbar shows a count badge linking to that page. - A signed-in user SHALL be able to view a social feed of public activities from the users they follow with an accepted relation, reverse-chronological, at a dedicated
/feedroute linked from the nav. - The logged-in home (
/) SHALL link prominently to the new social feed; personal dashboard content stays as-is for now. - Profile pages SHALL show follower and following counts (accepted-only) and a context-aware Follow control (Follow / Request to follow / Requested / Unfollow).
- Privacy: follower/following counts and lists are public per instance. Only
publicactivities appear in the feed; pending follows don't contribute content. - Privacy manifest: documents the new
followsrelation as data the instance retains about who follows whom. - Forward-compatible schema:
followsis keyed by anactor_iri TEXTcolumn even though all rows are local now (local user IRIs look likehttps://{DOMAIN}/users/{username}). Whensocial-federationlands, remote IRIs go in the same column with no migration. - Out of scope (tracked as follow-ups):
- ActivityPub federation primitives —
social-federation. Goal there is "Mastodon can follow a trails account" inbound and "trails can follow other trails instances" outbound. - A unified notification UI. The follow-request count badge is a one-off; broader notifications (e.g., "your follow request was approved", "new public activity from people you follow") will be designed in a follow-up.
- ActivityPub federation primitives —
Capabilities
New Capabilities
social-follows: the follow/unfollow graph (local now, federated later) and the aggregated social activity feed surfaced at/feed.
Modified Capabilities
public-profiles: add aprofile_visibilitysetting on users, gate/users/:usernameon it, and add Follow / Unfollow button + follower and following counts to the profile page.journal-landing: add a prominent link from the signed-in home to the new/feedroute; personal dashboard content stays.
Impact
- Code: new
followstable (Drizzle + migration), newusers.profile_visibilitycolumn. Queries for follower/following lists, aggregated feed query, new routes (/feed,/users/:username/followers,/users/:username/following), profile-visibility setting in account settings. - API: new endpoints
POST /api/users/:username/follow,POST /api/users/:username/unfollow; existing public-profile and feed endpoints gain follower/following counts. - UI: Follow button + counts on
/users/:username; new/feedroute with card list (reuses the home-feed card component); nav item "Feed" visible to signed-in users; profile-visibility toggle in/settings/profile. - Federation: deferred to
social-federation. The schema's IRI key +users.profile_visibilityare placed now so the federation change is additive (no schema rewrite). - Dependencies: none new;
drizzle-kit pushhandles the schema migration. - Privacy manifest: new entry documenting the
followsrelation (which user follows whom on this instance, when). - Operational: the social feed query joins follows with activities; the existing index on
activities(created_at)plus a new(follower_id, created_at DESC)onfollowskeeps the join cheap to thousands of follows per user.