はじめに
前回の記事より、栽培管理アプリをちょこちょこ作り始めているのですが、
栽培管理アプリの設計を進めていたところ、@supabase/ssrというものの存在を知りました。
「ssr? 出荷アプリは@supabase/supabase-jsだったけど、何が違う?」
調べてみると、これは単なるアップデートではなく、Next.js App Routerでの推奨方法が根本的に変わったことを意味していました。
結果として、「複数アプリを連携する予定なら、統一すべき」という結論に至り、既存の出荷管理アプリも含めて大規模な移行作業を行うことになりました。
注意: この記事は2025年11月時点の情報に基づいています。Supabase/Next.jsのエコシステムは活発に開発が進んでいるため、将来的に推奨方法が変わる可能性があります。開発前には必ず公式ドキュメント等で最新情報をご確認ください。
ChatGPTとのやり取りで混乱した経緯
実は、@supabase/ssrへの移行は一直線に決まったわけではありません。ChatGPTとのやり取りで二転三転し、最終的に自分で判断する必要がありました。
実際の流れ:
1. 出荷アプリ開発時(2025年6月)
「Next.js × Supabaseで出荷管理アプリを作りたい」
ChatGPT: 「@supabase/supabase-jsを使いましょう」
当時は@supabase/ssrがまだベータ版(v0.x)だったため、この判断は妥当でした。
2. 栽培アプリの設計開始(2025年11月)
コードを書き始めると、ChatGPTは@supabase/supabase-jsの例を提示。
3. 連携の話が出る
「ある程度出来ている出荷アプリと、今作っている栽培アプリとの連携も考えたい」と伝えると…
ChatGPT: 「じゃあ@supabase/ssrにしましょう!」
import { createBrowserClient } from "@supabase/ssr"
export const createSupabaseBrowserClient = () => ...
私: 「ssr? 何じゃそれは? 」
4. 混乱ポイント1: 定型ファイルは後回し?
私: 「今書いてる@supabase/supabase-jsのコードはどうするの?」
ChatGPT: 「それはそれでOKです。server.tsとmiddleware.tsは後で必要になったら作りましょう」
私: 「ssrと混在するってこと?後回しって何!? 絶対忘れるわ!」
5. 混乱ポイント2: 既存アプリはどうする?
私: 「じゃあ、既存の出荷アプリも@supabase/ssrにすべき?」
ChatGPT: 「動いているなら後回しでいいです」
私: 「連携するって言って、既存アプリは放置って意味わからん。」
生成AIは嘘をつくこともあるとは言うものの
生成AIとのやり取りにはテクニックがあるのは知っているが、普通に何も考えずに対話でやっていくとまあ、こういうことがよくある。
- 中途半端な提案: ssrを勧めたのに、定型ファイルは「後で」
- 矛盾した判断: 連携を考慮してssrを勧めたのに、既存アプリは「後回し」
- 一貫性のなさ: 提案の根拠(連携)を無視した結論
自分で判断した結果
「複数アプリを連携する」という目的を考えると:
- ssrで統一する必要がある ← これは正しい
- 定型ファイル最初から作るべき ← これも正しい
- 既存アプリも今統一すべき ← これも正しい
ChatGPTの「後回し」という判断はわかる(栽培アプリ制作を優先すべき)のだが、人間は必ず今のやりとりを忘れる。
と、いうか今の議論自体が不毛。
結局@supabase/ssrへの移行が必要なのか
@supabase/supabase-jsと@supabase/ssrの違い
@supabase/supabase-js
- localStorageベースの認証(ブラウザに置きっぱなし。サーバーは何も知らない)
- クライアント側でセッション管理(画面表示だけで完結するログイン)
- Server Components(サーバーで動くReact)との相性が悪い
@supabase/ssr
- Cookieベースの認証(ログイン情報がサーバーに届くので、Next.js と噛み合う)
- サーバー側でのセッション管理に対応(認証をサーバーが見るので安全度UP)
- Next.js App Routerに最適化(Supabase公式が「Next.js 最新構成と組みやすいように」作ってくれている)
- PKCEフローをデフォルトで使用(パスワードを送らずに安全に認証する最新の方式、supabase-jsでも使えるが手動設定が必要)
移行しないとどうなる?
複数アプリを連携する場合、認証方式が異なると以下の問題が発生します:
- セッションの共有ができない:アプリ間でログイン状態が同期されない
- 認証フローの不一致:localStorageとCookieの混在でバグの温床に
- 将来的なメンテナンス困難:古い方式のサポートは終了予定
「じゃあ、@supabase/supabase-jsに統一すればいいのでは?」
@supabase/supabase-js(localStorage方式)だとこうなる。
出荷アプリ (shipment.cropgarage.com)
└─ localStorage: { session: "xxx" }
栽培アプリ (cultivation.cropgarage.com)
└─ localStorage: { session: null } ← 別ドメイン扱い
localStorageはドメインごとに独立しているため、Supabaseプロジェクトが同じでも、サブドメインが違えばセッション共有できません。
つまり、@supabase/supabase-jsに統一しても、複数アプリ連携の問題は解決しない。
複数アプリを連携する場合、以下の2つが必要:
- Cookieベースの認証(ドメイン間でセッション共有可能)
- Next.js App RouterのSSR対応(Server Componentsで認証情報にアクセス)
この両方を満たすのが@supabase/ssrのようだ。
私の場合、出荷管理アプリ・栽培管理アプリ・分析アプリ(将来的に)の3つを連携させる予定だったため、今のうちに統一しておく必要があると判断。
@supabase/supabase-jsはもう使わないのか?
ここで疑問が浮かびます。「じゃあ、@supabase/supabase-jsは完全に不要になったの?」
答え: 100%ではない
実は、@supabase/supabase-jsを使うべきケースもあります。
@supabase/supabase-jsを使うべき場合:
1. Edge Functions(Supabase Functions)
// Supabase Edge Function内
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, anonKey)
Edge Functionsのようなサーバーサイド専用の環境では、Cookie管理が不要なため、直接@supabase/supabase-jsを使います。
2. サーバー管理タスク(SERVICE_ROLEキー使用)
// 管理用クライアント
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
url,
SERVICE_ROLE_KEY, // 管理者権限
{ auth: { persistSession: false } }
)
@supabase/ssrはSERVICE_ROLEキーの使用を制限しています。管理タスクには@supabase/supabase-jsが必要です。
3. サードパーティ認証を使う場合
Better-authなどのサードパーティ認証を使う場合、@supabase/ssrは使えず、@supabase/supabase-jsを使う必要があります。
4. クライアントのみのSPA
SSRが不要で、localStorageベースの認証で問題ない場合は@supabase/supabase-js
使い分けの判断基準:
| ケース | 使うべきパッケージ |
|---|---|
| Next.js App Router | @supabase/ssr |
| SSRフレームワーク全般 | @supabase/ssr |
| (パスワードを送らずに安全に認証する最新の方式) | @supabase/supabase-js |
| 管理タスク(SERVICE_ROLE) | @supabase/supabase-js |
| サードパーティ認証 | @supabase/supabase-js |
| クライアントのみのSPA | @supabase/supabase-js |
私のケースでの判断
3アプリ構成(Next.js App Router使用)で、将来的に連携する予定があるため、100%@supabase/ssrで統一するべきだと思いました。
複数アプリ連携での@supabase/supabase-jsの問題
「Supabaseプロジェクトは同じなんだから、@supabase/supabase-jsでも連携できるんじゃないの?」
と、先ほど触れましたが、supabase/supabase-jsでは各アプリでログインが必要になるので面倒極まりない。
以下により詳細。
localStorageの制限
【同一Supabaseプロジェクト】
Project ID: abc123
Database: 同じ
【でもアプリは別ドメイン】
出荷アプリ: shipment.cropgarage.com
└─ localStorage (独立)
栽培アプリ: cultivation.cropgarage.com
└─ localStorage (独
localStorageはドメインごとに独立しているため、Supabaseプロジェクトが同じでも、セッション共有できません。
実際にどうなるか?
❌ @supabase/supabase-js(localStorage方式)の場合:
// 出荷アプリでログイン
const supabase = createClient(SAME_PROJECT_URL, SAME_ANON_KEY)
await supabase.auth.signIn({ email, password })
// shipment.cropgarage.comのlocalStorageに保存
// 栽培アプリに移動(cultivation.cropgarage.com)
// → localStorageは別なので、未ログイン状態 ❌
// ユーザーは再度ログインが必要
ユーザー体験が最悪です。アプリを移動するたびにログインが必要になります。
✅ @supabase/ssr(Cookie方式)の場合:
// 出荷アプリでログイン
const supabase = createClient()
await supabase.auth.signIn({ email, password })
// Cookie (domain=.cropgarage.com) に保存
// 栽培アプリに移動(cultivation.cropgarage.com)
// → Cookieが共有されるので、ログイン済み ✅
// シームレスな体験
Cookieは親ドメインを共有できるため、一度ログインすれば全アプリで認証済みになります。
同一ドメインならOK?
「じゃあ、全部同じドメインにすればいいじゃん」と思うかもしれませんが:
cropgarage.com/shipment → 出荷アプリ
cropgarage.com/cultivation → 栽培アプリ
cropgarage.com/analytics → 分析アプリ
これは1つのアプリをルーティングで分けただけなので、複数アプリではなくなります。
規模が大きくなると、1つのアプリで全部管理するのは大変です。
まとめ: 複数アプリ連携なら@supabase/ssr
| 要件 | @supabase/supabase-js | @supabase/ssr |
|---|---|---|
| 複数サブドメイン間でセッション共有 | ❌ 不可能 | ✅ 可能 |
| ユーザー体験 | ❌ 各アプリで再ログイン | ✅ 1回で全アプリOK |
| 実装の手間 | ❌ 独自の仕組みが必要 | ✅ 標準機能 |
「複数アプリを連携する」という時点で、@supabase/ssrなのかな?と。
移行のタイミング
いつから@supabase/ssrが主流になったのか
調査の結果、以下のタイムラインが判明しました:
- 2024年5月〜6月:公式リリース(Consolidation Month)
- 2024年6月時点:まだベータ版(v0.x)
- 2025年11月現在:v0.7.0、公式推奨パッケージに
2025年6月に出荷アプリを作った際、ChatGPTが@supabase/supabase-jsを推奨したのは、当時まだベータ版だったため保守的な判断をしたと考えられます。
しかし現在は@supabase/ssrが明確に推奨されているらしい・・・とClaudeの情報。
実際の移行手順
前提条件
- Next.js 14以降(App Router使用)
- 既存プロジェクトで
@supabase/supabase-jsを使用中 - Gitでバージョン管理している
ステップ1: ブランチを切る
取り返しがつかなくなる可能性を考え、まず作業用ブランチを作成します。
# 現在の状態をコミット
git add .
git commit -m "移行前の安定版: OCR改善後の状態"
# 移行用ブランチ作成
git switch -c feature/ssr(自分のわかりやすいブランチ名)
ステップ2: パッケージをインストール
pnpm install @supabase/ssr @supabase/supabase-js
ステップ3: 3つの定型ファイルを作成
@supabase/ssrでは、用途に応じて3種類のクライアントを使い分けます。
1. Client Component用(ブラウザ)
src/utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
2. Server Component用
src/utils/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から呼ばれた場合は無視
}
},
},
}
)
}
3. Middleware用
src/utils/supabase/middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let response = 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, options }) => {
request.cookies.set(name, value);
response.cookies.set(name, value, options);
});
response = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
}
);
// セッションをリフレッシュ
await supabase.auth.getUser();
return response;
}
4. ルート直下のMiddleware
src/middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
ステップ4: 既存コードの置き換え(出荷アプリの変更)
影響範囲の確認
まず、変更が必要なファイル数を確認します。
grep -r "from.*@/lib/supabase" --include="*.ts" --include="*.tsx" src/
私の場合は16ファイルでした。
Client Componentの判別
“use client”がないファイルを特定します。
for file in $(grep -rl "@/lib/supabase" src/ --include="*.ts" --include="*.tsx"); do
if ! grep -q "use client" "$file"; then
echo "$file"
fi
done
結果、src/hooks/useDestinationMaster.tsの1ファイルのみがServer Component用でした。
VSCodeで一括置換
置換1: インポート文
- 検索:
import { supabase } from "@/lib/supabase" - 置換:
import { createClient } from "@/utils/supabase/client"
置換2: 使用方法の変更
各ファイルで以下のように変更が必要です。
// 変更前
import { supabase } from "@/lib/supabase"
function MyComponent() {
const { data } = await supabase.from('table').select()
// ...
}
// 変更後
import { createClient } from "@/utils/supabase/client"
function MyComponent() {
const supabase = createClient()
const { data } = await supabase.from('table').select()
// ...
}
AIエディタ(Cursor等)を使うと、この変更を一括で適用できます。
ステップ5: 動作確認
以下の項目を必ず確認します。
✅ 認証機能
- ログインができるか
- ログアウトができるか
- セッションが維持されるか
✅ データアクセス
- データの取得ができるか
- データの更新ができるか
- RLS(Row Level Security)が正しく動作するか
✅ マルチテナント機能
- 他の会社のデータが見えないか
- 自分の会社のデータだけ表示されるか
すべて問題なければ移行完了です。
ステップ6: 旧ファイルの削除とコミット
# 旧ファイルを削除
rm src/lib/supabase.ts
# コミット
git add .
git commit -m "移行完了: @supabase/ssrに統一"
移行後のメリット
技術スタックの統一
複数アプリで同じ認証方式を使うことで、以下が実現できます:
- セッションの共有
- コードの再利用性向上
- メンテナンス性の向上
最新のベストプラクティスに準拠
公式推奨の方法に従うことで:
- セキュリティの向上
- 将来的なアップデートへの対応
- コミュニティのサポートを受けやすい
Server Componentsとの親和性
Next.js App Routerの機能をフル活用できます:
- Server Componentsでのデータフェッチ
- Server Actionsでのデータ更新
- 適切なキャッシュ戦略
まとめ
当初は「また足止めか…」と思いましたが、将来のアプリ連携を考えると前もってやっておいたほうがいいというのが正直な感想です。
また変わるかもしれませんが、絶対に今の流れを覚えてない自信満々なので、いざログインが面倒になったときに「なんでだろう」「おかしいな」ってなるリスクはちょっとは減ったんではないかと思っています。
参考リンク
