◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-03-31上級

Rork Max ソーシャルアプリ機能 構築ガイド — タイムライン・フォロー・リアルタイムフィード

Rork Max + Supabase でフォロー/タイムライン/いいね・コメント/リアルタイム同期/プッシュ通知を本番品質で組み上げる手順を、静かなユーティリティアプリを長く運用してきた個人開発の視点からまとめたガイドです。

rork-max41social-apptimelinefollowrealtimesupabase3feed

✦ プレミアム記事

個人開発で壁紙系・癒し系・引き寄せ系といった、いわゆる「静かな」ユーティリティアプリを長く育ててきました。どれも一つのことを丁寧にこなす単機能ツールでしたが、ここ数年は同じユーザーが「他の人の投稿を見たい」「お気に入りを誰かと共有したい」と言ってくれる場面が増えてきました。ソーシャル機能を後から載せるべきかどうか、実際の運用で何度も迷ってきたテーマです。

ソーシャル機能は、最初の数千 DAU までは「あるとちょっと嬉しい付加機能」で済みます。けれど、DAU が万を超え、リテンションを 7 日・30 日のスケールで見るようになった瞬間、データベースの形・タイムラインの並べ方・通知の出し方が、そのままユーザーの定着率に直結し始めます。私自身、ある壁紙アプリで「フォロー外の人気投稿をタイムラインに混ぜる」設定にしてしまい、Day7 リテンションを 41% から 28% まで落としたことがあります。そこから半年かけて元に戻した過程で気づいたことを、このガイドには織り込みました。

このガイドは、Rork Max と Supabase で「壊れにくい」ソーシャルアプリ機能を組み上げるための実装手順書です。フォロー/フォロワー、タイムラインフィード、いいね・コメント、リアルタイム更新、プッシュ通知までを一気通貫で扱います。読み終わったときに、自分のアプリのどこから着手すれば良いかが具体的に見える状態を目指しました。

取り組みの背景

ソーシャル機能は「ユーザー同士の接続をどう保つか」と「タイムラインで何を見せるか」の二点に集約されます。Twitter が爆発的に伸びたのもフォローという接続モデル、Instagram が Facebook を抜いたのもフォトを中心にした純粋なフォローフィードがあったからです。

ただし、これらの巨大サービスを真似しようとすると、ほぼ確実に「全部入りで動きはするが、誰のフィードも汚い」という状態に陥ります。私が壁紙アプリで実際に経験したのも同じパターンでした。データベース設計・タイムラインの並び・通知の出し方、それぞれで「やらない判断」を最初に決めるのが、結果として一番ユーザーに親切な作りになります。私はこの「全部入りの誘惑」を、実装に入る前の設計段階で意識的に断ち切るようにしています。一度コードに落ちてしまうと、機能を足すより外すほうが何倍も難しくなるからです。

以降の章では、データベース・フォロー・タイムライン・楽観的更新・リアルタイム・無限スクロール・通知の順に、Supabase をバックエンドにした実装パターンを 1 つずつ作っていきます。


データベース設計 — Supabaseスキーマの全体像

ソーシャル機能の土台となるデータベーススキーマ設計が最も重要です。不適切な設計は、後々のパフォーマンス低下につながります。

テーブル構成

profiles
├── id (UUID, PK)
├── user_id (UUID, FK → auth.users)
├── username (TEXT, UNIQUE)
├── display_name (TEXT)
├── avatar_url (TEXT)
├── bio (TEXT)
├── followers_count (INT)
├── following_count (INT)
├── created_at (TIMESTAMP)
└── updated_at (TIMESTAMP)

follows
├── id (UUID, PK)
├── follower_id (UUID, FK → profiles)
├── following_id (UUID, FK → profiles)
├── created_at (TIMESTAMP)
└── UNIQUE(follower_id, following_id)

posts
├── id (UUID, PK)
├── author_id (UUID, FK → profiles)
├── content (TEXT)
├── image_urls (TEXT[])
├── likes_count (INT)
├── comments_count (INT)
├── created_at (TIMESTAMP)
├── updated_at (TIMESTAMP)
└── deleted_at (TIMESTAMP, soft delete)

likes
├── id (UUID, PK)
├── post_id (UUID, FK → posts)
├── user_id (UUID, FK → profiles)
├── created_at (TIMESTAMP)
└── UNIQUE(post_id, user_id)

comments
├── id (UUID, PK)
├── post_id (UUID, FK → posts)
├── author_id (UUID, FK → profiles)
├── parent_comment_id (UUID, FK → comments, nullable)
├── content (TEXT)
├── likes_count (INT)
├── created_at (TIMESTAMP)
├── updated_at (TIMESTAMP)
└── deleted_at (TIMESTAMP)

notifications
├── id (UUID, PK)
├── user_id (UUID, FK → profiles)
├── type (TEXT: 'like', 'comment', 'follow')
├── triggered_by_id (UUID, FK → profiles)
├── post_id (UUID, FK → posts, nullable)
├── comment_id (UUID, FK → comments, nullable)
├── is_read (BOOLEAN)
├── created_at (TIMESTAMP)
└── INDEX(user_id, is_read)

Supabase での実装

Supabase SQL エディタで以下を実行します:

-- profiles テーブル
CREATE TABLE profiles (
  id UUID PRIMARY KEY DEFAULT auth.uid(),
  user_id UUID REFERENCES auth.users NOT NULL,
  username TEXT UNIQUE NOT NULL,
  display_name TEXT,
  avatar_url TEXT,
  bio TEXT,
  followers_count INT DEFAULT 0,
  following_count INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now()
);
 
-- RLS ポリシー
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles are viewable by all"
  ON profiles FOR SELECT USING (true);
CREATE POLICY "Users can update their own profile"
  ON profiles FOR UPDATE USING (auth.uid() = id);
 
-- follows テーブル
CREATE TABLE follows (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  follower_id UUID REFERENCES profiles NOT NULL,
  following_id UUID REFERENCES profiles NOT NULL,
  created_at TIMESTAMP DEFAULT now(),
  UNIQUE(follower_id, following_id),
  CHECK (follower_id != following_id)
);
 
ALTER TABLE follows ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public follows are viewable"
  ON follows FOR SELECT USING (true);
CREATE POLICY "Users can manage their follows"
  ON follows FOR INSERT WITH CHECK (auth.uid() = follower_id);
CREATE POLICY "Users can delete their follows"
  ON follows FOR DELETE USING (auth.uid() = follower_id);
 
-- posts テーブル
CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  author_id UUID REFERENCES profiles NOT NULL,
  content TEXT NOT NULL,
  image_urls TEXT[],
  likes_count INT DEFAULT 0,
  comments_count INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now(),
  deleted_at TIMESTAMP
);
 
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public posts are viewable" ON posts
  FOR SELECT USING (deleted_at IS NULL);
CREATE POLICY "Users can create posts" ON posts
  FOR INSERT WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update own posts" ON posts
  FOR UPDATE USING (auth.uid() = author_id);
 
-- likes テーブル
CREATE TABLE likes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  post_id UUID REFERENCES posts NOT NULL,
  user_id UUID REFERENCES profiles NOT NULL,
  created_at TIMESTAMP DEFAULT now(),
  UNIQUE(post_id, user_id)
);
 
ALTER TABLE likes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Likes are viewable" ON likes FOR SELECT USING (true);
CREATE POLICY "Users can like posts" ON likes
  FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can unlike" ON likes
  FOR DELETE USING (auth.uid() = user_id);
 
-- comments テーブル
CREATE TABLE comments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  post_id UUID REFERENCES posts NOT NULL,
  author_id UUID REFERENCES profiles NOT NULL,
  parent_comment_id UUID REFERENCES comments,
  content TEXT NOT NULL,
  likes_count INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now(),
  deleted_at TIMESTAMP
);
 
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Comments are viewable" ON comments
  FOR SELECT USING (deleted_at IS NULL);
CREATE POLICY "Users can comment" ON comments
  FOR INSERT WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update own comments" ON comments
  FOR UPDATE USING (auth.uid() = author_id);
 
-- notifications テーブル
CREATE TABLE notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES profiles NOT NULL,
  type TEXT CHECK (type IN ('like', 'comment', 'follow')),
  triggered_by_id UUID REFERENCES profiles NOT NULL,
  post_id UUID REFERENCES posts,
  comment_id UUID REFERENCES comments,
  is_read BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT now()
);
 
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own notifications" ON notifications
  FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can mark notifications as read" ON notifications
  FOR UPDATE USING (auth.uid() = user_id);
 
-- インデックス作成
CREATE INDEX idx_profiles_username ON profiles(username);
CREATE INDEX idx_follows_follower_id ON follows(follower_id);
CREATE INDEX idx_follows_following_id ON follows(following_id);
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_likes_post_id ON likes(post_id);
CREATE INDEX idx_likes_user_id ON likes(user_id);
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_comments_author_id ON comments(author_id);
CREATE INDEX idx_notifications_user_id ON notifications(user_id, is_read);

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦本番運用で見えた、ソーシャル機能を「壊さずに伸ばす」設計判断(フォロー外混入・フォロワーキャッシュ・LWWで十分な領域)
✦Supabase Realtime が1万接続超で詰まる前に切り替える段階的フォールバック戦略と、リトライ・無効通知の抑制コード
✦プッシュ通知の「効く通知」だけを残すセグメンテーション基準(許可率・OPEN率の閾値と、誤送信時の収束手順)
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-14
起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、読み込みより先に過ぎていたためです
Android 版の起動広告が、アプリを完全に終了してから開く回だけ一度も出ていませんでした。「前面に戻った」合図を読み込みの完了と切り離し、順番ごとにテストできる形へ組み直した記録です。
⬡ 開発ツール2026-10-13
リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる
ソースに鍵がなくても、出来上がったアプリの中に鍵が残ることがあります。Expo SDK 57 で実際に expo export を実行し、バンドルの中身を grep と 60 行ほどの Node スクリプトで調べて、見つかる鍵と見つからない鍵の違いを確かめました。