supabase について
supabaseは、Supabaseを使ったNext.js SaaSの構築を支援するセットアップガイドスキルです。RLSで保護するユーザーデータにはSupabaseクライアントを、サーバー側のアクセスにはDrizzleとプーラーを使う二系統の構成を説明します。
主な機能
- 01二系統のデータアクセス構成の設計
- 02Supabase SSRクライアントとミドルウェアの設定
- 03Google OAuth認証フローの設定
- 04Drizzleによるデータベース接続とマイグレーション
- 05RLSポリシーとセキュリティのチェックリスト
- 06プーラーとコスト最適化の注意点
活用シーン
- 01Next.js SaaS向けSupabase認証の設定
- 02Drizzle経由でのSupabaseデータベース接続
- 03publicスキーマのテーブルでのRLS有効化
- 04プーラーやトークン更新の問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dicklesworthstone/ agent_flywheel_clawdbot_skills_and_integrations --skill supabaseインストール後、Claude Code で/supabaseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/ARCHITECTURE.mdAUTH-SSR.mdCOST-OPTIMIZATION.mdDRIZZLE-SETUP.mdGOOGLE-OAUTH.mdPOOLER-GOTCHAS.mdQUICK-REFERENCE.mdRLS-PATTERNS.mdTROUBLESHOOTING.md詳細
Next.js SaaS 向け Supabase
重要なポイント: 2 つの経路を使い分けます。ユーザー単位のデータには Supabase クライアント(RLS による強制)を、サーバー単位のデータには Drizzle(特権アクセス)を使用します。
getSession()ではなくgetClaims()を使用してください。トランザクションプーラーではprepare: falseが必要です。
目次
正確なプロンプト
Set up Supabase for a Next.js SaaS with:
1. Auth: Google OAuth only
2. ORM: Drizzle with Supabase pooler (IPv4)
3. Environment: [Vercel / Cloudflare / local dev]
4. Cost tier: [Free / Pro with Spend Cap / Team]
Follow supabase-guide skill patterns.
アーキテクチャ
┌─────────────────────────────────────────────────────────────────────┐
│ TWO-LANE DATA ACCESS MODEL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LANE A: User-scoped (RLS-enforced) │
│ ══════════════════════════════════ │
│ @supabase/ssr → PostgREST Data API │
│ • Browser + Server Components │
│ • JWT validated via getClaims() │
│ • Multi-tenant reads/writes obey RLS "naturally" │
│ │
│ LANE B: Server-scoped (privileged) │
│ ══════════════════════════════════ │
│ Drizzle → postgres.js → Supavisor pooler │
│ • Migrations, admin tasks, billing webhooks │
│ • Transaction mode (port 6543): prepare: false │
│ • Session mode (port 5432): migrations only │
│ │
└─────────────────────────────────────────────────────────────────────┘
この使い分けの理由: ブラウザーからのアクセスでは、RLS を適用した Data API を使用します。サーバー/サーバーレスではプーラーを使用します。IPv4 のみの環境(Vercel/GH Actions)では、直接接続(IPv6)を使用できません。
クイックスタート
1. Drizzle + プーラー(トランザクションモード)
// src/db/index.ts
import postgres from "postgres"
import { drizzle } from "drizzle-orm/postgres-js"
import * as schema from "./schema"
const g = globalThis as unknown as { __sql?: ReturnType<typeof postgres> }
export const sql = g.__sql ?? postgres(process.env.DATABASE_URL!, {
prepare: false, // REQUIRED for transaction pooler
max: process.env.NODE_ENV === "production" ? 5 : 1,
})
if (process.env.NODE_ENV !== "production") g.__sql = sql
export const db = drizzle(sql, { schema })
2. Supabase SSR クライアント
// src/lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr"
export function supabaseBrowser() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr"
import { cookies } from "next/headers"
export function supabaseServer() {
const store = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll: () => store.getAll(),
setAll: (toSet) => {
try { toSet.forEach(({ name, value, options }) => store.set(name, value, options)) }
catch {}
},
},
}
)
}
3. ミドルウェア(トークンの更新)
// middleware.ts
import { createServerClient } from "@supabase/ssr"
import { NextResponse, type NextRequest } from "next/server"
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request: { headers: request.headers } })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (toSet) => {
toSet.forEach(({ name, value }) => request.cookies.set(name, value))
response = NextResponse.next({ request: { headers: request.headers } })
toSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options))
},
},
}
)
await supabase.auth.getClaims() // NOT getSession()
return response
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
}
環境変数
# Supabase Auth / API
NEXT_PUBLIC_SUPABASE_URL="https://<project_ref>.supabase.co"
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY="sb_publishable_..." # or anon key during transition
SUPABASE_SERVICE_ROLE_KEY="..." # server-only, NEVER ship to client
# Postgres: app runtime (transaction pooler, port 6543)
DATABASE_URL="postgresql://postgres.<project_ref>:<PASSWORD>@aws-0-<region>.pooler.supabase.com:6543/postgres?sslmode=require"
# Postgres: migrations (session pooler, port 5432)
DATABASE_URL_MIGRATIONS="postgresql://postgres.<project_ref>:<PASSWORD>@aws-0-<region>.pooler.supabase.com:5432/postgres?sslmode=require"
ユーザー名の形式: postgres.<project_ref> — ピリオドは実際の文字です。Dashboard → Connect ボタンからコピーしてください。
ワークフローチェックリスト
フェーズ 1:プロジェクトのセットアップ
- 本番用とステージング用の Supabase プロジェクトを作成します(影響範囲を分離します)
- Connect ボタンから接続文字列をコピーします(推測で入力しないでください)
- 環境変数を設定します:
NEXT_PUBLIC_SUPABASE_URL、NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY、DATABASE_URL、DATABASE_URL_MIGRATIONS - Spend Cap を有効にします(Pro プラン)— COST-OPTIMIZATION.md
フェーズ 2:認証(Google のみ)
- Dashboard → Auth → Providers で Google プロバイダーを有効にします
- メール/パスワード、マジックリンク、電話番号を無効にします
- Google Cloud Console にリダイレクト URI を追加します:
https://<project_ref>.supabase.co/auth/v1/callback - SSR クライアントとミドルウェアをセットアップします — AUTH-SSR.md
- OAuth コールバックルートを作成します — GOOGLE-OAUTH.md
フェーズ 3:データベース + Drizzle
- インストールします:
bun add drizzle-orm postgres && bun add -d drizzle-kit -
prepare: falseを指定してsrc/db/index.tsを作成します — DRIZZLE-SETUP.md - マイグレーション用のセッションプーラーを参照する
drizzle.config.tsを作成します -
drizzle-kit generateとdrizzle-kit migrateを実行します
フェーズ 4:RLS + データモデル
-
auth.users.idをキーとするuser_profilesテーブルを作成します -
publicスキーマのすべてのテーブルで RLS を有効にします - サインアップ時にプロフィールを自動作成するトリガーを作成します
- RLS ポリシーで使用する列にインデックスを作成します — RLS-PATTERNS.md
フェーズ 5:コストの最適化
- 実行時にはトランザクションプーラーを、マイグレーションにはセッションプーラーを使用します
- Data API を強化します:機密性の高いテーブルを
privateスキーマに移動します - イベント数の多いテーブルに対して、cron による集計を設定します
- Storage 用に Smart CDN と cache-control を設定します — COST-OPTIMIZATION.md
重大な注意点
| 症状 | 原因 | 解決方法 |
|---|---|---|
ENETUNREACH / network unreachable | 直接接続が IPv6 | プーラーの接続文字列を使用します |
prepared statement does not exist | トランザクションプーラーとプリペアドステートメントの併用 | postgres.js で prepare: false を設定します |
Tenant or user not found | プーラーのホストまたはユーザー名の形式が間違っている | Connect ダイアログから正確な文字列をコピーし、ユーザー名に .project_ref を含めます |
| ユーザーがランダムにログアウトされる | ミドルウェアでトークンが更新されていない | ミドルウェアでは getSession() ではなく getClaims() を使用します |
| RLS ポリシーがアクセスをブロックしない | テーブルで enable row level security が設定されていない | ALTER TABLE x ENABLE ROW LEVEL SECURITY を実行します |
| RLS を使ったクエリが遅い | RLS のフィルター列にインデックスがない | user_id、tenant_id 列にインデックスを追加します |
| 想定外の請求 | Spend Cap が無効で、エグレスがキャッシュされていない | Spend Cap を有効にし、Smart CDN と高い cache-control を使用します |
トラブルシューティングの詳細:TROUBLESHOOTING.md
セキュリティルール
getSession()ではなくgetClaims()を使用する — Cookieは偽装される可能性があるため、getClaims()は毎回JWT署名を検証しますservice_roleキーを絶対に公開しない — RLSを回避するため、バックエンド専用ですpublic内のすべてのテーブルでRLSを有効にする — RLSがないテーブルにはanonロールでアクセスできます- tenant_idには
user_metadataではなくapp_metadataを使用する — user_metadataはユーザーが変更できます
クイックコマンド
# Supabase CLI
supabase login
supabase link --project-ref $PROJECT_REF
supabase db pull # Pull remote schema
supabase migration new <name> # Create migration
supabase db push # Apply migrations
# Drizzle
drizzle-kit generate # Generate migrations
drizzle-kit migrate # Apply migrations
drizzle-kit studio # Visual browser
# Secrets (Edge Functions)
supabase secrets set --env-file .env # Push secrets
supabase secrets list # Verify
AGENTS.mdの説明
プロジェクトのAGENTS.mdにコピーしてください:
### Supabase
Supabase CLI is installed. Project: `<PROJECT_REF>`.
Auth:
\`\`\`bash
supabase login
supabase link --project-ref $PROJECT_REF
\`\`\`
Database:
\`\`\`bash
supabase db pull # Pull schema to migrations
supabase migration new <name>
supabase db push # Apply migrations
\`\`\`
Connection strings:
- **Runtime (Drizzle):** Transaction pooler (port 6543), `prepare: false`
- **Migrations:** Session pooler (port 5432)
Copy exact connection string from Dashboard → Connect button.
参考資料
| トピック | 参考資料 |
|---|---|
| コピー&ペースト用のパターン | QUICK-REFERENCE.md |
| 2レーンアーキテクチャ+コストモデル | ARCHITECTURE.md |
| Drizzle+プーラーのセットアップ | DRIZZLE-SETUP.md |
| getClaims()を使ったNext.js SSR認証 | AUTH-SSR.md |
| Google OAuthのセットアップ | GOOGLE-OAUTH.md |
| SaaS向けRLSパターン | RLS-PATTERNS.md |
| プーラーの注意点(IPv4/IPv6、モード) | POOLER-GOTCHAS.md |
| コスト最適化(Spend Cap、cron) | COST-OPTIMIZATION.md |
| トラブルシューティングガイド | TROUBLESHOOTING.md |
検証
# After setup, verify:
# 1. Google OAuth flow works (login → callback → dashboard)
# 2. Drizzle can connect: bun run -e "import { db } from './src/db'; console.log('OK')"
# 3. RLS blocks unauthorized access via Data API
# 4. Middleware refreshes tokens (check cookies in DevTools)
コメント
Markdown 対応