Skip to main content

Best Boilerplates for Building Social Apps 2026

·StarterPick Team
social-appreal-timesupabasestreampusherboilerplate2026

TL;DR

Building a social app means solving the social graph, real-time feeds, and notifications — three fundamentally different problems. No single boilerplate handles all three well. The 2026 stack: Supabase (database + auth + realtime) for the core, Stream (feed infrastructure) or custom fan-out for production-scale feeds, and Pusher/Ably for real-time notifications. For MVP: Supabase realtime subscriptions handle everything. For scale: Stream Feed API.

Key Takeaways

  • Social graph: Postgres self-join (follows table) is the right approach for most apps
  • Feed: Fan-out on write (push to followers) vs fan-out on read (pull at load time)
  • Stream Feed API: Managed feed infrastructure for 10M+ activity items
  • Supabase Realtime: Good for MVP, channel-based subscriptions
  • Notifications: Supabase row-level subscriptions or Pusher for client push
  • Media uploads: Supabase Storage or Cloudflare R2

The Social App Data Model

-- Minimal social graph schema:
CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  display_name TEXT,
  bio TEXT,
  avatar_url TEXT,
  followers_count INTEGER DEFAULT 0,
  following_count INTEGER DEFAULT 0,
  posts_count INTEGER DEFAULT 0
);

CREATE TABLE follows (
  follower_id UUID REFERENCES profiles(id),
  following_id UUID REFERENCES profiles(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (follower_id, following_id)
);

CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES profiles(id) NOT NULL,
  content TEXT NOT NULL,
  media_urls TEXT[],
  likes_count INTEGER DEFAULT 0,
  replies_count INTEGER DEFAULT 0,
  reposts_count INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE likes (
  user_id UUID REFERENCES profiles(id),
  post_id UUID REFERENCES posts(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (user_id, post_id)
);

CREATE TABLE notifications (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES profiles(id) NOT NULL,
  actor_id UUID REFERENCES profiles(id) NOT NULL,
  type TEXT NOT NULL,  -- 'like', 'follow', 'reply', 'mention'
  post_id UUID REFERENCES posts(id),
  read BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- RLS policies:
ALTER TABLE follows ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can see all follows" ON follows FOR SELECT USING (true);
CREATE POLICY "Users can follow others" ON follows FOR INSERT WITH CHECK (auth.uid() = follower_id);
CREATE POLICY "Users can unfollow" ON follows FOR DELETE USING (auth.uid() = follower_id);

Follow/Unfollow with Supabase

// hooks/useSocial.ts:
import { useSupabaseClient } from '@supabase/auth-helpers-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useFollow(targetUserId: string) {
  const supabase = useSupabaseClient();
  const queryClient = useQueryClient();
  
  const follow = useMutation({
    mutationFn: async () => {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) throw new Error('Not authenticated');
      
      // Insert follow relationship:
      await supabase.from('follows').insert({
        follower_id: user.id,
        following_id: targetUserId,
      });
      
      // Create notification for the target user:
      await supabase.from('notifications').insert({
        user_id: targetUserId,
        actor_id: user.id,
        type: 'follow',
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['profile', targetUserId] });
      queryClient.invalidateQueries({ queryKey: ['is-following', targetUserId] });
    },
  });
  
  const unfollow = useMutation({
    mutationFn: async () => {
      const { data: { user } } = await supabase.auth.getUser();
      await supabase.from('follows')
        .delete()
        .eq('follower_id', user!.id)
        .eq('following_id', targetUserId);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['profile', targetUserId] });
    },
  });
  
  return { follow, unfollow };
}

Activity Feed

// Simple fan-out on read (good for MVPs):
// Fetch posts from people the current user follows

export async function getFeed(userId: string, page = 0) {
  const { data } = await supabase
    .from('posts')
    .select(`
      *,
      profiles (username, display_name, avatar_url),
      likes (user_id)
    `)
    .in('user_id', 
      // Subquery: get all users this person follows
      supabase.from('follows')
        .select('following_id')
        .eq('follower_id', userId)
    )
    .order('created_at', { ascending: false })
    .range(page * 20, (page + 1) * 20 - 1);
  
  return data;
}

// Or with a SQL function for performance:
// CREATE FUNCTION get_feed(p_user_id UUID, p_limit INT, p_offset INT)
// RETURNS SETOF posts AS $$
//   SELECT p.* FROM posts p
//   JOIN follows f ON p.user_id = f.following_id
//   WHERE f.follower_id = p_user_id
//   ORDER BY p.created_at DESC
//   LIMIT p_limit OFFSET p_offset;
// $$ LANGUAGE sql;

Real-Time Feed with Supabase

// components/Feed.tsx — real-time new posts:
'use client';
import { useEffect, useState } from 'react';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';

export function Feed({ userId }: { userId: string }) {
  const [newPosts, setNewPosts] = useState<Post[]>([]);
  const supabase = createClientComponentClient();
  
  useEffect(() => {
    // Subscribe to new posts from followed users:
    const channel = supabase
      .channel('feed-updates')
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'posts',
        },
        (payload) => {
          // Check if this is from someone we follow:
          checkIfFollowing(payload.new.user_id).then((isFollowing) => {
            if (isFollowing) {
              setNewPosts((prev) => [payload.new as Post, ...prev]);
            }
          });
        }
      )
      .subscribe();
    
    return () => {
      supabase.removeChannel(channel);
    };
  }, [userId]);
  
  return (
    <div>
      {newPosts.length > 0 && (
        <button className="w-full bg-blue-50 text-blue-600 py-2 rounded-lg mb-4"
          onClick={() => setNewPosts([])}>
          {newPosts.length} new post{newPosts.length > 1 ? 's' : ''}
        </button>
      )}
      <PostList userId={userId} prependPosts={newPosts} />
    </div>
  );
}

Stream Feed: Production Scale

npm install getstream
// For high-volume social apps (Twitter-scale):
import { connect } from 'getstream';

const client = connect(
  process.env.STREAM_API_KEY!,
  process.env.STREAM_API_SECRET!,
  process.env.STREAM_APP_ID!
);

// Add a post to the user's feed:
async function addPost(userId: string, post: { content: string; mediaUrls: string[] }) {
  const userFeed = client.feed('user', userId);
  
  await userFeed.addActivity({
    actor: `SU:${userId}`,
    verb: 'post',
    object: post.id,
    content: post.content,
    media_urls: post.mediaUrls,
    time: new Date().toISOString(),
  });
}

// Get timeline (aggregated feed of followed users):
async function getTimeline(userId: string, limit = 20, offset = 0) {
  const timeline = client.feed('timeline', userId);
  const { results } = await timeline.get({ limit, offset });
  return results;
}

// Follow/unfollow:
async function followUser(follower: string, target: string) {
  const followerTimeline = client.feed('timeline', follower);
  await followerTimeline.follow('user', target);
}

Decision Guide

Use Supabase Realtime for social if:
  → MVP / early stage (< 10K users)
  → Budget constrained
  → Team knows SQL well
  → Simple feed (chronological, not algorithmic)

Use Stream Feed if:
  → Need to scale to millions of activities
  → Need aggregated feeds (notifications digest)
  → Want fan-out handled automatically
  → Enterprise features (analytics, moderation)

Use Pusher/Ably for notifications if:
  → Need client-push (user gets ping, no polling)
  → Native mobile app notifications
  → Real-time typing indicators
  → Supabase realtime isn't granular enough

Avoid building from scratch if:
  → You're implementing feed fan-out yourself
  → Writing your own social graph traversal SQL
  → Building your own notification delivery system
  → Focus on your product, not infrastructure

Find social app boilerplates at StarterPick.

Comments