SupabasePrisma認証PostgreSQLTypeScript

Supabase × Prismaで認証システムを最速で構築する方法

読了約 7

バックエンドを一から構築する時間がない個人開発者や小規模チームにとって、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とキーを取得します。

bash
# 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/postgres

Prismaスキーマ定義

Supabaseの認証はauth.usersテーブルで管理されます。このテーブルを参照する形で、アプリ固有のユーザー情報テーブルを作成します。

typescript
// 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エディタで以下のポリシーを設定します。

typescript
-- 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ルートでセッションを管理します。

typescript
// 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のミドルウェアを使うと、保護されたルートへのアクセスを一元管理できます。認証済みでないユーザーは自動的にログインページにリダイレクトされます。

typescript
// 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を使う場合は、ユーザーの権限チェックをアプリケーションレベルで実装する必要があります。

SupabasePrisma認証PostgreSQLTypeScript

AI協働開発のご相談はこちら

この記事の内容を実際のプロジェクトに活用したい方、 開発のご依頼・ご質問はお気軽にどうぞ。

無料相談を申し込む