📘

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) を忘れない。

どう組み合わせる?(典型フロー)

  1. ユーザーがログイン
  2. サーバー/SDKが access_tokenrefresh_token を取得
  3. それらを HttpOnly クッキーに保存(SSR で読める)
  4. API 呼び出し時は access_token を Authorization ヘッダで送る
  5. 期限切れ→ refresh_token で自動再発行
  6. 画面のリアルタイム更新は ソケットで購読(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