markdown · 3456 bytes Raw Blame History

Social Feed

S42 turns the S26 social primitives into a GitHub-like network surface: follow graph, signed-in Explore/Home feed, public Explore feed, and cached trending rankings.

Follow Graph

follows stores one follower user and exactly one target:

  • followee_user_id for user profiles.
  • followee_org_id for organization profiles.

The schema enforces target XOR, blocks user self-follows, cascades on deleted users/orgs, and uses partial unique indexes so follow/unfollow is idempotent. State changes go through internal/social and record audit rows when an audit recorder is supplied.

Follow actions emit public user-scoped domain_events:

  • followed_user, source_kind = "user", source_id = target_user_id
  • followed_org, source_kind = "org", source_id = target_org_id

The web layer exposes profile/org Follow buttons and follower/following tabs. Suspended actors are rejected by middleware/policy before mutation.

Feeds

Feeds read from domain_events; handlers never hand-roll visibility logic. Public feeds require domain_events.public = true, non-deleted actors, non-suspended actors, and a current public repo if the event is repo-scoped. This second repo visibility check is load-bearing: an event emitted while a repo was public must not leak after the repo becomes private.

After sign-in, the default destination is /explore. / remains the public build/landing page so the top-left shithub brand is always a way back to the instance/version stamp.

The signed-in /explore feed includes:

  • the viewer's own public activity,
  • public activity from followed users,
  • public activity from repos the viewer watches,
  • public activity from repos owned by followed orgs,
  • public org-scoped activity for followed orgs.

Anonymous Explore uses the global public feed. Both feeds page with a keyset cursor over (created_at, id).

Event Kinds

Current feed sources include:

  • repo_created
  • push
  • star / unstar
  • forked
  • issue_created, comments, close/reopen, assignment events
  • pr_opened and pull-request comment events
  • followed_user / followed_org

unstar events remain in domain_events for audit/product history, but the feed queries suppress them because GitHub does not surface unstars as activity feed stories.

The kind and source_kind columns remain text. New product surfaces can add events without a schema migration as long as their payload is small JSON and the public flag is set conservatively.

trending_snapshots stores denormalized rankings for day/week/month windows and two kinds:

  • repos
  • users

The trending:compute worker job captures all six snapshots. A job with an empty payload schedules its next run one hour later; pass {"schedule_next":false} for a one-off recompute. Explore reads the latest weekly snapshot and falls back to live computation before the first worker run.

The repo score weights recent public stars, forks, and unique push actors. The user score weights recent followers plus recent public event activity.

Operational Notes

Seed the recurring job once after deploy:

INSERT INTO jobs (kind, payload) VALUES ('trending:compute', '{}');
SELECT pg_notify('shithub_jobs', '');

The job is safe to re-run. Multiple recurring seeds produce multiple hourly refresh jobs, so operators should keep one scheduled chain per instance unless they intentionally want a shorter effective interval.

View source
1 # Social Feed
2
3 S42 turns the S26 social primitives into a GitHub-like network surface:
4 follow graph, signed-in Explore/Home feed, public Explore feed, and cached
5 trending rankings.
6
7 ## Follow Graph
8
9 `follows` stores one follower user and exactly one target:
10
11 - `followee_user_id` for user profiles.
12 - `followee_org_id` for organization profiles.
13
14 The schema enforces target XOR, blocks user self-follows, cascades on
15 deleted users/orgs, and uses partial unique indexes so follow/unfollow is
16 idempotent. State changes go through `internal/social` and record audit
17 rows when an audit recorder is supplied.
18
19 Follow actions emit public user-scoped `domain_events`:
20
21 - `followed_user`, `source_kind = "user"`, `source_id = target_user_id`
22 - `followed_org`, `source_kind = "org"`, `source_id = target_org_id`
23
24 The web layer exposes profile/org Follow buttons and follower/following
25 tabs. Suspended actors are rejected by middleware/policy before mutation.
26
27 ## Feeds
28
29 Feeds read from `domain_events`; handlers never hand-roll visibility
30 logic. Public feeds require `domain_events.public = true`, non-deleted
31 actors, non-suspended actors, and a current public repo if the event is
32 repo-scoped. This second repo visibility check is load-bearing: an event
33 emitted while a repo was public must not leak after the repo becomes
34 private.
35
36 After sign-in, the default destination is `/explore`. `/` remains the
37 public build/landing page so the top-left shithub brand is always a way
38 back to the instance/version stamp.
39
40 The signed-in `/explore` feed includes:
41
42 - the viewer's own public activity,
43 - public activity from followed users,
44 - public activity from repos the viewer watches,
45 - public activity from repos owned by followed orgs,
46 - public org-scoped activity for followed orgs.
47
48 Anonymous Explore uses the global public feed. Both feeds page with a
49 keyset cursor over `(created_at, id)`.
50
51 ## Event Kinds
52
53 Current feed sources include:
54
55 - `repo_created`
56 - `push`
57 - `star` / `unstar`
58 - `forked`
59 - `issue_created`, comments, close/reopen, assignment events
60 - `pr_opened` and pull-request comment events
61 - `followed_user` / `followed_org`
62
63 `unstar` events remain in `domain_events` for audit/product history, but
64 the feed queries suppress them because GitHub does not surface unstars as
65 activity feed stories.
66
67 The `kind` and `source_kind` columns remain text. New product surfaces
68 can add events without a schema migration as long as their payload is
69 small JSON and the public flag is set conservatively.
70
71 ## Trending
72
73 `trending_snapshots` stores denormalized rankings for day/week/month
74 windows and two kinds:
75
76 - `repos`
77 - `users`
78
79 The `trending:compute` worker job captures all six snapshots. A job with
80 an empty payload schedules its next run one hour later; pass
81 `{"schedule_next":false}` for a one-off recompute. Explore reads the
82 latest weekly snapshot and falls back to live computation before the
83 first worker run.
84
85 The repo score weights recent public stars, forks, and unique push
86 actors. The user score weights recent followers plus recent public event
87 activity.
88
89 ## Operational Notes
90
91 Seed the recurring job once after deploy:
92
93 ```sql
94 INSERT INTO jobs (kind, payload) VALUES ('trending:compute', '{}');
95 SELECT pg_notify('shithub_jobs', '');
96 ```
97
98 The job is safe to re-run. Multiple recurring seeds produce multiple
99 hourly refresh jobs, so operators should keep one scheduled chain per
100 instance unless they intentionally want a shorter effective interval.