九州北部が梅雨明けしたと思ったら、毎日のように熱中症アラート。
体力を削り取られないように朝の農作業が勝負な時期となっている。
それはさておき、前回のブログで触れたのだが、農業向けの在庫・出荷管理アプリの開発を進めている。
今回は実際に実装した認証システムとページアクセス制御について詳しく紹介します。
まずは開発の進捗報告から。
本当にざっくりですが、アプリの基本機能は形になってきました。
まずはユーザー登録とログイン機能をとりあえず実装しました。
ユーザー登録とログインの導入
まずは、Supabase の auth
機能を使って、メールアドレスとパスワードで新規登録(サインアップ)できるようにしました。
const { error } = await supabase.auth.signUp({
email,
password,
});
現在のところ、サインアップ処理はこれだけで完了していて、Supabaseの auth.users
にユーザーが作成されるのみです。

その後、開発中は Supabase のコンソールから、
手動で「ユーザーID」「役割(role)」「会社名(company_name)」「会社種別(company_type)」を登録しています。
実際の制御は「ユーザーID」と「役割(role)」のみで行っており、会社情報は参考程度に留まっています。
アプリ側からの自動登録はまだ実装していません。
理想と現実のギャップ
本来は、以下のように signUp()
成功後に users
テーブルへ .insert()
する処理を入れれば、登録フローを自動化できるはずなんですが、
なんかうまくいかないので、とりあえずそのあたりは後回しにして、先に進めることにしました。
await supabase.from("users").insert({
id: user.id,
email: user.email,
role: "farmer", // or 'trader'
company_name: "○○農園",
company_type: "個人"
});
なお、開発中に生産者と流通業者の両方を試験的に作成・確認したい場面が多いため、メールアドレスは Gmail のエイリアス(example+1@gmail.com
など)を使って複数アカウントを作っています。
これにより、ひとつの受信ボックスで複数のアカウントを試せるので便利です。
usersテーブルの設計と運用
手動で管理しているusers
テーブルの構造は以下のようになっています:

id (uuid): auth.usersのUIDと一致(主キー)
created_at (timestamptz): 登録日時
email (text): ログイン用メールアドレス
role (text): “farmer”(生産者), “trader”(流通業者), “admin”(管理者)company_name (text): 会社名・農園名(参考程度
company_type (text): “個人”, “法人”, “内部”(参考程度)
実際のアクセス制御では**id
とrole
のみを使用**しており、company系の情報は将来の拡張を見越して作成したものの、現在は実質的に使用していません。
Row Level Security(RLS)の設定
Supabaseの特徴でもある**RLS(行レベルセキュリティ)**を使って、各ユーザーがアクセスできるデータを制限しました。
🔸 ログイン後の role 判定に必要な SELECT ポリシー
ログイン処理が完了したあと、アプリ側では次のように users
テーブルから 自分の「役割(role)」を取得して、画面を振り分ける処理を行っています。
ログイン成功後、ユーザーのroleを取得して振り分け
const { data: { user } } = await supabase.auth.getUser();
const { data: profile, error: profileError } = await supabase
.from("users")
.select("role")
.eq("id", user?.id)
.single();
このとき、Supabaseでは Row Level Security(RLS) が有効になっているため、users
テーブルに対して 明示的に SELECT ポリシーを設定しておかないと、読み取りが拒否されてしまいます。
そのため、次のようなポリシーを追加しています:
policy "Users can read their own user info"
on users
for select
using (id = auth.uid());
このポリシーによって、ログイン中のユーザーが、自分自身の users
レコードだけを参照できるようになります。
🔹 他人の情報は見られない設計
この制限によって、生産者や流通業者が他のユーザー情報を読み取ることはできません。
あくまで「ログイン直後の自分の役割確認」用途のみに限定した、安全なアクセスになっています。
管理者権限的な概念も少し検討しましたが、「グループ単位 + 役割」で十分柔軟に制御できると判断しました。
Next.js 側での認証状態管理とページ制御
認証チェックの方法(Client Components)
Next.js では useEffect
でセッション状態を取得して、ログイン済みかどうかを確認します。
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
if (!session) {
router.push("/login");
}
});
}, []);
役割に応じたリダイレクト
ログイン後に public.users
テーブルを参照して、ユーザーの role
をチェック。ページ遷移を制御します。
下のコードからfarmer(生産者)がログインすれば、出荷登録フォームへ(new-shipment.tsx)
trader(流通業者)がログインすれば、出荷予定表フォームへ(planned-shipment.tsx)
if (role === "farmer") {
router.push("/auth/new-shipment");
} else if (role === "trader") {
router.push("/auth/planned-shipments");
}
UIの切り替え
ヘッダーのメニューやボタンなども、ユーザーの役割に応じて表示・非表示を切り替えることで、UXを損なわずにアクセス制御ができます。
実装してみての感想
SupabaseのRLSを使えば、フロント側で過剰に制御しなくてもデータアクセスを安全に制限できるのが非常に便利でした。
一方で、Next.js 側ではログインチェック・ページ制御のタイミングに注意が必要で、認証情報の取得が遅れると画面が一瞬ブレたりする点もあるため、UX改善の余地があります。
次回
次回は「出荷先マスタ登録機能の実装」について書く予定です。