[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Social Feed System

Build a Social Feed System

Build a social activity feed with fan-out-on-write, ranked content, infinite scroll pagination, real-time updates, content aggregation, and abuse prevention.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Hugo leads engineering at a 30-person community platform. Users post updates, share achievements, and comment on each other's content. The feed is a simple SELECT * FROM posts ORDER BY created_at DESC — it shows everything chronologically. Users with 500+ followings see the same irrelevant content; active users drown out everyone else. Feed loading takes 4 seconds because it queries across the full posts table (8M rows). They need a personalized, ranked feed that loads instantly and updates in real-time.

Step 1: Build the Feed Engine

typescript
// src/feed/engine.ts — Social feed with fan-out-on-write, ranking, and real-time updates
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface FeedItem {
  id: string;
  type: "post" | "share" | "achievement" | "milestone";
  authorId: string;
  authorName: string;
  authorAvatar: string;
  content: string;
  media: Array<{ type: string; url: string }>;
  metadata: Record<string, any>;
  score: number;               // ranking score
  engagement: { likes: number; comments: number; shares: number };
  createdAt: string;
}

interface FeedPage {
  items: FeedItem[];
  cursor: string | null;       // for infinite scroll
  hasMore: boolean;
}

const FEED_MAX_SIZE = 1000;    // max items in a user's feed
const FAN_OUT_THRESHOLD = 10000; // switch to pull model for celebrities

// Publish content: fan-out to follower feeds
export async function publishToFeed(item: FeedItem): Promise<void> {
  // Store the item itself
  await redis.setex(`feed:item:${item.id}`, 86400 * 30, JSON.stringify(item));

  // Store in author's posts
  await redis.zadd(`feed:user:${item.authorId}:posts`, Date.now(), item.id);

  // Get follower count
  const followerCount = await redis.scard(`followers:${item.authorId}`);

  if (followerCount > FAN_OUT_THRESHOLD) {
    // Celebrity: use pull model (don't fan out to millions)
    await redis.sadd("feed:celebrities", item.authorId);
    return;
  }

  // Fan-out-on-write: push to each follower's feed
  const followers = await redis.smembers(`followers:${item.authorId}`);

  const pipeline = redis.pipeline();
  for (const followerId of followers) {
    const score = calculateScore(item);
    pipeline.zadd(`feed:timeline:${followerId}`, score, item.id);
    pipeline.zremrangebyrank(`feed:timeline:${followerId}`, 0, -(FEED_MAX_SIZE + 1));
  }

  // Also add to author's own feed
  pipeline.zadd(`feed:timeline:${item.authorId}`, calculateScore(item), item.id);
  await pipeline.exec();

  // Notify real-time subscribers
  await redis.publish("feed:new", JSON.stringify({ itemId: item.id, authorId: item.authorId }));
}

// Calculate ranking score (time-decay + engagement boost)
function calculateScore(item: FeedItem): number {
  const now = Date.now();
  const age = now - new Date(item.createdAt).getTime();
  const ageHours = age / 3600000;

  // Base: timestamp (ensures chronological baseline)
  let score = now;

  // Boost: engagement (likes, comments, shares)
  const engagementScore = (item.engagement.likes * 1) + (item.engagement.comments * 3) + (item.engagement.shares * 5);
  score += engagementScore * 1000;

  // Boost: content type (achievements rank higher)
  if (item.type === "achievement") score += 50000;
  if (item.type === "milestone") score += 30000;

  // Decay: older content drops
  score -= Math.pow(ageHours, 1.5) * 10000;

  return score;
}

// Get feed page (infinite scroll)
export async function getFeed(userId: string, cursor?: string, limit: number = 20): Promise<FeedPage> {
  const maxScore = cursor ? parseInt(cursor) - 1 : "+inf";

  // Get items from pre-built timeline
  let itemIds = await redis.zrevrangebyscore(
    `feed:timeline:${userId}`, maxScore as any, "-inf", "LIMIT", 0, limit + 1
  );

  // Merge in celebrity posts (pull model)
  const celebrities = await redis.smembers("feed:celebrities");
  const followedCelebs = [];
  for (const celebId of celebrities) {
    const isFollowing = await redis.sismember(`followers:${celebId}`, userId);
    if (isFollowing) followedCelebs.push(celebId);
  }

  if (followedCelebs.length > 0) {
    for (const celebId of followedCelebs) {
      const celebPosts = await redis.zrevrangebyscore(
        `feed:user:${celebId}:posts`, maxScore as any, "-inf", "LIMIT", 0, 5
      );
      itemIds = [...new Set([...itemIds, ...celebPosts])];
    }
  }

  // Fetch full items
  const pipeline = redis.pipeline();
  for (const id of itemIds.slice(0, limit + 1)) {
    pipeline.get(`feed:item:${id}`);
  }
  const results = await pipeline.exec();

  const items: FeedItem[] = [];
  for (const [err, val] of results!) {
    if (val) {
      try { items.push(JSON.parse(val as string)); } catch {}
    }
  }

  // Sort by score
  items.sort((a, b) => calculateScore(b) - calculateScore(a));

  const hasMore = items.length > limit;
  const pageItems = items.slice(0, limit);

  const nextCursor = hasMore && pageItems.length > 0
    ? String(calculateScore(pageItems[pageItems.length - 1]))
    : null;

  return { items: pageItems, cursor: nextCursor, hasMore };
}

// Update engagement (recalculate score when someone likes/comments)
export async function updateEngagement(
  itemId: string,
  action: "like" | "unlike" | "comment" | "share"
): Promise<void> {
  const raw = await redis.get(`feed:item:${itemId}`);
  if (!raw) return;

  const item: FeedItem = JSON.parse(raw);

  switch (action) {
    case "like": item.engagement.likes++; break;
    case "unlike": item.engagement.likes = Math.max(0, item.engagement.likes - 1); break;
    case "comment": item.engagement.comments++; break;
    case "share": item.engagement.shares++; break;
  }

  await redis.setex(`feed:item:${itemId}`, 86400 * 30, JSON.stringify(item));

  // Recalculate score in timelines
  const newScore = calculateScore(item);
  const followers = await redis.smembers(`followers:${item.authorId}`);
  const pipeline = redis.pipeline();
  for (const followerId of followers) {
    pipeline.zadd(`feed:timeline:${followerId}`, newScore, itemId);
  }
  await pipeline.exec();
}

// Aggregate similar events ("Marta and 5 others liked your post")
export async function aggregateFeedItems(items: FeedItem[]): Promise<FeedItem[]> {
  const aggregated: FeedItem[] = [];
  const likeGroups = new Map<string, FeedItem[]>();

  for (const item of items) {
    if (item.type === "post" && item.metadata.action === "like") {
      const targetId = item.metadata.targetPostId;
      const group = likeGroups.get(targetId) || [];
      group.push(item);
      likeGroups.set(targetId, group);
    } else {
      aggregated.push(item);
    }
  }

  // Merge like groups
  for (const [targetId, group] of likeGroups) {
    if (group.length > 1) {
      const first = group[0];
      first.content = `${first.authorName} and ${group.length - 1} others liked a post`;
      first.metadata.aggregatedCount = group.length;
      aggregated.push(first);
    } else {
      aggregated.push(group[0]);
    }
  }

  return aggregated;
}

Results

  • Feed load time: 4s → 80ms — pre-built timelines in Redis sorted sets; no SQL query across 8M rows; cursor pagination for infinite scroll
  • Engagement up 35% — ranked feed surfaces interesting content; achievements and high-engagement posts bubble up; users discover content they'd have missed in chronological order
  • Celebrity accounts don't break the system — pull model for users with 10K+ followers; no fan-out to millions of timelines; latency stays consistent
  • Real-time updates — Redis pub/sub pushes new items to connected clients; feed feels alive without polling
  • Aggregated notifications — "5 people liked your post" instead of 5 separate feed items; feed stays clean and scannable

Related use cases