RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/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-07-13
Detox と Maestro の E2E が全部緑なのに本番でクラッシュが出ていたとき — 自動リトライが握りつぶした不安定テストをフレーク率で炙り出す運用メモ
E2Eが全部緑なのに本番クラッシュが出る。原因は自動リトライがフレークを握りつぶしていたこと。テスト単位でフレーク率を計測し、隔離とリトライ予算で本物の失敗だけを残す運用メモです。
開発ツール2026-07-01
Rork Max のクラウドコンパイル — Mac を持たずにネイティブアプリを仕上げる
Rork Max のクラウドコンパイルで、ローカル Mac なしに iOS/iPadOS のネイティブアプリをビルド・実機テスト・公開する流れを、失敗時の切り分けと実機 Mac を残す判断基準まで含めて実務目線でまとめます。
開発ツール2026-06-17
誕生日を集めずに年齢を確認する — Declared Age Range API を Rork アプリに組み込む
iOS 26 の Declared Age Range API を使い、生年月日を保存せずに年齢層だけを受け取る実装方法。Rork Max のネイティブ Swift と、標準 Rork(Expo)のネイティブモジュール越しの呼び出しを、責務の線引きとともに整理します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →