SupabaseとResendで会員管理とメール送信ができる簡易SaaSを構築できるようになる
- Supabaseを使ってデータベースと会員認証を持つバックエンドを構築できる
- 管理画面から会員情報を管理(検索・フィルター・プラン変更・CSVエクスポート)できる
- Resendを使って自動メール送信機能(ウェルカムメール・一括送信)を実装できる
- フロントエンド・バックエンド・メールを組み合わせた「簡易SaaS」を無料で公開できる
SUPABASE_SERVICE_ROLE_KEYはVercelの環境変数にのみ設定し、.envファイルに書いてGitHubにプッシュしないでください。
費用シミュレーション(無料枠)
| サービス | 無料枠 | 月額コスト |
|---|---|---|
| Supabase | 500MB DB・50,000 MAU・2プロジェクト | $0 |
| Resend | 100通/日・3,000通/月 | $0 |
| Vercel | 100GB帯域・Hobbyプラン | $0 |
| 合計 | 小規模SaaSなら十分 | 月$0 |
Phase 1: Supabaseでデータベースと認証を設定する
STEP 1-1: Supabaseプロジェクトの作成
SupabaseはBaaS(Backend as a Service)と呼ばれるサービスです。データベース・認証・ストレージ・Edge Functionsなどのバックエンド機能をまとめて提供しています。
- https://supabase.com にアクセスして「Continue with GitHub」でアカウント登録
- ダッシュボードで「New project」をクリック
- Name:
my-saas-app、Database Password(強力なもの)、Region:Northeast Asia (Tokyo)を入力 - 「Create new project」をクリック(初期化に1〜2分かかる)
- 「Settings」→「API」からProject URLとAnon Keyをメモする
取得した値を .env ファイルに追記します。
VITE_SUPABASE_URL=https://xxxxxxxxxxxxxx.supabase.co VITE_SUPABASE_ANON_KEY=eyJhb...(Anon Keyをここに貼り付け)
| id | plan | created_at | |
|---|---|---|---|
| uuid-1... | user1@example.com | free | 2026-01-01 |
| uuid-2... | user2@example.com | pro | 2026-01-02 |
▲ Supabase のテーブルエディタ。会員データを管理できる
⚠️ エラーが出た場合
「プロジェクトが作成できない」:無料プランではプロジェクトは2個までです。既存のプロジェクトを削除するか有料プランに移行してください。
「.envファイルが反映されない」:.envを編集したら開発サーバーを再起動してください(Ctrl+C → npm run dev)。
「GitHubに.envがプッシュされてしまった」:即座にSupabaseのダッシュボードからキーをローテーション(再発行)してください。
STEP 1-2: テーブルの作成(profiles テーブル + RLS設定)
Table Editorからprofilesテーブルを作成します。
| カラム名 | 型 | デフォルト値 | 備考 |
|---|---|---|---|
id | uuid | auth.uid() | Primary Key |
email | text | — | メールアドレス |
name | text | — | 表示名(Nullable) |
plan | text | 'free' | free / pro / premium |
role | text | 'user' | user / admin |
created_at | timestamptz | now() | 登録日時 |
テーブル作成時に「Enable Row Level Security (RLS)」を ON にします。次にRLSポリシーを追加します。
-- ポリシー1: 自分のプロフィールを読み取る -- Policy name: Users can view own profile -- Allowed operation: SELECT auth.uid() = id -- ポリシー2: 自分のプロフィールを更新する -- Policy name: Users can update own profile -- Allowed operation: UPDATE auth.uid() = id
⚠️ エラーが出た場合
「RLSポリシーでデータが取得できない」:RLSが有効でポリシーが設定されていない場合データは取得できません。SELECTポリシーが設定されているか確認してください。
「カラムのデータ型を間違えた」:テーブルを選択して「Edit Table」から修正できます。データが入っている場合はテーブルを作り直してください。
STEP 1-3: 認証の設定(Email/Password認証 + Google AI Studioでコード生成)
- 「Authentication」→「Providers」→「Email」プロバイダーが ON になっていることを確認
- Google AI Studioで以下のプロンプトを使ってサインアップ・サインインコンポーネントを生成する
- 生成されたコードを
src/components/Auth.tsxとして保存する
以下の条件でReact + TypeScriptのコードを作成してください。 【要件】 - Supabase の認証機能を使ったサインアップとサインインコンポーネントを作成する - @supabase/supabase-js を使用する - supabaseClient は ./supabaseClient からインポートする - サインアップ: メールアドレスとパスワードでアカウント作成 - サインイン: メールアドレスとパスワードでログイン - 認証状態に応じて表示を切り替える - エラーメッセージを日本語で表示する - TailwindCSSでシンプルなスタイリング 【ファイル構成】 - src/components/Auth.tsx(認証フォームコンポーネント) 完全なコードを出力してください。
⚠️ エラーが出た場合
「Email not confirmed」エラー:「Authentication」→「Providers」→「Email」で「Confirm email」をOFFにするか、メール確認のリンクをクリックしてから再サインインしてください。
「Invalid login credentials」:まずサインアップしてからサインインしているか確認してください。
「CORS エラー」:「Authentication」→「URL Configuration」の「Site URL」が正しく設定されているか確認してください。
STEP 1-4: Supabaseクライアントライブラリの設定とCRUD操作
PS> npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string if (!supabaseUrl || !supabaseAnonKey) { throw new Error('VITE_SUPABASE_URL と VITE_SUPABASE_ANON_KEY を .env に設定してください') } export const supabase = createClient(supabaseUrl, supabaseAnonKey) export type Profile = { id: string email: string name: string | null plan: 'free' | 'pro' | 'premium' role: 'user' | 'admin' created_at: string }
主なCRUD操作
// SELECT: 自分のプロフィールを取得 const { data, error } = await supabase .from('profiles') .select('*') .eq('id', userId) .single() // INSERT: プロフィールを新規作成 const { data, error } = await supabase .from('profiles') .insert({ id: userId, email, name, plan: 'free', role: 'user' }) // UPDATE: プランを更新 const { data, error } = await supabase .from('profiles') .update({ plan: 'pro' }) .eq('id', userId)
⚠️ エラーが出た場合
「Module not found: @supabase/supabase-js」:npm install @supabase/supabase-jsを実行してください。
「Cannot read properties of undefined (reading 'VITE_SUPABASE_URL')」:.envファイルが正しい場所(package.jsonと同じフォルダ)にあるか確認してください。開発サーバーを再起動してください。
「RLS policy violation」や「permission denied」:認証済みユーザーとして操作しているか確認してください。
- Supabaseのアカウントを作成し、新規プロジェクトが作成できている
profilesテーブルが作成され、RLSが有効になっている.envファイルにVITE_SUPABASE_URLとVITE_SUPABASE_ANON_KEYが設定されている- サインアップ・サインイン・サインアウトが動作している
- 認証状態に応じてページの表示が切り替わっている
supabaseClient.tsが作成され、CRUD操作のコードが動作している
Phase 2: 管理画面とデータベース連携
STEP 2-1: 管理画面の設計と作成(管理者ロール制御)
まずSQL Editorでテスト用の管理者ユーザーのロールを変更します。
UPDATE profiles SET role = 'admin' WHERE email = 'あなたのメールアドレス@example.com';
PS> npm install react-router-dom
Google AI Studioで管理画面コンポーネントを生成します。
以下の条件でReact + TypeScriptの管理画面コンポーネントを作成してください。 【要件】 - React Router v6 を使用する - /admin パスを管理者ロールのユーザーのみアクセスできるよう保護する - Supabase の profiles テーブルから会員一覧を取得して表示する - 検索機能: メールアドレスまたは名前で絞り込み - フィルター機能: プラン(free/pro/premium)で絞り込み - ページネーション: 1ページ10件表示 - 管理者でない場合は / にリダイレクト 【ファイル】 - src/components/AdminGuard.tsx - src/pages/AdminDashboard.tsx - src/pages/MemberList.tsx
⚠️ エラーが出た場合
「管理者なのに /admin にアクセスできない」:SQL EditorでSELECT role FROM profiles WHERE email = 'あなたのメール'を実行してroleがadminになっているか確認してください。ブラウザのキャッシュをクリアして再ログインしてください。
「Supabase から会員一覧が取得できない」:管理者が全データを読み取れるRLSポリシーが必要です。SQL Editorで管理者向けSELECTポリシーを追加してください。
STEP 2-2: 会員管理機能(プラン変更・CSVエクスポート)
const updateMemberPlan = async ( memberId: string, newPlan: 'free' | 'pro' | 'premium' ) => { const { error } = await supabase .from('profiles') .update({ plan: newPlan }) .eq('id', memberId) if (error) { console.error('プランの更新に失敗しました:', error.message) return false } return true }
CSVエクスポート機能もGoogle AI Studioで生成します。UTF-8 BOM付きで出力することでExcelでも文字化けしません。
以下の要件でTypeScriptのCSVエクスポート関数を作成してください。 - Supabase の profiles テーブルのデータをCSVでダウンロードする - ファイル名: members_YYYYMMDD.csv - 文字コード: UTF-8 BOM付き(Excelで文字化けしないため) - ヘッダー行: ID, メールアドレス, 名前, プラン, 登録日 - ブラウザのダウンロードダイアログを表示する
⚠️ エラーが出た場合
「CSVが文字化けする」:BOM(new Uint8Array([0xEF, 0xBB, 0xBF]))をCSVデータの先頭に付与することで解決します。
「更新処理が失敗する」:RLSポリシーで管理者のUPDATE権限が設定されているか確認してください。
STEP 2-3: データ可視化ダッシュボード(recharts)
PS> npm install recharts
以下の要件でReact + TypeScript + rechartsを使ったダッシュボードを作成してください。 【グラフ1】月別登録者数推移(LineChart)- 過去6ヶ月 【グラフ2】プラン別内訳(PieChart)- free/pro/premium の人数割合 【表】月次収益シミュレーション: free=$0, pro=$9.99, premium=$29.99で計算 - recharts の ResponsiveContainer でレスポンシブ対応 - TailwindCSS でカード形式のレイアウト
⚠️ エラーが出た場合
「グラフが表示されない・サイズが0になる」:ResponsiveContainerの親要素に高さを指定してください(例: <div style={{ height: 300 }}>)。
STEP 2-4: VercelへのデプロイとEnvironment Variables設定
- Vercelダッシュボードでデプロイ済みプロジェクトを開く
- 「Settings」→「Environment Variables」を開く
VITE_SUPABASE_URLとVITE_SUPABASE_ANON_KEYを追加する(Production, Preview, Development すべてに設定)- 「Redeploy」ボタンで再デプロイする
- SupabaseダッシュボードのURL Configuration に Vercel ドメインを追加する
VITE_SUPABASE_URL = https://xxxxxx.supabase.co VITE_SUPABASE_ANON_KEY = eyJhb...(Anon Key)
⚠️ エラーが出た場合
「本番環境でSupabaseに接続できない」:Vercelの環境変数が正しく設定されているか確認してください。変数名がVITE_で始まっているか確認してください。
「本番環境でログイン後にリダイレクトが失敗する」:SupabaseのRedirect URLsにVercelのドメインが追加されているか確認してください。
/adminルートが管理者ロールのユーザーのみアクセスできるよう保護されている- 会員一覧が表示され、検索・フィルタリングができる
- 会員のプラン変更・情報編集ができる
- 会員リストをCSV形式でエクスポートできる
- 登録者数グラフまたはプラン別グラフが表示されている
- Vercelの環境変数にSupabaseのキーが設定されデプロイが成功している
Phase 3: Resendでメール送信機能を実装する
STEP 3-1: Resend のセットアップ(APIキー取得)
Resendは開発者向けのメール送信APIサービスです。シンプルなAPIでHTMLメールを送信できます。無料枠は1日100通・月3,000通です。
- https://resend.com にアクセスしてGitHubアカウントで登録
- ダッシュボードの「API Keys」→「Create API Key」をクリック
- Name:
my-saas-app、Permission:Sending accessで作成 - 生成されたAPIキー(
re_xxxxxx)をメモ(一度しか表示されない) .envに追記する(VITE_プレフィックスを付けないこと)
RESEND_API_KEY=re_xxxxxx(取得したAPIキー)
RESEND_API_KEY には VITE_ プレフィックスを付けないでください。これはVercel Edge Functionsでのみ使用します。フロントエンドに露出すると第三者にメールを悪用される危険があります。PS> npm install resend
⚠️ エラーが出た場合
「APIキーが無効」:APIキーが正しくコピーされているか確認してください。Resendダッシュボードで新しいAPIキーを再発行してください。
「ドメイン認証ができない」:DNSの変更が反映されるまで最大48時間かかることがあります。
STEP 3-2: Vercel Edge FunctionsでメールAPIを作成
プロジェクトのルートに api フォルダを作成し、send-email.ts を作成します。
import { Resend } from 'resend' const resend = new Resend(process.env.RESEND_API_KEY) export const config = { runtime: 'edge' } export default async function handler(request: Request): Promise<Response> { if (request.method !== 'POST') { return new Response('Method Not Allowed', { status: 405 }) } const { to, subject, name, type, message } = await request.json() const { data, error } = await resend.emails.send({ from: 'noreply@あなたのドメイン.com', // テスト用: onboarding@resend.dev to: [to], subject: subject, html: generateEmailTemplate(type, name, message), }) if (error) { return new Response(JSON.stringify({ error: error.message }), { status: 500 }) } return new Response(JSON.stringify({ success: true, id: data?.id }), { status: 200 }) }
▲ Resend APIのテスト送信。{"success":true}が返ればメール送信成功
⚠️ エラーが出た場合
「RESEND_API_KEY is not defined」:Vercelの環境変数にRESEND_API_KEYが設定されているか確認してください。ローカルテストにはvercel devで起動してください。
「from アドレスが無効」:ドメイン認証が完了していない場合はonboarding@resend.devを使用してください。
「メールが届かない・迷惑メールに入る」:迷惑メールフォルダを確認してください。ドメイン認証(SPF・DKIM)が完了していることを確認してください。
STEP 3-3: ウェルカムメール・お知らせメール・一括送信の実装
1. ウェルカムメール(会員登録時自動送信)
const handleSignUp = async (email: string, password: string, name: string) => { // 1. Supabase でアカウント作成 const { data, error } = await supabase.auth.signUp({ email, password }) if (error) throw error // 2. profiles テーブルにプロフィールを追加 await supabase.from('profiles').insert({ id: data.user!.id, email, name, plan: 'free', role: 'user' }) // 3. ウェルカムメールを送信 await fetch('/api/send-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: email, subject: '【サービス名】ご登録ありがとうございます', name, type: 'welcome' }) }) }
2. 一括メール送信(全会員へ)
SUPABASE_SERVICE_ROLE_KEYはサーバーサイドのみで使用します。Vercelの環境変数に設定し、フロントエンドコードには絶対に含めないでください。import { Resend } from 'resend' import { createClient } from '@supabase/supabase-js' const resend = new Resend(process.env.RESEND_API_KEY) const supabase = createClient( process.env.VITE_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! // Service Role Key(サーバーサイドのみ!) ) // 全会員を取得 → 50件ずつ分割して送信(無料枠の制限対応) const chunkSize = 50 for (let i = 0; i < emailBatch.length; i += chunkSize) { await resend.batch.send(emailBatch.slice(i, i + chunkSize)) }
⚠️ エラーが出た場合
「ウェルカムメールが送信されない」:ブラウザの開発者ツール(F12)のネットワークタブで/api/send-emailのリクエストが送信されているか確認してください。
「一括送信で一部が失敗する」:Resendの無料枠(100通/日)を超えていないか確認してください。チャンクサイズを小さくしてみてください。
STEP 3-4: 完成したSaaSアプリの動作確認と公開
全機能の動作テストチェックリスト
- 新規アカウントでサインアップできる
- サインアップ後にprofilesテーブルにデータが作成される
- サインアップ後にウェルカムメールが届く
- メールアドレス・パスワードでサインインできる
- サインアウトできる
- 管理者アカウントで /admin にアクセスできる
- 通常ユーザーは /admin にアクセスできない
- 会員一覧・検索・フィルターが動作する
- CSVエクスポートができる
- お知らせメールを手動送信できる
デプロイ手順
PS> git add . PS> git commit -m "feat: SaaSアプリ完成 - Supabase認証・管理画面・Resendメール送信" PS> git push origin main → Vercelに自動デプロイ開始
よくある本番環境エラー
| エラー | 対処法 |
|---|---|
Invalid API key | Vercelダッシュボードで環境変数を確認・再設定 |
CORS error | SupabaseのRedirect URLsにVercelドメインを追加 |
Row not found | Supabase Policiesを確認 |
| 全ページが404になる | プロジェクトルートにvercel.jsonを作成(SPA設定) |
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
⚠️ エラーが出た場合
「環境変数を追加したのに反映されない」:Vercelダッシュボードで「Redeploy」→「Use existing Build Cache をOFF」にして再デプロイしてください。
「ビルドエラーが出る」:VercelのDeployments → 失敗したデプロイ → Build Logsでエラー内容を確認してください。主な原因はTypeScriptの型エラーやimportパスの誤りです。
- Resendのアカウントを作成しAPIキーを取得できている
/api/send-email.tsが作成されVercel Edge Functionとして動作している- 会員登録時にウェルカムメールが自動送信される
- 管理画面からお知らせメールを手動送信できる
- 全会員への一括メール送信が動作している
- 本番環境での全機能の動作テストが完了している
この講座で作ったもの — 完成したSaaSアプリの機能一覧
- 認証画面(サインアップ/サインイン/サインアウト)
- ユーザーダッシュボード
- 管理画面 (/admin)
- 会員一覧(検索/フィルター/ページネーション)
- 会員編集(プラン変更)
- CSVエクスポート
- データ可視化グラフ
- Supabase(PostgreSQL)
- profiles テーブル
- Row Level Security
- Supabase Auth(認証)
- Vercel Edge Functions
- /api/send-email(個別送信)
- /api/send-bulk-email(一括送信)
- Resend APIによるメール送信
- ウェルカムメール(自動)
- お知らせメール(手動)
- 一括送信
- HTMLメールテンプレート
学んだ技術スタック
| カテゴリ | 技術 | 用途 |
|---|---|---|
| フロントエンド | React + TypeScript + Vite | UI構築 |
| データベース | Supabase (PostgreSQL) | データ保存 |
| 認証 | Supabase Auth | ログイン管理 |
| ホスティング | Vercel | 公開・APIサーバー |
| メール送信 | Resend | トランザクションメール |
| コード生成 | Google AI Studio | 効率的な開発 |
| コード編集 | Cursor | 実装・デバッグ |