-
supabase x next.jsのセットアップや主要機能の実装
- google認証でのログイン, セッション取得, DB CRUD, ストレージへのアップロード
-
アーキテクチャ
- next.js(docker), supabase
- 2025/7現在の最新バージョンを使用
-
公式ドキュメント
- 環境設定&セットアップ
- docker-compose
version: "3.8"
services:
nextjs:
image: node:22
container_name: nextjs-app
working_dir: /app
volumes:
- ./:/app
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
- NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
tty: true
- コンテナに入る(僕はvs codeのattach to running container)
- Next.jsプロジェクト作成
npx create-next-app@latest
- supabaseクライアントインストール
npm install @supabase/supabase-js
-
プロジェクトルートでnpm install する
-
supabase google認証
- supabaseプロジェクト作成
- Authentication/Sign In・Providersからgoogleを選択
-
Callback urlを保存(後でgoogle cloud consoleで使う)
-
google cloud consoleでクライアントID, クライアントシークレットを作成
- プロジェクト作成
- APIとサービス/認証情報
- OAuthクライアントIDを作成
- プロジェクトで最初に作成するとき、同意画面の構成を求められる
- 開始をクリック
- 順に入力
- 任意のアプリ名と連絡先のメアド
- 今回は外部を選択
- 連絡先のメアド
-
作成完了
-
改めてOAuthクライアントIDの作成
- アプリケーションの種類はウェブ
- 承認済みのJS生成元は開発用にlocalhost
- (追加で本番用のドメインを登録してもよい)
- 承認済みのリダイレクトURIは先ほどのsupabase google認証のCallback URLを入力
- 作成完了
- クライアントIDとクライアントシークレットを保存しておく
- supabaseでgoogle認証を追加する
- 先ほど取得したクライアントIDとクライアントシークレットを入力
-
.env.localやdockerfile, docker-composeに書いておく
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhb...
GOOGLE_AUTH_CLIENT_ID=780764369850-xxx.apps.googleusercontent.com
GOOGLE_AUTH_CLIENT_SECRET=GOCS..
- next.jsでの利用
- 認証
- 今回はフロントエンドでの認証とセッションの取得
- サーバーサイドでもできるらしい
'use client'
import { supabase } from '@/utils/supabase/supabaseClient'
export default function Home() {
const signInWithGoogle = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
//redirectTo: 'http://localhost:3000/auth/callback',
},
})
}
return (
<main>
<h1>ログインページ</h1>
<button onClick={signInWithGoogle}>Googleでログイン</button>
</main>
)
}
- 実行してみる
- cssを割り当てていない, 「Googleでログイン」部分をクリック
- 見慣れたログイン画面
- supabaseコンソールでユーザー登録を確認
- セッション取得
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/utils/supabase/supabaseClient'
import type { Session } from '@supabase/supabase-js'
export default function CallbackPage() {
const router = useRouter()
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
// 1. 直近のセッションを取得
const fetchSession = async () => {
const { data, error } = await supabase.auth.getSession()
if (error) console.error('セッション取得失敗:', error)
setSession(data.session ?? null)
setLoading(false)
}
fetchSession()
// 2. ログイン/ログアウトをリアルタイムで監視
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, newSession) => {
setSession(newSession)
})
// 3. クリーンアップ
return () => {
subscription.unsubscribe()
}
}, [])
/* --------- 画面描画 --------- */
if (loading) {
return <p>ログイン処理中...</p>
}
if (session) {
return (
<div>
<p>ログイン成功!ようこそ {session.user.email}</p>
<button onClick={() => router.push('/')}>トップへ戻る</button>
</div>
)
}
return (
<div>
<p>認証に失敗しました。再度お試しください。</p>
<button onClick={() => router.push('/')}>トップへ戻る</button>
</div>
)
}
- crud
- 簡単なTodoライクアプリ
- 認証状態を保持し、自動でRLSが適応される
- RLS
- row level security
- ログインユーザーごとにアクセス可能なデータ行を細かく制御できる仕組み
- DBだけでなくStorage にも適用され、自分の画像だけをアップロード・閲覧できる
- 簡単な SQL ポリシーを書くことで、安全なアクセス制御が実現できる
'use client'
import { useEffect, useState } from 'react'
import { supabase } from '@/utils/supabase/supabaseClient'
type SampleRow = {
id: string
created_at: string
text: string
user_id: string
}
export default function SampleCrudPage() {
const [samples, setSamples] = useState<SampleRow[]>([])
const [inputText, setInputText] = useState('')
const [userId, setUserId] = useState<string | null>(null)
/* ──────── セッション取得 ──────── */
useEffect(() => {
const getSession = async () => {
const { data } = await supabase.auth.getSession()
setUserId(data.session?.user.id ?? null)
}
getSession()
}, [])
/* ──────── 初回 READ ──────── */
useEffect(() => {
if (userId) fetchSamples()
}, [userId])
/* 取得 (自分のデータのみ) */
const fetchSamples = async () => {
const { data, error } = await supabase
.from('SampleTable')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
if (error) {
console.error('データ取得エラー:', error)
} else {
setSamples(data)
}
}
/* 追加 (user_id 必須) */
const addSample = async () => {
if (!userId) return
const { error } = await supabase
.from('SampleTable')
.insert({ text: inputText, user_id: userId })
if (error) {
alert('追加失敗: ' + error.message)
} else {
setInputText('')
fetchSamples()
}
}
/* 削除 (RLS が自分のデータのみ許可) */
const deleteSample = async (id: string) => {
const { error } = await supabase
.from('SampleTable')
.delete()
.eq('id', id)
if (error) {
alert('削除失敗: ' + error.message)
} else {
fetchSamples()
}
}
return (
<div style={{ padding: '2rem' }}>
<h1>SampleTable CRUD</h1>
<input
value={inputText}
onChange={e => setInputText(e.target.value)}
placeholder="テキストを入力"
/>
<button onClick={addSample} disabled={!userId}>
追加
</button>
<ul>
{samples.map(item => (
<li key={item.id}>
{item.text}{' '}
<button onClick={() => deleteSample(item.id)}>削除</button>
</li>
))}
</ul>
</div>
)
}
- table, RLS
/* -----------------------------------------------------------
Todo ライクな「SampleTable」 ― スキーマ & RLS ポリシー
-----------------------------------------------------------
- 各ユーザーが自分のタスクだけを CRUD 可能
- Supabase がデフォルトで用意する auth.users(id) と連携
----------------------------------------------------------- */
/* 1) テーブル定義 ----------------------------------------- */
create table public."SampleTable" (
id uuid primary key default uuid_generate_v4(),
created_at timestamp default now(),
text text not null,
user_id uuid not null references auth.users(id)
);
/* 2) RLS を有効化 ----------------------------------------- */
alter table public."SampleTable" enable row level security;
/* 3) ポリシー: 自分の行だけ参照・操作できる --------------- */
/* ── SELECT(読み取り) ── */
create policy "sample_select_own"
on public."SampleTable"
for select
using ( auth.uid() = user_id );
/* ── INSERT(追加) ── */
create policy "sample_insert_own"
on public."SampleTable"
for insert
with check ( auth.uid() = user_id );
/* ── UPDATE(更新) ── */
create policy "sample_update_own"
on public."SampleTable"
for update
using ( auth.uid() = user_id )
with check ( auth.uid() = user_id );
/* ── DELETE(削除) ── */
create policy "sample_delete_own"
on public."SampleTable"
for delete
using ( auth.uid() = user_id );
- SQL editorで実行
- 動作する
- DBにレコードが登録されていることを確認
-
ファイルサーバー
- こちらも自動でRLSが適応される
- 事前にバケットを作成
- new bucketから新規バケットを作成
- public bucketはONにしてください(後から変更できます)
- RLS
- https://zenn.dev/joo_hashi/articles/fe02fa0036af7b
- コンソールで設定していく
- policies//new policy
- 今回はget started quickly
- 今回は2行目の「uidでフォルダーを作ってそこにアクセスできるようにする」を選択
- allow operationのCRUD全てにチェック
- ポリシーが追加される
- アップロードとパブリックURLの取得
'use client'
import { ChangeEvent, useState } from 'react'
import Image from 'next/image'
import { supabase } from '@/utils/supabase/client'
export default function SingleImageUploader() {
const [path, setPath] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
/* ── アップロード ── */
const onFile = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
/* ① ログイン必須 ―― 未ログインなら弾く */
const {
data: { session },
} = await supabase.auth.getSession()
if (!session) {
return setError('ログインしてからアップロードしてください')
}
/* ② ファイルパス = {uid}/{timestamp}.{ext} */
const uid = session.user.id
const ext = file.name.split('.').pop()
const key = `${uid}/${Date.now()}.${ext}`
/* ③ upload ―― 初回 INSERT, 2回目以降は upsert ⇒ UPDATE */
const { error } = await supabase.storage.from('images').upload(key, file, {
upsert: true,
cacheControl: '3600',
})
if (error) return setError(error.message)
setPath(key)
}
/* ── URL 変換 ── */
const url =
path && supabase.storage.from('images').getPublicUrl(path).data.publicUrl
/* ── UI ── */
return (
<main className="p-6 space-y-4">
<input type="file" accept="image/*" onChange={onFile} />
{error && <p className="text-red-600">{error}</p>}
{url && (
<div className="relative w-60 h-60">
<Image
src={url}
alt="uploaded"
fill
sizes="240px"
className="object-cover rounded"
/>
</div>
)}
</main>
)
}
- 画像アップロードとパブリックURLでの画像表示ができた
まとめ
- 以上です。お疲れ様でした
- supabaseのようなBaaSを使うことでかなり開発期間を短縮できます
- MCPを使いAI駆動で利用することもできるようです
- 個人開発では熱意が冷めないうちに形にすることが大切だと思います
- 一緒に頑張りましょう