OmochiXを検索

Esc で閉じる

Next.js でログインとダッシュボード付きの Web アプリを作る

メール + パスワードのログインと、自分のデータだけを扱うダッシュボードを Next.js と Supabase(Auth + Postgres + RLS)で作る。

中級 約240分 最終検証 Claude Code Cursor
プレビュー(サンドボックス内で動作。送信などは模擬です)

この教材で作るもの

サインアップ・ログイン・ログアウト、ログイン後のダッシュボードでメモの追加・編集・削除ができる Web アプリ。他のユーザーのメモは API を直接叩いても見えない。Vercel で公開済み。

動作確認環境: Node.js 20 / Next.js 15(App Router、Server Actions) / @supabase/ssr / Supabase(Postgres + Auth、2026年10月時点の無料枠) / Playwright(E2E)

前提条件

  • GitHub アカウントと Vercel アカウント
  • Supabase の無料プロジェクト(Auth のメール確認を有効にする)
  • Node.js 20 以上、Claude Code か Cursor
  • 先に読む: ログイン機能を追加する / データベースを追加する(Learn Tutorials)

実装手順

  1. 作るものを決める

    この教材では「自分だけのメモ」を扱います。あなたのアプリでは、ユーザーごとに分かれていなければならないデータを 1 つ選んでください(予約・日報・在庫など)。

    確認: テーブルにする対象が 1 つ決まった

  2. Supabase でテーブルと RLS を作る

    AI に SQL を出してもらい、Supabase の SQL エディタで実行します。RLS を有効にする前にデータを入れないでください。

    AI に渡すプロンプトデータ設計と RLS ポリシーclaude-code
    Supabase(Postgres)で「ユーザーごとのメモ」を扱うテーブルを設計してください。
    
    要件:
    - テーブル notes: id (uuid, 既定値 gen_random_uuid()), user_id (auth.users への参照), title (text, 1〜120 文字), body (text, 最大 5000 文字), created_at, updated_at
    - RLS を有効にし、select / insert / update / delete のすべてで auth.uid() = user_id だけを許可するポリシー
    - updated_at を自動更新するトリガー
    
    出力: Supabase の SQL エディタにそのまま貼れる SQL と、各ポリシーが何を防ぐかの 1 行説明。service role key を使う前提の設計にはしないこと。
    コード例supabase/notes.sql
    create table if not exists public.notes (
      id uuid primary key default gen_random_uuid(),
      user_id uuid not null references auth.users (id) on delete cascade,
      title text not null check (char_length(title) between 1 and 120),
      body text not null default '' check (char_length(body) <= 5000),
      created_at timestamptz not null default now(),
      updated_at timestamptz not null default now()
    );
    
    alter table public.notes enable row level security;
    
    create policy "own notes: select" on public.notes for select using (auth.uid() = user_id);
    create policy "own notes: insert" on public.notes for insert with check (auth.uid() = user_id);
    create policy "own notes: update" on public.notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
    create policy "own notes: delete" on public.notes for delete using (auth.uid() = user_id);

    確認: Table Editor で notes が見え、RLS が ON になっている

  3. プロジェクトを作り、環境変数を置く

    npx create-next-app@latest でプロジェクトを作り、.env.local に URL と anon key を入れます。.env.example はプレースホルダーだけです。

    コード例.env.example
    # 公開してよいのはこの 2 つだけ。service role key はここにも書かない
    NEXT_PUBLIC_SUPABASE_URL=<ここに貼る:Supabase プロジェクトの URL>
    NEXT_PUBLIC_SUPABASE_ANON_KEY=<ここに貼る:anon key(RLS があるので公開可)>
    # E2E テスト用(ローカルと CI のみ)
    E2E_EMAIL=<ここに貼る:テスト用ユーザーのメール>
    E2E_PASSWORD=<ここに貼る:テスト用ユーザーのパスワード>

    確認: git ls-files .env.local が空

  4. 認証 UI を作る

    サインアップ・ログイン・ログアウトと、/dashboard の保護を作ります。確認メールが届いてからログインできることを確かめます。

    AI に渡すプロンプト認証 UI(サインアップ / ログイン / ログアウト)claude-code
    Next.js(App Router)と @supabase/ssr で、メール + パスワードの認証を作ってください。
    
    画面:
    - /signup: メール・パスワード(8 文字以上)・確認。登録後は確認メールの案内
    - /login: メール・パスワード。失敗時は理由を表示(存在しないメールか間違いかは区別しない)
    - ログアウトはヘッダーのボタン
    - /dashboard は未ログインなら /login へ 302(middleware で保護)
    
    条件:
    - クライアントには NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY だけを渡す。service role key は使わない
    - セッションは cookie(@supabase/ssr の推奨どおり)
    - フォームは aria-live でエラーを読み上げる
    - 環境変数は .env.example にキー名とプレースホルダー(<ここに貼る:…>)だけ書く
    コード例middleware.ts
    import { NextResponse, type NextRequest } from 'next/server';
    import { updateSession } from '@/lib/supabase/middleware';
    
    export async function middleware(request: NextRequest) {
      const { response, user } = await updateSession(request);
      if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
        const url = request.nextUrl.clone();
        url.pathname = '/login';
        return NextResponse.redirect(url);
      }
      return response;
    }
    
    export const config = { matcher: ['/dashboard/:path*'] };

    確認: 未ログインで /dashboard を開くと /login に行く

  5. ダッシュボードのメモ CRUD を作る

    一覧・追加・編集・削除を Server Actions で作ります。user_id は必ずサーバーで付けます。

    AI に渡すプロンプトダッシュボードのメモ CRUDclaude-code
    /dashboard にメモの一覧・追加・編集・削除を作ってください。
    
    条件:
    - Server Actions で insert / update / delete を行い、必ずログインユーザーの user_id を付ける(クライアントから user_id を受け取らない)
    - 一覧はサーバーコンポーネントで取得し、空のときは「まだメモがありません」
    - 追加フォームは送信中に無効化、完了後にリストを再検証(revalidatePath)
    - 削除は確認ダイアログ付き
    - 読み込み中はスケルトン、完了時は小さなトーストで知らせる
    - 375px で 1 カラムになる
    コード例app/dashboard/actions.ts
    'use server';
    
    import { revalidatePath } from 'next/cache';
    import { createClient } from '@/lib/supabase/server';
    
    export async function addNote(formData: FormData) {
      const supabase = await createClient();
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) return { ok: false, message: 'ログインが必要です' };
    
      const title = String(formData.get('title') ?? '').trim();
      if (title.length < 1 || title.length > 120) return { ok: false, message: 'タイトルは 1〜120 文字です' };
    
      // user_id はサーバーで付ける(クライアントからは受け取らない)
      const { error } = await supabase.from('notes').insert({ title, user_id: user.id });
      if (error) return { ok: false, message: '保存できませんでした' };
    
      revalidatePath('/dashboard');
      return { ok: true };
    }

    確認: 追加したメモが一覧に出て、削除で消える

  6. 別ユーザーで分離を確かめる

    2 つ目のユーザーでログインし、1 つ目のメモが見えないこと、id を指定した更新・削除も失敗することを確かめます。これが RLS の効果です。

    AI に渡すプロンプトセキュリティのセルフチェックclaude-code
    このアプリについて、次を確認して結果を表にしてください。
    
    - RLS が notes で有効で、4 つの操作すべてにポリシーがある
    - Server Action が user_id をクライアントから受け取っていない
    - service role key がクライアント側のコードと .env.example に無い
    - 未ログインで /dashboard を開くと /login に行く
    - 別ユーザーのメモ id を指定して update / delete を呼んでも失敗する(curl の手順も書く)
    - エラーメッセージに他ユーザーの情報やスタックトレースが出ない
    
    問題があれば修正案も出してください。

    確認: 別ユーザーのメモは一覧にも API にも出ない

  7. E2E テストを 3 本書く

    ログイン保護・追加と削除・ログアウト後の保護を Playwright で自動化します。テスト用ユーザーの情報は環境変数から読みます。

    AI に渡すプロンプトE2E テスト 3 本claude-code
    Playwright で次の 3 本の E2E テストを書いてください。テスト用ユーザーは環境変数 E2E_EMAIL / E2E_PASSWORD から読み、コードに書かないこと。
    
    1. 未ログインで /dashboard を開くと /login に遷移する
    2. ログインしてメモを追加すると一覧に表示され、削除すると消える
    3. ログアウト後に /dashboard を開くと /login に戻る
    
    npm run test:e2e で動くようにし、CI で動かす場合の注意点を最後に 3 行で書いてください。

    確認: npm run test:e2e が 3 本とも通る

  8. Vercel に公開する

    Vercel に環境変数(公開してよい 2 つ)を登録してデプロイします。Supabase の Auth 設定で、公開 URL を Site URL と Redirect URLs に追加します。

    確認: 公開 URL でサインアップから削除まで一通りできる

テスト・検証

チェックはこのブラウザにだけ保存されます。

検証項目
済項目やり方期待結果
ログアウトした状態で /dashboard を開く /login に 302 で戻る
2 ユーザーで交互にログインして一覧を見る 相手のメモは一覧に出ない
ユーザー B のセッションで、ユーザー A のメモ id を指定して update / delete を呼ぶ(Supabase REST を curl) 0 行が対象になり、A のメモは変わらない
追加フォームの送信に user_id を混ぜて送る 無視され、ログインユーザーの id で保存される
ビルド後の JS を SUPABASE_SERVICE_ROLE で検索する 見つからない
/login の「パスワードを忘れた」から再設定する メールが届き、新しいパスワードでログインできる
npm run test:e2e 3 本すべて成功
端末エミュレーションで /login と /dashboard を見る 1 カラムになり、横スクロールが出ない

安全性・秘密情報

  • anon key は公開前提だが、RLS が有効で正しいポリシーがあることが前提。RLS なしで公開しない
  • service role key はサーバーの秘密。この教材では一切使わず、.env.example にも書かない
  • user_id はサーバー(Server Action)で付け、クライアントからの値を信用しない
  • エラーメッセージに他ユーザーの情報やスタックトレースを出さない
  • テスト用ユーザーの認証情報は環境変数に置き、コードに書かない
  • パスワードは 8 文字以上、確認メールを有効にする

「<ここに貼る:…>」の形で書かれた値は、環境変数など公開されない場所に置きます。コード・リポジトリ・このページには入れません。

公開手順

  1. Vercel の Environment Variables に NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY を登録する
  2. Supabase の Authentication → URL Configuration に公開 URL を Site URL と Redirect URLs として追加する
  3. 公開 URL でサインアップ → 確認メール → ログイン → メモ追加 → 削除を 1 周する
  4. 独自ドメインを設定し、HTTPS を確認する

よくあるエラー

ログイン後に /dashboard を開いても /login に戻される

原因: cookie ベースのセッションが middleware で更新されていない、または Supabase の Site URL / Redirect URLs に公開 URL が無い

対処: @supabase/ssr の updateSession を middleware で呼ぶ。Supabase の URL Configuration に公開 URL を追加する

メモを追加しても一覧に出ない

原因: insert は成功しているが revalidatePath を呼んでいない、または RLS の select ポリシーが無い

対処: Server Action の最後で revalidatePath('/dashboard') を呼ぶ。4 つのポリシーが揃っているか確認する

insert が「new row violates row-level security policy」で失敗する

原因: insert の with check が auth.uid() = user_id なのに、user_id を付けていない

対処: Server Action でログインユーザーの id を user_id に入れる

他人のメモが見えてしまう

原因: RLS を有効化していない(alter table … enable row level security を実行していない)

対処: RLS を有効化し、ポリシーを作り直す。公開前に別ユーザーで必ず確認する

E2E が CI だけ落ちる

原因: CI に E2E_EMAIL / E2E_PASSWORD が無い、または確認メール待ちのユーザーを使っている

対処: CI の Secrets に設定し、確認済みのテスト用ユーザーを使う

AIへの実装指示書

この教材の手順・プロンプト・検証・安全性を 1 枚にまとめた Markdown です。コピーして Claude Code や Codex に渡します。

# 実装指示: Next.js でログインとダッシュボード付きの Web アプリを作る
このファイルの指示に従って実装してください。分からない点は実装前に質問してください。

## 概要
- 教材: https://omochix.com/learn/practice/nextjs-login-dashboard/
- 難易度: 中級 / 所要 約240分
- 対応AI: Claude Code / Cursor
- 使用技術: Next.js / Supabase / TypeScript / Vercel
- 最終検証日: 2026-10-21

## 作るもの
サインアップ・ログイン・ログアウト、ログイン後のダッシュボードでメモの追加・編集・削除ができる Web アプリ。他のユーザーのメモは API を直接叩いても見えない。Vercel で公開済み。

## 前提条件
- GitHub アカウントと Vercel アカウント
- Supabase の無料プロジェクト(Auth のメール確認を有効にする)
- Node.js 20 以上、Claude Code か Cursor
- 先に読む: ログイン機能を追加する / データベースを追加する(Learn Tutorials)

## 動作確認環境
- Node.js 20
- Next.js 15(App Router、Server Actions)
- @supabase/ssr
- Supabase(Postgres + Auth、2026年10月時点の無料枠)
- Playwright(E2E)

## 実装手順

### STEP 1: 作るものを決める
この教材では「自分だけのメモ」を扱います。あなたのアプリでは、ユーザーごとに分かれていなければならないデータを 1 つ選んでください(予約・日報・在庫など)。

- 確認: テーブルにする対象が 1 つ決まった

### STEP 2: Supabase でテーブルと RLS を作る
AI に SQL を出してもらい、Supabase の SQL エディタで実行します。RLS を有効にする前にデータを入れないでください。

AI に渡すプロンプト(データ設計と RLS ポリシー):
```text
Supabase(Postgres)で「ユーザーごとのメモ」を扱うテーブルを設計してください。

要件:
- テーブル notes: id (uuid, 既定値 gen_random_uuid()), user_id (auth.users への参照), title (text, 1〜120 文字), body (text, 最大 5000 文字), created_at, updated_at
- RLS を有効にし、select / insert / update / delete のすべてで auth.uid() = user_id だけを許可するポリシー
- updated_at を自動更新するトリガー

出力: Supabase の SQL エディタにそのまま貼れる SQL と、各ポリシーが何を防ぐかの 1 行説明。service role key を使う前提の設計にはしないこと。
```

コード例(supabase/notes.sql):
```sql
create table if not exists public.notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references auth.users (id) on delete cascade,
  title text not null check (char_length(title) between 1 and 120),
  body text not null default '' check (char_length(body) <= 5000),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

alter table public.notes enable row level security;

create policy "own notes: select" on public.notes for select using (auth.uid() = user_id);
create policy "own notes: insert" on public.notes for insert with check (auth.uid() = user_id);
create policy "own notes: update" on public.notes for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
create policy "own notes: delete" on public.notes for delete using (auth.uid() = user_id);
```

- 確認: Table Editor で notes が見え、RLS が ON になっている

### STEP 3: プロジェクトを作り、環境変数を置く
npx create-next-app@latest でプロジェクトを作り、.env.local に URL と anon key を入れます。.env.example はプレースホルダーだけです。

コード例(.env.example):
```bash
# 公開してよいのはこの 2 つだけ。service role key はここにも書かない
NEXT_PUBLIC_SUPABASE_URL=<ここに貼る:Supabase プロジェクトの URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ここに貼る:anon key(RLS があるので公開可)>
# E2E テスト用(ローカルと CI のみ)
E2E_EMAIL=<ここに貼る:テスト用ユーザーのメール>
E2E_PASSWORD=<ここに貼る:テスト用ユーザーのパスワード>
```

- 確認: git ls-files .env.local が空

### STEP 4: 認証 UI を作る
サインアップ・ログイン・ログアウトと、/dashboard の保護を作ります。確認メールが届いてからログインできることを確かめます。

AI に渡すプロンプト(認証 UI(サインアップ / ログイン / ログアウト)):
```text
Next.js(App Router)と @supabase/ssr で、メール + パスワードの認証を作ってください。

画面:
- /signup: メール・パスワード(8 文字以上)・確認。登録後は確認メールの案内
- /login: メール・パスワード。失敗時は理由を表示(存在しないメールか間違いかは区別しない)
- ログアウトはヘッダーのボタン
- /dashboard は未ログインなら /login へ 302(middleware で保護)

条件:
- クライアントには NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY だけを渡す。service role key は使わない
- セッションは cookie(@supabase/ssr の推奨どおり)
- フォームは aria-live でエラーを読み上げる
- 環境変数は .env.example にキー名とプレースホルダー(<ここに貼る:…>)だけ書く
```

コード例(middleware.ts):
```typescript
import { NextResponse, type NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';

export async function middleware(request: NextRequest) {
  const { response, user } = await updateSession(request);
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone();
    url.pathname = '/login';
    return NextResponse.redirect(url);
  }
  return response;
}

export const config = { matcher: ['/dashboard/:path*'] };
```

- 確認: 未ログインで /dashboard を開くと /login に行く

### STEP 5: ダッシュボードのメモ CRUD を作る
一覧・追加・編集・削除を Server Actions で作ります。user_id は必ずサーバーで付けます。

AI に渡すプロンプト(ダッシュボードのメモ CRUD):
```text
/dashboard にメモの一覧・追加・編集・削除を作ってください。

条件:
- Server Actions で insert / update / delete を行い、必ずログインユーザーの user_id を付ける(クライアントから user_id を受け取らない)
- 一覧はサーバーコンポーネントで取得し、空のときは「まだメモがありません」
- 追加フォームは送信中に無効化、完了後にリストを再検証(revalidatePath)
- 削除は確認ダイアログ付き
- 読み込み中はスケルトン、完了時は小さなトーストで知らせる
- 375px で 1 カラムになる
```

コード例(app/dashboard/actions.ts):
```typescript
'use server';

import { revalidatePath } from 'next/cache';
import { createClient } from '@/lib/supabase/server';

export async function addNote(formData: FormData) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return { ok: false, message: 'ログインが必要です' };

  const title = String(formData.get('title') ?? '').trim();
  if (title.length < 1 || title.length > 120) return { ok: false, message: 'タイトルは 1〜120 文字です' };

  // user_id はサーバーで付ける(クライアントからは受け取らない)
  const { error } = await supabase.from('notes').insert({ title, user_id: user.id });
  if (error) return { ok: false, message: '保存できませんでした' };

  revalidatePath('/dashboard');
  return { ok: true };
}
```

- 確認: 追加したメモが一覧に出て、削除で消える

### STEP 6: 別ユーザーで分離を確かめる
2 つ目のユーザーでログインし、1 つ目のメモが見えないこと、id を指定した更新・削除も失敗することを確かめます。これが RLS の効果です。

AI に渡すプロンプト(セキュリティのセルフチェック):
```text
このアプリについて、次を確認して結果を表にしてください。

- RLS が notes で有効で、4 つの操作すべてにポリシーがある
- Server Action が user_id をクライアントから受け取っていない
- service role key がクライアント側のコードと .env.example に無い
- 未ログインで /dashboard を開くと /login に行く
- 別ユーザーのメモ id を指定して update / delete を呼んでも失敗する(curl の手順も書く)
- エラーメッセージに他ユーザーの情報やスタックトレースが出ない

問題があれば修正案も出してください。
```

- 確認: 別ユーザーのメモは一覧にも API にも出ない

### STEP 7: E2E テストを 3 本書く
ログイン保護・追加と削除・ログアウト後の保護を Playwright で自動化します。テスト用ユーザーの情報は環境変数から読みます。

AI に渡すプロンプト(E2E テスト 3 本):
```text
Playwright で次の 3 本の E2E テストを書いてください。テスト用ユーザーは環境変数 E2E_EMAIL / E2E_PASSWORD から読み、コードに書かないこと。

1. 未ログインで /dashboard を開くと /login に遷移する
2. ログインしてメモを追加すると一覧に表示され、削除すると消える
3. ログアウト後に /dashboard を開くと /login に戻る

npm run test:e2e で動くようにし、CI で動かす場合の注意点を最後に 3 行で書いてください。
```

- 確認: npm run test:e2e が 3 本とも通る

### STEP 8: Vercel に公開する
Vercel に環境変数(公開してよい 2 つ)を登録してデプロイします。Supabase の Auth 設定で、公開 URL を Site URL と Redirect URLs に追加します。

- 確認: 公開 URL でサインアップから削除まで一通りできる

## 検証
- 未ログインで保護される — やり方: ログアウトした状態で /dashboard を開く / 期待結果: /login に 302 で戻る
- 自分のメモだけ見える — やり方: 2 ユーザーで交互にログインして一覧を見る / 期待結果: 相手のメモは一覧に出ない
- API を直接叩いても他人のデータは触れない — やり方: ユーザー B のセッションで、ユーザー A のメモ id を指定して update / delete を呼ぶ(Supabase REST を curl) / 期待結果: 0 行が対象になり、A のメモは変わらない
- user_id を偽装できない — やり方: 追加フォームの送信に user_id を混ぜて送る / 期待結果: 無視され、ログインユーザーの id で保存される
- service role key がクライアントに無い — やり方: ビルド後の JS を SUPABASE_SERVICE_ROLE で検索する / 期待結果: 見つからない
- パスワードリセットが動く — やり方: /login の「パスワードを忘れた」から再設定する / 期待結果: メールが届き、新しいパスワードでログインできる
- E2E テストが通る — やり方: npm run test:e2e / 期待結果: 3 本すべて成功
- 375px で崩れない — やり方: 端末エミュレーションで /login と /dashboard を見る / 期待結果: 1 カラムになり、横スクロールが出ない

## 安全性・秘密情報
- anon key は公開前提だが、RLS が有効で正しいポリシーがあることが前提。RLS なしで公開しない
- service role key はサーバーの秘密。この教材では一切使わず、.env.example にも書かない
- user_id はサーバー(Server Action)で付け、クライアントからの値を信用しない
- エラーメッセージに他ユーザーの情報やスタックトレースを出さない
- テスト用ユーザーの認証情報は環境変数に置き、コードに書かない
- パスワードは 8 文字以上、確認メールを有効にする
- 「<ここに貼る:…>」の形のプレースホルダーは、環境変数など公開されない場所に実際の値を置き、コードやリポジトリに書かない

## 公開手順
- Vercel の Environment Variables に NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY を登録する
- Supabase の Authentication → URL Configuration に公開 URL を Site URL と Redirect URLs として追加する
- 公開 URL でサインアップ → 確認メール → ログイン → メモ追加 → 削除を 1 周する
- 独自ドメインを設定し、HTTPS を確認する

## よくあるエラー
- ログイン後に /dashboard を開いても /login に戻される — 原因: cookie ベースのセッションが middleware で更新されていない、または Supabase の Site URL / Redirect URLs に公開 URL が無い / 対処: @supabase/ssr の updateSession を middleware で呼ぶ。Supabase の URL Configuration に公開 URL を追加する
- メモを追加しても一覧に出ない — 原因: insert は成功しているが revalidatePath を呼んでいない、または RLS の select ポリシーが無い / 対処: Server Action の最後で revalidatePath('/dashboard') を呼ぶ。4 つのポリシーが揃っているか確認する
- insert が「new row violates row-level security policy」で失敗する — 原因: insert の with check が auth.uid() = user_id なのに、user_id を付けていない / 対処: Server Action でログインユーザーの id を user_id に入れる
- 他人のメモが見えてしまう — 原因: RLS を有効化していない(alter table … enable row level security を実行していない) / 対処: RLS を有効化し、ポリシーを作り直す。公開前に別ユーザーで必ず確認する
- E2E が CI だけ落ちる — 原因: CI に E2E_EMAIL / E2E_PASSWORD が無い、または確認メール待ちのユーザーを使っている / 対処: CI の Secrets に設定し、確認済みのテスト用ユーザーを使う

## 参照した公式仕様
- Supabase Row Level Security https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase Auth with Next.js (SSR) https://supabase.com/docs/guides/auth/server-side/nextjs
- Next.js Server Actions https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- Playwright Getting started https://playwright.dev/docs/intro

## 受け入れ条件
- 上の検証項目がすべて期待結果どおりになる
- 秘密情報がコード・設定ファイル・公開ページに含まれていない
- スマートフォン幅でも表示が崩れず、キーボードだけで操作できる

参照した公式仕様

更新履歴

2026.10.21
初版
指示書 検証