メインコンテンツへスキップ
← 記事一覧に戻る

Claude Code × Supabase で管理画面を30分で生成する Skill 実装ガイド

11 min readClaude Code
#Claude Code#Supabase#管理画面#個人開発

「個人開発の SaaS を作るとき、管理画面の実装が一番つまらない」——多くの開発者が同意するはずです。

CRUD 画面はパターン化しているのに、毎回手で書くのは時間の無駄。これを Claude Code の Skill で自動化して、Supabase のテーブルから管理画面を30分で生成できるようにします。

この記事では、その Skill の作り方と使い方を、実コード付きで全公開します。

結論:DB スキーマを読んで管理画面を生成する Skill を作る

結論から言います。Claude Code の Skills と Supabase MCP を組み合わせれば、次の手順で管理画面が自動生成できます。

  1. Supabase MCP 経由で対象テーブルのスキーマを取得
  2. Skill が「管理画面を作る手順」をロード
  3. Claude が Next.js + Tailwind の CRUD 画面を生成
  4. Server Actions で更新ロジックを実装
  5. RLS ポリシー対応の認証チェックを組み込み

これを Skill 化すれば、テーブル名を指定するだけで30分で管理画面が完成します。

必要な前提

  • Claude Code がインストール済み(Ep.01 参照)
  • Supabase MCP が動作している(公式または自作、Ep.02 参照)
  • Next.js 16 + Tailwind のプロジェクトが用意されている

ステップ1:Skill ファイルの作成

~/.claude/skills/supabase-admin-gen/SKILL.md を作成します。

---
name: supabase-admin-gen
description: TRIGGER when the user asks to generate an admin/CRUD page for a Supabase table. Reads the table schema via Supabase MCP and generates a Next.js admin page with list, create, edit, delete actions.
---

# Supabase 管理画面ジェネレータ

## 入力

- 対象テーブル名(例: `products`)
- 出力先ディレクトリ(例: `src/app/admin/products`)

## 手順

### 1. スキーマ取得
Supabase MCP の `get_table_schema` を呼んで、対象テーブルのカラム情報を取得する。

### 2. ファイル生成
以下の4ファイルを生成する:

- `page.tsx`: 一覧表示(Server Component)
- `[id]/page.tsx`: 詳細・編集(Server Component)
- `actions.ts`: Server Actions(create/update/delete)
- `_components/Form.tsx`: 入力フォーム(Client Component)

### 3. 認証チェック
各 Server Action の冒頭で、現在のユーザーが admin ロールかを確認する:

```typescript
import { createClient } from '@/lib/supabase/server'

const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')

const { data: profile } = await supabase
  .from('profiles')
  .select('role')
  .eq('id', user.id)
  .single()
if (profile?.role !== 'admin') throw new Error('Forbidden')

4. RLS 対応

RLS ポリシーが設定されているテーブルの場合、createClient が現在のユーザー権限で動作するため、追加対応は不要。

5. UI スタイル

Tailwind の標準クラスを使い、prose クラスは使わない。テーブル一覧は以下の構造:

<table className="w-full border-collapse">
  <thead className="bg-slate-100 dark:bg-slate-800">
    <tr>
      <th className="border p-2 text-left">カラム名</th>
      ...
    </tr>
  </thead>
  ...
</table>

注意

  • Server Component を優先、Client Component は Form のみ
  • Server Actions は use server ディレクティブを必ず冒頭に
  • DELETE は確認モーダル(window.confirm)を経由
  • 日付は YYYY-MM-DD HH:mm 形式で表示
  • 数値はカンマ区切り

## ステップ2:使ってみる

Claude Code を起動して次のように頼みます。

Supabase の products テーブル用の管理画面を生成して、 src/app/admin/products に配置して


Claude は次の流れで動きます:

1. `supabase-admin-gen` Skill をロード
2. Supabase MCP で `get_table_schema('products')` を実行
3. カラム情報を確認(id, name, price, stock, created_at など)
4. 4ファイルを順次生成

生成完了まで体感5〜10分。生成後、`npm run dev` で起動すれば管理画面がそのまま動きます。

## 生成されるコード例

### page.tsx(一覧)

```tsx
import { createClient } from '@/lib/supabase/server'
import Link from 'next/link'

export default async function ProductsAdminPage() {
  const supabase = await createClient()
  const { data: products } = await supabase
    .from('products')
    .select('*')
    .order('created_at', { ascending: false })

  return (
    <div className="p-8">
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-3xl font-bold">商品管理</h1>
        <Link
          href="/admin/products/new"
          className="rounded bg-indigo-600 px-4 py-2 text-white"
        >
          新規追加
        </Link>
      </div>
      <table className="w-full border-collapse">
        <thead className="bg-slate-100 dark:bg-slate-800">
          <tr>
            <th className="border p-2 text-left">ID</th>
            <th className="border p-2 text-left">商品名</th>
            <th className="border p-2 text-right">価格</th>
            <th className="border p-2 text-right">在庫</th>
            <th className="border p-2 text-left">作成日時</th>
            <th className="border p-2"></th>
          </tr>
        </thead>
        <tbody>
          {products?.map((p) => (
            <tr key={p.id}>
              <td className="border p-2">{p.id}</td>
              <td className="border p-2">{p.name}</td>
              <td className="border p-2 text-right">¥{p.price.toLocaleString()}</td>
              <td className="border p-2 text-right">{p.stock}</td>
              <td className="border p-2">
                {new Date(p.created_at).toLocaleString('ja-JP')}
              </td>
              <td className="border p-2">
                <Link href={`/admin/products/${p.id}`}>編集</Link>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

actions.ts(Server Actions)

'use server'

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

async function checkAdmin() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Unauthorized')

  const { data: profile } = await supabase
    .from('profiles')
    .select('role')
    .eq('id', user.id)
    .single()
  if (profile?.role !== 'admin') throw new Error('Forbidden')

  return supabase
}

export async function createProduct(formData: FormData) {
  const supabase = await checkAdmin()
  await supabase.from('products').insert({
    name: formData.get('name') as string,
    price: Number(formData.get('price')),
    stock: Number(formData.get('stock')),
  })
  revalidatePath('/admin/products')
  redirect('/admin/products')
}

export async function updateProduct(id: number, formData: FormData) {
  const supabase = await checkAdmin()
  await supabase.from('products').update({
    name: formData.get('name') as string,
    price: Number(formData.get('price')),
    stock: Number(formData.get('stock')),
  }).eq('id', id)
  revalidatePath('/admin/products')
  redirect('/admin/products')
}

export async function deleteProduct(id: number) {
  const supabase = await checkAdmin()
  await supabase.from('products').delete().eq('id', id)
  revalidatePath('/admin/products')
}

これらが Claude によって自動生成されます。

💡

事例: Anthropic 公式(How Anthropic teams use Claude Code)の体験談

Anthropic 自身のレポート「How Anthropic teams use Claude Code」によれば、社内の非エンジニアもこのパターンの恩恵を受けています。 具体例として、法務チームが電話ツリーシステムを構築マーケターが数百のバナー広告バリエーションを瞬時に生成データサイエンティストが JavaScript の知識なしに複雑な可視化を作成、といった事例が紹介されています。 これらに共通するのは「テンプレート化可能な定型作業を Claude Code に委譲する」という発想。管理画面のような「パターン化された CRUD」も、まさに Claude Code が最も得意とする領域です。Anthropic 自身、Skills の活用例として「企業ブランドガイドラインに沿った文書生成」「組織固有のワークフローでのデータ分析」を挙げています。

出典: How Anthropic teams use Claude Code / Equipping agents for the real world with Agent Skills

応用:他のテーブルにも横展開

この Skill は対象テーブル名を変えるだけで、どのテーブルにも適用できます。

users テーブル用の管理画面を /admin/users に生成して
orders テーブル用の管理画面を /admin/orders に生成して

10個のテーブルがあるプロジェクトでも、30分以内に全管理画面が完成します。

ハマりポイント

1. RLS が無効なテーブルで認証チェック忘れ

Supabase の RLS が無効になっているテーブルで管理画面を作る場合、認証チェックが Skill 内で必須です。これを忘れると、誰でも全データにアクセスできる管理画面が生成されます。

2. カスタム型カラムの非対応

PostgreSQL のカスタム型(enum など)を使っているカラムは、Skill が単純な文字列入力に変換してしまいます。手動で <select> に修正する必要があります。

3. リレーション表示の限界

外部キーで結ばれたテーブルの「JOIN表示」までは Skill で自動化できていません。リレーションは生成後に手動で追加します。

で、どう稼ぐ? — 管理画面自動生成の経済価値

1. 開発時間を「売れる機能」に集中できる

管理画面は内部用なので、ユーザーには見えません。ここに時間をかけることは「売上にならない時間」を消費することです。Skill で自動化すれば、その時間を売れる機能の実装に回せます。

2. 受託案件のスピード差別化

「Supabase + Next.js の管理画面構築」を受注したとき、人力で1日かかる作業を1時間で終わらせれば、時給は実質20倍です。固定報酬の案件で特に効きます。

3. SaaS スタートアップの MVP に最適

MVP では「管理画面なんて後回し」になりがちですが、Skill で自動生成すれば「ある」状態でリリースできます。投資家への説明やクライアントデモで信頼性が増します。

まとめ

Supabase 管理画面自動生成 Skill のポイントは次のとおり。

  • Supabase MCP + Skill の組み合わせで、テーブル名指定のみで管理画面生成
  • 認証チェックは Skill 内で必須化 する
  • 生成後の手動修正は最小限:リレーションとカスタム型のみ

次のエピソード Ep.11 は 有料記事 です。Hooks による事故防止の本格運用を解説します。本番 DB 保護、git secrets 検出、CI/CD への組み込みまで網羅します。

次に読む

Supabase RLS ペイウォールパターン (¥500)

管理画面の自動生成ができるようになったら、次は「課金ユーザー専用ページ」の実装です。Supabase の Row Level Security (RLS) を使った有料コンテンツゲーティングの実装パターンを、有料記事で全公開しています。

← 記事一覧に戻る