個人開発者がB2Bアプリで収益を上げることの難しさは、技術的な問題ではありません。「法人相手に営業なんて自分には無理」という思い込みが最大の壁です。
しかし現実を見ると、法人向けアプリの平均契約単価は個人向けの20〜100倍になります。月980円のサブスクリプションを100人集めるより、月10万円の法人契約を1社取る方が、同じ収益でもストレスは明確に少ありません。Rorkで作ったアプリが解決できる業務課題は、あなたが思っているより身近なところにあります。
B2B向けRorkアプリの設計原則
マルチテナント設計:1つのアプリで複数法人を管理する
B2Bアプリの根幹はマルチテナント設計です。「テナント」とは法人顧客1社のこと。A社のデータとB社のデータを完全に分離しつつ、1つのバックエンドで管理する仕組みが必要になります。
SupabaseのRow Level Security(RLS)を使うと、Rork Maxで安全なマルチテナント実装ができます。
-- Supabase: マルチテナント対応のテーブル設計
-- テナント(法人)テーブル
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
plan TEXT NOT NULL DEFAULT 'starter', -- starter, professional, enterprise
created_at TIMESTAMPTZ DEFAULT now()
);
-- ユーザーとテナントの紐付け
CREATE TABLE tenant_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member', -- owner, admin, member
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(tenant_id, user_id)
);
-- RLSポリシー: 自分のテナントのデータのみアクセス可能
ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can see their own tenant memberships"
ON tenant_users FOR SELECT
USING (user_id = auth.uid());
-- 業務データテーブル(例: 現場チェック記録)
CREATE TABLE site_checks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
title TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE site_checks ENABLE ROW LEVEL SECURITY;
-- RLS: 同じテナントのメンバーのみ閲覧・編集可能
CREATE POLICY "Tenant members can access site checks"
ON site_checks FOR ALL
USING (
tenant_id IN (
SELECT tenant_id FROM tenant_users
WHERE user_id = auth.uid()
)
);このSQL定義をSupabaseに設定すれば、Rorkアプリ側で特別な条件分岐を書かなくても、自動的にテナント単位でデータが分離されます。
管理者ダッシュボードの実装
法人顧客が最初に求める機能は「管理者画面」です。「誰が何を使っているか見たい」「ユーザーを追加・削除したい」というニーズは100%存在します。
// Rork Max: テナント管理APIクライアント
// src/lib/tenant-admin.ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL\!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY\!
);
// テナントのメンバー一覧取得
export async function getTenantMembers(tenantId: string) {
const { data, error } = await supabase
.from('tenant_users')
.select(`
id,
role,
created_at,
users:user_id (
id,
email,
raw_user_meta_data->>full_name as name,
last_sign_in_at
)
`)
.eq('tenant_id', tenantId)
.order('created_at', { ascending: true });
if (error) {
console.error('メンバー取得エラー:', error.message);
throw new Error(`メンバー情報の取得に失敗しました: ${error.message}`);
}
return data ?? [];
}
// メンバー招待(メールアドレスで招待リンクを送る)
export async function inviteMember(
tenantId: string,
email: string,
role: 'admin' | 'member' = 'member'
) {
// 1. Supabase Auth でユーザーを招待(メール送信)
const { data: inviteData, error: inviteError } =
await supabase.auth.admin.inviteUserByEmail(email, {
data: { tenant_id: tenantId, role },
redirectTo: `${process.env.EXPO_PUBLIC_APP_URL}/auth/callback`
});
if (inviteError) {
throw new Error(`招待メールの送信に失敗: ${inviteError.message}`);
}
// 2. tenant_users テーブルにレコードを事前作成(招待中の表示用)
const { error: insertError } = await supabase
.from('tenant_users')
.insert({
tenant_id: tenantId,
user_id: inviteData.user.id,
role
});
if (insertError) {
throw new Error(`テナント登録エラー: ${insertError.message}`);
}
return { success: true, email };
}
// メンバーのロール変更
export async function updateMemberRole(
tenantId: string,
userId: string,
newRole: 'admin' | 'member'
) {
const { error } = await supabase
.from('tenant_users')
.update({ role: newRole })
.eq('tenant_id', tenantId)
.eq('user_id', userId);
if (error) {
throw new Error(`ロール更新エラー: ${error.message}`);
}
}監査ログの実装(法人顧客が必ず求める機能)
法人顧客、特に士業や医療・金融系は「誰がいつ何をしたか」の記録を求めてくる。これを「監査ログ」または「アクティビティログ」と呼ぶ。実装のポイントは、業務処理と同時にログを記録するパターンを徹底することです。
// 監査ログ記録ユーティリティ
// src/lib/audit-log.ts
type AuditAction =
| 'member_invited'
| 'member_removed'
| 'record_created'
| 'record_updated'
| 'record_deleted'
| 'report_exported'
| 'setting_changed';
interface AuditLogEntry {
tenant_id: string;
user_id: string;
action: AuditAction;
target_type: string; // 'user' | 'site_check' | 'report' など
target_id?: string;
metadata?: Record<string, unknown>;
}
export async function recordAuditLog(entry: AuditLogEntry) {
const { error } = await supabase
.from('audit_logs')
.insert({
...entry,
ip_address: null, // モバイルアプリでは取得不可の場合多い
user_agent: `Rork/${process.env.EXPO_PUBLIC_APP_VERSION}`,
created_at: new Date().toISOString()
});
if (error) {
// ログ記録の失敗はサイレントに処理(業務処理をブロックしない)
console.warn('監査ログ記録失敗:', error.message);
}
}
// 使用例: レコード作成時に自動でログを記録
export async function createSiteCheckWithLog(
tenantId: string,
userId: string,
data: { title: string; description: string }
) {
// 業務処理
const { data: record, error } = await supabase
.from('site_checks')
.insert({ tenant_id: tenantId, created_by: userId, ...data })
.select()
.single();
if (error) throw new Error(`作成エラー: ${error.message}`);
// 監査ログを非同期で記録(業務処理の遅延を防ぐ)
recordAuditLog({
tenant_id: tenantId,
user_id: userId,
action: 'record_created',
target_type: 'site_check',
target_id: record.id,
metadata: { title: data.title }
}).catch(console.warn);
return record;
}法人向け料金設計:適切な価格設定が全てを決める
3つの課金モデルと使い分け
ユーザー数課金(Per Seat) 月額 × ユーザー数で課金する最もシンプルなモデル。予測可能な収益になりやすく、法人担当者も予算計画が立てやすい。
- 適している: ユーザー全員が同様に使うツール(チェックリスト、勤怠、経費)
- 例: 1ユーザー月5,000円、最低5ユーザーから(月2.5万円〜)
機能別課金(Tiered Feature) 「スタータープラン」「プロフェッショナルプラン」「エンタープライズプラン」と機能で価格を分けるモデル。上位プランへのアップセルが狙いやすい。
- 適している: 機能の豊富さが差別化になるツール
- 例: スターター月3万円(10ユーザー)/ プロ月8万円(50ユーザー+API連携)/ エンタープライズ要見積もり
使用量課金(Usage-Based) 実際の利用量(処理件数、APIコール数、ストレージ量)で課金するモデル。導入ハードルが低く、成長とともに収益が増える。
- 適している: AI機能を含むアプリ、データ処理量が変動するツール
- 例: 基本料月1万円 + 処理件数1件あたり10円
StripeでのB2B請求書払い対応
法人顧客の多くが「請求書払い(後払い)」を求めてくる。Stripeはこれに対応しており、Rorkアプリからプログラムで請求書を発行できます。
// B2B請求書の自動発行
// src/app/api/billing/invoice/route.ts(Next.js互換のCloudflare Workers)
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY\!, {
apiVersion: '2024-06-20'
});
export async function POST(request: Request) {
const { tenantId, planId, userCount } = await request.json();
try {
// テナント情報をDBから取得
const tenant = await getTenantById(tenantId);
// Stripeカスタマーが未作成なら作成
let customerId = tenant.stripe_customer_id;
if (\!customerId) {
const customer = await stripe.customers.create({
name: tenant.company_name,
email: tenant.billing_email,
metadata: {
tenant_id: tenantId,
app: 'your-app-name'
}
});
customerId = customer.id;
// テナントにStripeカスタマーIDを保存
await updateTenantStripeId(tenantId, customerId);
}
// 請求書アイテムの追加
const pricePerUser = 5000; // 1ユーザー月5,000円
await stripe.invoiceItems.create({
customer: customerId,
amount: pricePerUser * userCount * 100, // Stripeは円(最小単位)
currency: 'jpy',
description: `プロフェッショナルプラン(${userCount}ユーザー)`
});
// 請求書の作成と送付
const invoice = await stripe.invoices.create({
customer: customerId,
collection_method: 'send_invoice',
days_until_due: 30, // 30日後支払い
metadata: { tenant_id: tenantId, plan_id: planId }
});
// 請求書を確定して送付
const finalizedInvoice = await stripe.invoices.finalizeInvoice(invoice.id);
await stripe.invoices.sendInvoice(finalizedInvoice.id);
return Response.json({
success: true,
invoice_url: finalizedInvoice.hosted_invoice_url,
invoice_pdf: finalizedInvoice.invoice_pdf
});
} catch (error) {
console.error('請求書作成エラー:', error);
return Response.json(
{ error: '請求書の作成に失敗しました' },
{ status: 500 }
);
}
}この実装で、管理画面から「請求書発行」ボタンを押すだけで法人担当者にPDFつき請求書メールが届く。支払い確認もStripeのWebhookで自動化できます。
年間契約で収益を安定させる
月額課金に加えて年間一括払いオプションを用意すると、以下のメリットがある:
- 開発者側:年間収益が確定し、キャッシュフローが安定
- 法人側:通常10〜20%の割引が得られ、予算処理が楽
年間契約への誘導は、最初の提案時に「年間プランだと月額換算でN%お得です」と明示するだけで、30〜40%の法人が選んでくれる経験則があります。