◉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-04-01上級

RorkでB2B SaaSアプリを構築する — マルチテナント設計・RBAC・Stripe組織課金の実装

RorkでB2B SaaSアプリを構築するための完全ガイド。マルチテナントアーキテクチャ、ロールベースアクセス制御(RBAC)、組織ごとのStripe課金、招待フローを実装する方法を詳細コード付きで解説します。

Rork585B2BSaaS3マルチテナントRBACStripe18Supabase37アプリ開発76上級7

✦ プレミアム記事

取り組みの背景 — なぜB2B SaaSが個人開発者にとって最もスケールしやすいか

個人開発やスモールチームでアプリをリリースする場合、B2C(消費者向け)よりもB2B(企業・チーム向け)のほうが収益化しやすいという現実があります。B2Bアプリは組織単位で契約されるため、1件の契約が継続する限り毎月安定した収益をもたらします。チームで使われるツールは「解約コストが高い」という性質もあり、チャーン(解約率)を低く保ちやすい点も魅力です。

しかしB2B SaaSを構築するには、B2Cとは異なる設計上の課題があります。

  • マルチテナント: 複数の組織(テナント)のデータを安全に分離して管理する
  • RBAC(ロールベースアクセス制御): 組織内のメンバーに役割を付与し、画面や機能へのアクセスを制限する
  • 組織課金: 個人ではなく組織単位でのStripe課金・メンバー数連動プラン
  • 招待フロー: 既存ユーザーが新しいメンバーを組織に招待する仕組み

ここで扱うのはRorkを使ってこれらすべてを実装する方法を、実際のコードとSupabase・Stripeのスキーマ設計を含めて丁寧に解説します。

対象読者は、すでにRorkの基本操作に慣れており、Supabaseを使ったデータベース連携の経験がある方を想定しています。Supabaseのリアルタイム機能と認証の活用ガイドも参照すると理解が深まります。


マルチテナントアーキテクチャの設計

テナント分離の3つの方式

マルチテナントには大きく3つの分離方式があります。

方式1: データベース分離型(Database per Tenant) テナントごとに別々のデータベースを用意する方式です。セキュリティが最も高い一方、コストとメンテナンス負荷が大きいため、大企業向けエンタープライズプランに限定するのが一般的です。

方式2: スキーマ分離型(Schema per Tenant) 1つのデータベース内でテナントごとに別スキーマを使います。PostgreSQLでは各テナントに独自のRow Level Securityポリシーを定義できます。

方式3: 行レベル分離型(Shared Schema + RLS) すべてのテナントが同じテーブルを共有し、organization_id カラムで区別する方式です。Supabaseと組み合わせる場合に最もコスパがよく、個人開発・スモールチームに最適です。

Supabaseのスキーマ設計

まずSupabaseダッシュボードのSQL Editorで以下のスキーマを作成します。

-- 組織テーブル
create table organizations (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  slug text unique not null,
  plan text not null default 'free', -- free / starter / growth / enterprise
  stripe_customer_id text,
  stripe_subscription_id text,
  max_members int not null default 3,
  created_at timestamptz default now()
);
 
-- 組織メンバーテーブル(ユーザーと組織の紐付け・ロール管理)
create table organization_members (
  id uuid primary key default gen_random_uuid(),
  organization_id uuid references organizations(id) on delete cascade,
  user_id uuid references auth.users(id) on delete cascade,
  role text not null default 'member', -- owner / admin / member / viewer
  invited_by uuid references auth.users(id),
  accepted_at timestamptz,
  created_at timestamptz default now(),
  unique(organization_id, user_id)
);
 
-- 招待テーブル
create table invitations (
  id uuid primary key default gen_random_uuid(),
  organization_id uuid references organizations(id) on delete cascade,
  email text not null,
  role text not null default 'member',
  token text unique not null default encode(gen_random_bytes(32), 'hex'),
  invited_by uuid references auth.users(id),
  expires_at timestamptz default now() + interval '7 days',
  accepted_at timestamptz,
  created_at timestamptz default now()
);
 
-- RLSを有効化
alter table organizations enable row level security;
alter table organization_members enable row level security;
alter table invitations enable row level security;

Row Level Security(RLS)ポリシーの設定

-- ユーザーが所属する組織のみ参照可能
create policy "users can view their organizations"
on organizations for select
using (
  id in (
    select organization_id from organization_members
    where user_id = auth.uid()
    and accepted_at is not null
  )
);
 
-- organization_membersはメンバー自身が参照可能
create policy "members can view own org members"
on organization_members for select
using (
  organization_id in (
    select organization_id from organization_members
    where user_id = auth.uid()
    and accepted_at is not null
  )
);
 
-- owner/adminのみ招待の作成・管理が可能
create policy "admins can manage invitations"
on invitations for all
using (
  organization_id in (
    select organization_id from organization_members
    where user_id = auth.uid()
    and role in ('owner', 'admin')
    and accepted_at is not null
  )
);

✦

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

この記事の続きを読む

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

この記事で得られること
✦マルチテナント設計のベストプラクティスとSupabaseのRow Level Securityを使ったデータ分離の実装手順を習得できる
✦Owner・Admin・Member・Viewerの4段階RBACをRorkで実装し、画面ごとの権限制御を適用する方法を理解できる
✦組織単位のStripeサブスクリプション管理・メンバー数連動課金・請求ポータル統合を完全に実装できる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

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

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

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

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

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

関連記事

⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-13
リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる
ソースに鍵がなくても、出来上がったアプリの中に鍵が残ることがあります。Expo SDK 57 で実際に expo export を実行し、バンドルの中身を grep と 60 行ほどの Node スクリプトで調べて、見つかる鍵と見つからない鍵の違いを確かめました。
⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。