Next.js + Supabaseで農業向け栽培管理アプリを開発しています。
今回はデモ環境を作ってワンクリックログインを実装した話です。
デモ環境について
自分が実際に農業をしながら、作ったアプリにポチポチとデータを入れています。圃場の登録、作業記録、収穫記録など、日々の農作業データをそのまま本番データとして蓄積しています。
デモ環境には、その本運用データを定期的に移しています。つまりデモにはリアルな農業データが入っている状態です。
出荷アプリのデモ環境は、訪問者がメールアドレスやパスワードを入力する形にしてましたが、それはそれで面倒なので、ボタン一発で入れる仕組みに変えました。
やりたかったこと
- デモ版:「デモを体験する」ボタン一発でログイン → アプリへ
- 本運用版:通常のログイン画面
- コードは共通、Vercelの環境変数で切り替え
Vercelにデモ用と本運用用の2つのプロジェクトを用意して、環境変数だけ変えることでUIを出し分けます。
# デモVercelプロジェクトのみ設定
NEXT_PUBLIC_DEMO_MODE=true
[email protected] # NEXT_PUBLIC_禁止(セキュリティのため)
DEMO_PASSWORD=xxxxxxxx # NEXT_PUBLIC_禁止(セキュリティのため)
DEMO_EMAILとDEMO_PASSWORDにNEXT_PUBLIC_をつけてはいけません。NEXT_PUBLIC_をつけるとブラウザ側のJavaScriptに値が露出してしまいます。サーバー側だけで使う値は必ずプレフィックスなしにしましょう。
最初の実装(うまく動かなかった)
まず一番シンプルな方法として、/demo/page.tsxをServer Componentで作りました。
// src/app/demo/page.tsx(最初の実装・動かなかった版)
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
export default async function DemoLoginPage() {
const supabase = await createClient();
// デモアカウントでログイン
const { error } = await supabase.auth.signInWithPassword({
email: process.env.DEMO_EMAIL!,
password: process.env.DEMO_PASSWORD!,
});
if (error) {
// エラー処理
}
redirect("/fields"); // ログイン後にアプリへ
}
一見シンプルで正しそうに見えます。でも実際に動かすとログイン画面に戻ってしまいます。
なぜ動かなかったのか
結論からいうと原因は、Server Componentではログイン後のCookieを書き込めなかったことです。
原因を理解するには、以前の記事で説明したcreateClient()の仕組みを知る必要があります。
ここでいう createClient() は、Supabaseに接続するための準備をする関数です。
どの場所で使うかによって、Cookieを書き込めるかどうかが変わります。
前回の実装でsrc/utils/supabase/server.tsのset()とremove()を空にしました。
// src/utils/supabase/server.ts
export async function createClient() {
return createServerClient(..., {
cookies: {
get(name) {
return cookieStore.get(name)?.value;
},
set() {
// 空!何もしない
},
remove() {
// 空!何もしない
},
},
});
}
空にした理由は前回記事で説明しましたが、改めて整理します。
Cookieを「メモ書き」で例えると
ログイン状態はブラウザのCookieという場所に「ログイン中ですよ」というメモとして保存されます。
Next.jsには2種類の処理場所があります。
- Server Component(page.tsxなど):Cookieの読み取りはOK、書き込みはNG(Next.jsの仕様)
- Route Handler / Server Action:Cookieの読み書きどちらもOK
set()を空にしたのは「Server Componentでエラーが出ないようにするため」でした。
でも今回の失敗はこういう流れで起きていました。
/demo にアクセス(Server Component)
↓
createClient()(set()が空の版)でログイン処理
↓
Supabaseが「ログイン成功!Cookieに保存しとくね」
↓
set()が空なので何もしない → Cookieに何も保存されない
↓
redirect("/fields")
↓
「Cookieないですよね?ログインしてください」→ ログイン画面に戻される
ログイン自体は成功しているのに、その証拠(Cookie)が保存されないので、次のページに移動した瞬間に「未ログイン」と判断されてしまいます。
Next.js + Supabaseでデモログインを作る方法
ログイン処理をRoute Handler(/api/demo-login)に切り出して、Cookieに書き込めるcreateActionClient()を使います。
ファイル構成
src/app/
├── demo/
│ ├── page.tsx ← Server Component(ログイン済みチェックのみ)
│ └── DemoLoginClient.tsx ← Client Component(APIを呼ぶ)
├── api/
│ └── demo-login/
│ └── route.ts ← Route Handler(実際のログイン処理)
└── utils/supabase/
├── server.ts ← Server Component用(set()空)
└── server-action.ts ← Route Handler / Server Action用(set()あり)
① server-action.ts(Cookieに書き込める版)
// src/utils/supabase/server-action.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createActionClient() {
const cookieStore = cookies(); // Next.js 15以降はawaitが必要な場合あり(バージョン確認を)
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 }); // ちゃんと書き込む
},
remove(name: string, options?: CookieOptions) {
cookieStore.set({ name, value: "", ...options, maxAge: 0 });
},
},
}
);
}
※ cookies()のawaitについて:Next.js 15以降ではcookies()が非同期になっているため、await cookies()が必要です。Next.js 14以前では不要です。使っているバージョンに合わせてください。
② route.ts(ログイン処理本体)
// src/app/api/demo-login/route.ts
import { createActionClient } from "@/utils/supabase/server-action";
import { NextResponse } from "next/server";
export async function POST() {
const email = process.env.DEMO_EMAIL;
const password = process.env.DEMO_PASSWORD;
if (!email || !password) {
return NextResponse.json({ ok: false, error: "環境変数が未設定です" }, { status: 500 });
}
const supabase = await createActionClient(); // 書き込める版を使う
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
return NextResponse.json({ ok: false, error: error.message }, { status: 401 });
}
return NextResponse.json({ ok: true }); // Cookieが正しく保存される
}
③ DemoLoginClient.tsx(APIを呼ぶClient Component)
// src/app/demo/DemoLoginClient.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function DemoLoginClient() {
const router = useRouter();
useEffect(() => {
fetch("/api/demo-login", { method: "POST" })
.then((res) => res.json())
.then((data) => {
if (data.ok) {
router.push("/fields"); // ログイン成功したらアプリへ
}
});
}, [router]);
return (
デモ環境を準備中..
そのままお待ちください
);
}
④ demo/page.tsx(ログイン済みチェックのみ)
// src/app/demo/page.tsx
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
import DemoLoginClient from "./DemoLoginClient";
export default async function DemoLoginPage() {
const email = process.env.DEMO_EMAIL;
const password = process.env.DEMO_PASSWORD;
if (!email || !password) {
return 環境変数 DEMO_EMAIL / DEMO_PASSWORD を設定してください。;
}
// 既にログイン済みならそのままアプリへ
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (user) redirect("/fields");
// 未ログインならClient Componentでログイン処理
return ;
}
成功した流れ
/demo にアクセス
↓
Server Component:ログイン済みチェック(読み取りのみ)
↓
未ログインなら DemoLoginClient を表示
↓
Client Component が /api/demo-login を呼ぶ
↓
Route Handler:createActionClient()でログイン処理
↓
set()がCookieにちゃんと保存する
↓
Client Componentが /fields に移動
↓
「Cookieありますね、どうぞ」→ アプリ表示OK ✅
まとめ
| 場所 | Cookieの書き込み | 使うclient |
|---|---|---|
| Server Component(page.tsx等) | ❌ NG | createClient()(set()空) |
| Route Handler / Server Action | ✅ OK | createActionClient()(set()あり) |
今回のポイントは
Server Component はCookieの読み取りはできても、ログイン状態を書き込む処理には向きません。
ログイン処理は Route Handler や Server Action に分けると覚えておけばOKです。
(余談)本運用データをデモ環境に移す方法
農業作業のデータ移行はGitHub ActionsとSupabaseのバックアップ+リストアスクリプトで運用しています。詳細な手順は Supabaseのバックアップ・リストア実装の記事に書いていますが、ここでは流れだけ紹介します。
Step 1:バックアップをダウンロード
- GitHub → Actions → 「Supabase DB weekly backup」を開く
- 最新の成功した実行をクリック
- 下部の「Artifacts」→「supabase-db-backup」をダウンロード
- ZIPを解凍して
.dumpファイルをbackups/フォルダに移動
Step 2:デモDBにリストア
# デモDBに復元
bash scripts/restore.sh demo backups/supabase_〇〇.dump
これで本運用のデータがそのままデモ環境に反映されます。
注意:リストアするとRLSポリシーも上書きされる
ここが一番ハマりやすいポイントです。.dumpファイルにはテーブルデータだけでなくRLS(Row Level Security)ポリシーも含まれています。そのためリストア後は、本運用のRLSポリシーがデモ環境に上書きされた状態になります。
デモ環境では書き込みを禁止したいため、リストア後にデモ用のRLSポリシーを再適用する必要があります。具体的には:
- 本運用のINSERT / UPDATE / DELETEポリシーをすべてDROP
- SELECTのみ許可するポリシーに差し替え
この再適用を忘れると、デモユーザーがデータを書き換えられる状態になってしまうので要注意です。自分はリストアスクリプトの後処理としてSQLを自動実行する形にして、うっかり忘れを防いでいます。
