はじめに
Next.js App Router と Supabase Auth は、個人開発の定番スタックになりつつある。しかし、この組み合わせには独特のハマりポイントがある。
特に middleware.ts の設計を間違えると、認証が機能しなかったり、ログアウトしてもセッションが残ったりする。
2つのプロダクト開発で、Claude Code と共に試行錯誤して得た知見をまとめる。
問題の発見
2025年12月23日、CancelNavi の開発日記にはこう書いた。
認証フロー(Google OAuth)の実装が思ったより複雑だった 認証周りはハマりポイントが多い(リダイレクト、セッション管理など)
そして2026年1月9日、BandBridge の開発日記。
ログアウト機能修正(サーバーサイドログアウト + middleware) クライアントサイドのみのログアウトだとセッションが残る問題があった。サーバーサイドでも確実にログアウトするように
2つのプロダクトで同じ問題を踏んでいた。
middleware.ts の役割
Next.js の middleware.ts は、リクエストがページに到達する前に実行される。認証ガードに最適だ。
開発日記にも書いた。
Next.js App Routerのmiddleware.tsは、認証ガードに最適
基本的な構成
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req, res });
// セッションを取得(これが重要)
const { data: { session } } = await supabase.auth.getSession();
// 保護されたルートへのアクセスチェック
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
ポイント1: getSession() の呼び出し
supabase.auth.getSession() を middleware 内で呼び出すことで、Cookie からセッション情報を読み取り、必要に応じてリフレッシュする。
これを省略すると、セッションが期限切れでも認証済みと判定されてしまう。
ポイント2: matcher の設定
どのルートで middleware を実行するかを config.matcher で指定する。
開発日記には悩みを記録していた。
middleware.tsの設計: どのルートを保護するか、リダイレクト先をどこにするかの判断
全ルートで実行すると不要なオーバーヘッドが発生する。保護が必要なルートのみを指定する。
ログアウトの落とし穴
クライアントサイドだけでログアウトしても、セッションが残ることがある。
// ❌ 不完全なログアウト
const handleLogout = async () => {
await supabase.auth.signOut();
router.push('/');
};
解決策: サーバーサイドでも確実にログアウト
// ✅ 完全なログアウト
// app/api/auth/logout/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST() {
const supabase = createRouteHandlerClient({ cookies });
await supabase.auth.signOut();
return NextResponse.redirect(new URL('/', process.env.NEXT_PUBLIC_SITE_URL));
}
クライアント側からこの API を呼び出す。
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
};
OAuth リダイレクトの処理
Google OAuth などの外部認証を使う場合、コールバック処理も重要だ。
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const supabase = createRouteHandlerClient({ cookies });
await supabase.auth.exchangeCodeForSession(code);
}
// 認証後のリダイレクト先
return NextResponse.redirect(new URL('/dashboard', requestUrl.origin));
}
学んだこと
2つのプロダクト開発を通じて得た教訓。
- middleware.ts で getSession() を必ず呼ぶ: セッションのリフレッシュが行われる
- ログアウトはサーバーサイドで: クライアントサイドだけでは不十分
- matcher は必要最小限に: パフォーマンスのため
開発日記には「認証周りはハマりポイントが多い」と書いたが、パターンを理解すれば対処できる。
まとめ
Next.js App Router + Supabase Auth の認証実装で押さえるべきポイント。
- middleware.ts を活用: 認証ガードの中心
- getSession() を middleware で呼ぶ: セッション管理の要
- ログアウトはサーバーサイド API で: 確実なセッション破棄
- コールバック処理を忘れずに: OAuth フローの完結
同じ問題を2つのプロダクトで踏んだからこそ、Claude Code の助けを借りながら汎用的なパターンとして整理できた。
関連記事
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。