バックエンドを一から構築する時間がない個人開発者や小規模チームにとって、SupabaseとPrismaの組み合わせは最強の選択肢の一つです。SupabaseはPostgreSQLベースのBaaS(Backend as a Service)で、認証、リアルタイム、ストレージ機能を提供します。PrismaはTypeScriptとの相性が抜群のORMです。この記事では、この組み合わせで堅牢な認証システムを最速で構築する方法を解説します。
なぜこの組み合わせを選ぶのか
Supabase単体でもSupabase Clientを使ったデータアクセスが可能ですが、Prismaと組み合わせることで以下のメリットが得られます。
- Prismaの型安全なクエリでバグを防止できる
- PrismaのマイグレーションでDBスキーマをコードで管理できる
- Prisma Studioで開発中のデータを視覚的に確認できる
- SupabaseのRLS(行レベルセキュリティ)でデータアクセスを細かく制御できる
- Supabaseの認証機能を使いながら、DBロジックはPrismaで記述できる
Supabaseプロジェクトのセットアップ
まずsupabase.comでプロジェクトを作成します。無料プランでも2つのプロジェクトを作成でき、500MBのPostgreSQLデータベースが利用できます。プロジェクト作成後、Settings → APIからURLとキーを取得します。
# Supabaseクライアントとサーバーサイド用SSRパッケージをインストール
npm install @supabase/supabase-js @supabase/ssr
# Prismaをインストール
npm install prisma @prisma/client
npx prisma init
# 環境変数の設定(.env.local)
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxxxxxxxxx
DATABASE_URL=postgresql://postgres:password@db.xxxxxxxxxx.supabase.co:5432/postgres
DIRECT_URL=postgresql://postgres:password@db.xxxxxxxxxx.supabase.co:5432/postgresPrismaスキーマ定義
Supabaseの認証はauth.usersテーブルで管理されます。このテーブルを参照する形で、アプリ固有のユーザー情報テーブルを作成します。
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model Profile {
id String @id @default(uuid())
userId String @unique @map("user_id")
displayName String? @map("display_name")
avatarUrl String? @map("avatar_url")
bio String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("profiles")
}
model Post {
id String @id @default(uuid())
authorId String @map("author_id")
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("posts")
}Row Level Security(RLS)の設定
RLSはPostgreSQLのセキュリティ機能で、行レベルでデータアクセスを制限できます。Supabaseでは全テーブルでRLSを有効化することが強く推奨されています。SupabaseダッシュボードのSQLエディタで以下のポリシーを設定します。
-- SQL(Supabaseダッシュボードのクエリエディタで実行)
-- profilesテーブルのRLSを有効化
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- ユーザーは自分のプロフィールのみ参照可能
CREATE POLICY "profiles_select_own" ON profiles
FOR SELECT
USING (auth.uid() = user_id);
-- ユーザーは自分のプロフィールのみ更新可能
CREATE POLICY "profiles_update_own" ON profiles
FOR UPDATE
USING (auth.uid() = user_id);
-- postsテーブルのRLSを有効化
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 公開済み投稿は全員が参照可能
CREATE POLICY "posts_select_published" ON posts
FOR SELECT
USING (published = true OR auth.uid() = author_id);
-- 著者のみ投稿を作成・更新・削除可能
CREATE POLICY "posts_insert_own" ON posts
FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "posts_update_own" ON posts
FOR UPDATE
USING (auth.uid() = author_id);JWT認証の実装
SupabaseはJWTベースの認証を提供します。Next.js App RouterではServer ActionsやAPIルートでセッションを管理します。
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Server Componentからの呼び出し時は無視
}
},
},
}
)
}
// app/auth/login/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function LoginPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
// ログイン済みならダッシュボードへリダイレクト
if (user) {
redirect('/dashboard')
}
return <LoginForm />
}ミドルウェアでの認証チェック
Next.jsのミドルウェアを使うと、保護されたルートへのアクセスを一元管理できます。認証済みでないユーザーは自動的にログインページにリダイレクトされます。
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// 保護されたルートのチェック
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone()
url.pathname = '/auth/login'
return NextResponse.redirect(url)
}
return supabaseResponse
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*'],
}💡 ヒント
Supabase × Prismaの構成で注意が必要なのは、PrismaはRLSをバイパスしてPostgreSQLに直接接続する点です。サーバーサイドのAPIルートでPrismaを使う場合は、ユーザーの権限チェックをアプリケーションレベルで実装する必要があります。