📘
Next.js14 + Supabase セッション トークン クッキー ソケット 購読
ひとことで要約
- セッション:ログイン中の継続状態
- トークン:その状態を証明する鍵(JWT など)
- クッキー:鍵を安全に運ぶ箱(HttpOnly が鉄則)
- ソケット:双方向の常時回線
- 購読:イベントを受け取り続ける登録(解除必須)
用語のざっくり整理
-
セッション (session)
「今このユーザーはログイン中だよね?」という継続中の状態。- 方式1: サーバー管理型(サーバーに状態を保存、クッキーにはIDだけ)
- 方式2: トークン型(状態は保存せず、JWT 等のトークンが「ログイン中」を証明)
Supabase は後者で、sessionオブジェクト=access_token + refresh_token の束を意味します。
-
トークン (token)
サーバーに「私は誰で、何が許されるか」を伝える署名付きの文字列。- Access Token(短命):API 呼び出しに付ける。期限切れしやすい。
- Refresh Token(長命):Access を再発行するためだけに使う。
- JWT or Opaque:中身を読める(JWT)/読めない(Opaque)。Supabase は JWT。
-
クッキー (cookie)
ブラウザが自動で送る小さな保存領域。- 認証では HttpOnly + Secure + SameSite を強く推奨(XSS/CSRF対策)。
- サーバー側レンダリング(SSR)と相性が良い(ヘッダで取れるため)。
-
ソケット (socket / WebSocket)
ブラウザとサーバーの双方向・常時接続。- 例:チャット、通知、リアルタイムメーター。
- HTTP とは別の常時張りっぱなしの回線。切断/再接続処理が要る。
-
購読 (subscription / subscribe)
「このイベントが起きたら知らせて」の受け取り登録。- 例:DB変更、メッセージ到着、GraphQL Subscriptions、RxJS の
observable.subscribe()。 - 実体は WebSocket / SSE / Supabase Realtime / Pusher 等。
- 解除 (unsubscribe) を忘れない。
- 例:DB変更、メッセージ到着、GraphQL Subscriptions、RxJS の
どう組み合わせる?(典型フロー)
- ユーザーがログイン
- サーバー/SDKが access_token と refresh_token を取得
- それらを HttpOnly クッキーに保存(SSR で読める)
- API 呼び出し時は access_token を Authorization ヘッダで送る
- 期限切れ→ refresh_token で自動再発行
- 画面のリアルタイム更新は ソケットで購読(DB変更イベントなど)
Next.js + Supabase(@supabase/ssr)の最小実装パターン
1) サーバークライアント(非推奨オーバーロード回避版)
// app/utils/supabase/server.ts
import { cookies } from 'next/headers'
import { createServerClient, type CookieOptions } from '@supabase/ssr'
export function createSupabaseServer() {
const cookieStore = cookies() // Next.js のサーバー側クッキー
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options }) // HttpOnly/SameSite 等は SDK が付与
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
- これが 現行の推奨形。
cookies: { get/set/remove }を渡すオーバーロードにすると、取り消し線(deprecated)を避けられます。 - SSR で
await createSupabaseServer().auth.getUser()のように利用。
2) ブラウザクライアント
// app/utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
3) ログイン/ログアウト(例)
// app/(auth)/login/actions.ts
'use server'
import { createSupabaseServer } from '@/app/utils/supabase/server'
export async function login(email: string, password: string) {
const supabase = createSupabaseServer()
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) throw error // クッキーは SDK が自動で更新
}
Supabase Realtime(DB変更の購読)
// e.g. app/member/messages/realtime.ts
'use client'
import { supabase } from '@/app/utils/supabase/client'
import { useEffect } from 'react'
export function useMessagesRealtime() {
useEffect(() => {
const channel = supabase
.channel('public:messages')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'messages' },
(payload) => {
// 追加/更新/削除イベントをハンドル
console.log('change:', payload)
}
)
.subscribe()
return () => {
supabase.removeChannel(channel) // ★購読解除を忘れずに
}
}, [])
}
- 裏側は WebSocket。接続エラー時の再接続は SDK が面倒見ます。
- 認可は RLS + JWT の claims で制御されます。
何をいつ使う?
- API 認証を SSR/CSR 両方で安全に → トークンを HttpOnly クッキーで管理(@supabase/ssr)
- リアルタイム UI(チャット/通知/カウンター) → ソケット + 購読
- 単発のデータ取得(一覧/詳細) → 通常の HTTP フェッチ(必要なら ISR/SSR)
- 長時間の処理進捗を追う → まずは HTTP + ポーリング、頻度/負荷次第で ソケットへ
セキュリティの要点(超重要)
- クッキーは HttpOnly + Secure + SameSite=Lax/Strict(XSS/CSRF対策の基本)
- トークンは localStorage に保存しない(XSS 全取得のリスク)
- 短命な Access / 長命な Refresh の分離(漏洩時リスク低減)
- **RLS(行レベルセキュリティ)**で DB 側も縛る(
auth.uid() = user_idだけでなく、用途別に最小権限) - ソケット接続にも認証(接続時にトークンを送る / SDK に任せる)
よくある混同の解消
-
セッション ≠ クッキー
セッションは「状態の概念」、クッキーは「保存/送信の箱」。 -
セッション ≒ トークン束(トークン型の場合)
Supabase のsessionは実態が「Access/Refresh トークンのセット」。 -
購読はプロトコルではない
購読は「パターン」。実装は WebSocket/SSE/Realtime/GraphQL など様々。
Discussion