jpskill.com
🛠️ 開発・MCP コミュニティ

supabase

データベース、認証、ストレージ、リアルタイム機能など、Supabaseの多岐にわたるサービスをバックエンドとして活用するSkill。

📜 元の英語説明(参考)

Supabase for database, auth, storage, and realtime features. Use when user mentions "supabase", "supabase auth", "supabase storage", "supabase realtime", "supabase edge functions", "postgres with supabase", "row level security", "RLS", "supabase client", or building apps with Supabase as the backend.

🇯🇵 日本人クリエイター向け解説

一言でいうと

データベース、認証、ストレージ、リアルタイム機能など、Supabaseの多岐にわたるサービスをバックエンドとして活用するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-17
取得日時
2026-05-17
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Supabase

セットアップ

npm install -g supabase          # または: brew install supabase/tap/supabase
supabase init                    # ローカルプロジェクトを初期化します
supabase link --project-ref <id> # リモートプロジェクトにリンクします

npm install @supabase/supabase-js  # JS/TS クライアント
# pip install supabase             # Python クライアント

クライアントの初期化:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

// 権限を昇格させたサーバーサイド (RLS をバイパスします。クライアントに公開しないでください)
const supabaseAdmin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)

生成された型を使用する場合:

import { Database } from './types/database'
const supabase = createClient<Database>(url, key)

データベース

create table public.posts (
  id uuid default gen_random_uuid() primary key,
  title text not null,
  content text,
  user_id uuid references auth.users(id) on delete cascade not null,
  created_at timestamptz default now() not null
);

マイグレーションと型

supabase migration new create_posts_table   # マイグレーションファイルを作成します
supabase db push                             # リモートにマイグレーションを適用します
supabase db pull                             # リモートスキーマをマイグレーションにプルします
supabase db reset                            # ローカル DB をリセットし、マイグレーションを再実行します
supabase gen types typescript --linked > src/types/database.ts  # 型を生成します

マイグレーションファイルは、タイムスタンプ付きのファイル名で supabase/migrations/ に保存されます。

行レベルセキュリティ (RLS)

クライアントに公開されるテーブルでは、常に RLS を有効にしてください。主な関数: auth.uid() は現在のユーザー ID を返し、auth.jwt() は完全な JWT クレームを返します。using はどの既存の行が表示されるかを制御し、with check はどの新規/変更された行が許可されるかを制御します。

alter table public.posts enable row level security;

create policy "Public read access" on public.posts
  for select using (true);

create policy "Users can insert own posts" on public.posts
  for insert to authenticated
  with check (auth.uid() = user_id);

create policy "Users can update own posts" on public.posts
  for update to authenticated
  using (auth.uid() = user_id) with check (auth.uid() = user_id);

create policy "Users can delete own posts" on public.posts
  for delete to authenticated
  using (auth.uid() = user_id);

認証

// メール/パスワードでのサインアップ
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com', password: 'secure-password',
})

// メール/パスワードでのサインイン
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com', password: 'secure-password',
})

// OAuth (github, google, apple, discord など)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: { redirectTo: 'https://yourapp.com/auth/callback' },
})

// マジックリンク
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: { emailRedirectTo: 'https://yourapp.com/welcome' },
})

// 電話番号 OTP
const { data, error } = await supabase.auth.signInWithOtp({ phone: '+15551234567' })
const { data, error } = await supabase.auth.verifyOtp({
  phone: '+15551234567', token: '123456', type: 'sms',
})

認証ヘルパー

const { data: { user } } = await supabase.auth.getUser()
const { data: { session } } = await supabase.auth.getSession()
await supabase.auth.signOut()

const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
  console.log(event, session)
})
subscription.unsubscribe()

ストレージ

// バケットを作成します (サービスロールまたは適切なポリシーが必要です)
await supabase.storage.createBucket('avatars', {
  public: false, fileSizeLimit: 1048576, allowedMimeTypes: ['image/png', 'image/jpeg'],
})

// アップロード
await supabase.storage.from('avatars').upload('user1/avatar.png', file, {
  cacheControl: '3600', upsert: true,
})

// ダウンロード
const { data } = await supabase.storage.from('avatars').download('user1/avatar.png')

// 署名付き URL (一時的、プライベートバケット)
const { data } = await supabase.storage.from('avatars').createSignedUrl('user1/avatar.png', 3600)

// 公開 URL (公開バケットのみ)
const { data } = supabase.storage.from('avatars').getPublicUrl('user1/avatar.png')

ストレージポリシーは storage.objects テーブルを使用します:

create policy "Users upload own avatars" on storage.objects
  for insert to authenticated
  with check (bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text);

create policy "Public avatar access" on storage.objects
  for select using (bucket_id = 'avatars');

リアルタイム

テーブルのリアルタイムを有効にします: alter publication supabase_realtime add table public.posts;

Postgres 変更の購読

const channel = supabase.channel('posts-changes')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' },
    (payload) => console.log('Change:', payload))
  .subscribe()

// イベントタイプでフィルタリング
supabase.channel('new-posts')
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' },
    (payload) => console.log('New:', payload.new))
  .subscribe()

supabase.removeChannel(channel) // 購読解除

プレゼンス

const channel = supabase.channel('room-1')
channel
  .on('presence', { event: 'sync' }, () => console.log(channel.presenceState()))
  .on('presence', { event: 'join' }, ({ key, newPresences }) => console.log('Joined:', newPresences))
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => console.log('Left:', leftPresences))
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({ user_id: 'user-1', online_at: new Date().toISOString() })
    }
  })

ブロードキャスト

const channel = supabase.channel('room-1')
channel.on('broadcast', { event: 'cursor-pos' }, (pay
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Supabase

Setup

npm install -g supabase          # or: brew install supabase/tap/supabase
supabase init                    # initialize local project
supabase link --project-ref <id> # link to remote project

npm install @supabase/supabase-js  # JS/TS client
# pip install supabase             # Python client

Client initialization:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

// Server-side with elevated privileges (bypasses RLS, never expose to client)
const supabaseAdmin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)

With generated types:

import { Database } from './types/database'
const supabase = createClient<Database>(url, key)

Database

create table public.posts (
  id uuid default gen_random_uuid() primary key,
  title text not null,
  content text,
  user_id uuid references auth.users(id) on delete cascade not null,
  created_at timestamptz default now() not null
);

Migrations and Types

supabase migration new create_posts_table   # create migration file
supabase db push                             # apply migrations to remote
supabase db pull                             # pull remote schema into migration
supabase db reset                            # reset local DB, rerun migrations
supabase gen types typescript --linked > src/types/database.ts  # generate types

Migration files live in supabase/migrations/ with timestamped filenames.

Row Level Security (RLS)

Always enable RLS on tables exposed to the client. Key functions: auth.uid() returns current user ID, auth.jwt() returns full JWT claims. using controls which existing rows are visible; with check controls which new/modified rows are allowed.

alter table public.posts enable row level security;

create policy "Public read access" on public.posts
  for select using (true);

create policy "Users can insert own posts" on public.posts
  for insert to authenticated
  with check (auth.uid() = user_id);

create policy "Users can update own posts" on public.posts
  for update to authenticated
  using (auth.uid() = user_id) with check (auth.uid() = user_id);

create policy "Users can delete own posts" on public.posts
  for delete to authenticated
  using (auth.uid() = user_id);

Authentication

// Email/password sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com', password: 'secure-password',
})

// Email/password sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com', password: 'secure-password',
})

// OAuth (github, google, apple, discord, etc.)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: { redirectTo: 'https://yourapp.com/auth/callback' },
})

// Magic link
const { data, error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: { emailRedirectTo: 'https://yourapp.com/welcome' },
})

// Phone OTP
const { data, error } = await supabase.auth.signInWithOtp({ phone: '+15551234567' })
const { data, error } = await supabase.auth.verifyOtp({
  phone: '+15551234567', token: '123456', type: 'sms',
})

Auth Helpers

const { data: { user } } = await supabase.auth.getUser()
const { data: { session } } = await supabase.auth.getSession()
await supabase.auth.signOut()

const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
  console.log(event, session)
})
subscription.unsubscribe()

Storage

// Create bucket (requires service role or appropriate policies)
await supabase.storage.createBucket('avatars', {
  public: false, fileSizeLimit: 1048576, allowedMimeTypes: ['image/png', 'image/jpeg'],
})

// Upload
await supabase.storage.from('avatars').upload('user1/avatar.png', file, {
  cacheControl: '3600', upsert: true,
})

// Download
const { data } = await supabase.storage.from('avatars').download('user1/avatar.png')

// Signed URL (temporary, private buckets)
const { data } = await supabase.storage.from('avatars').createSignedUrl('user1/avatar.png', 3600)

// Public URL (public buckets only)
const { data } = supabase.storage.from('avatars').getPublicUrl('user1/avatar.png')

Storage policies use the storage.objects table:

create policy "Users upload own avatars" on storage.objects
  for insert to authenticated
  with check (bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text);

create policy "Public avatar access" on storage.objects
  for select using (bucket_id = 'avatars');

Realtime

Enable realtime for a table: alter publication supabase_realtime add table public.posts;

Postgres Changes Subscription

const channel = supabase.channel('posts-changes')
  .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' },
    (payload) => console.log('Change:', payload))
  .subscribe()

// Filter by event type
supabase.channel('new-posts')
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' },
    (payload) => console.log('New:', payload.new))
  .subscribe()

supabase.removeChannel(channel) // unsubscribe

Presence

const channel = supabase.channel('room-1')
channel
  .on('presence', { event: 'sync' }, () => console.log(channel.presenceState()))
  .on('presence', { event: 'join' }, ({ key, newPresences }) => console.log('Joined:', newPresences))
  .on('presence', { event: 'leave' }, ({ key, leftPresences }) => console.log('Left:', leftPresences))
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({ user_id: 'user-1', online_at: new Date().toISOString() })
    }
  })

Broadcast

const channel = supabase.channel('room-1')
channel.on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload)).subscribe()
channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 100, y: 200 } })

Edge Functions

Deno-based server-side TypeScript functions.

supabase functions new my-function      # create
supabase functions serve my-function    # local dev
supabase functions deploy my-function   # deploy
supabase secrets set MY_API_KEY=value   # set secret
supabase secrets list                   # list secrets
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
  )
  const { data } = await supabase.from('posts').select('*')
  return new Response(JSON.stringify(data), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
})

Invoke from client:

const { data, error } = await supabase.functions.invoke('my-function', { body: { name: 'world' } })

Client Library Queries

Supabase auto-generates a PostgREST API from your schema. The client library wraps it.

// Select
const { data } = await supabase.from('posts').select('*')
const { data } = await supabase.from('posts').select('id, title')
const { data } = await supabase.from('posts').select('id, title, comments(id, body)')  // join
const { count } = await supabase.from('posts').select('*', { count: 'exact', head: true })

// Insert
const { data } = await supabase.from('posts')
  .insert({ title: 'Hello', content: 'World', user_id: userId }).select()
const { data } = await supabase.from('posts')
  .insert([{ title: 'A', user_id: userId }, { title: 'B', user_id: userId }]).select()

// Update
const { data } = await supabase.from('posts')
  .update({ title: 'New Title' }).eq('id', postId).select()

// Delete
await supabase.from('posts').delete().eq('id', postId)

// Filters
const { data } = await supabase.from('posts').select('*')
  .eq('user_id', userId)        // equals
  .neq('status', 'draft')       // not equals
  .gt('views', 100)             // greater than
  .lt('views', 1000)            // less than
  .like('title', '%hello%')     // case-sensitive pattern
  .ilike('title', '%hello%')    // case-insensitive pattern
  .in('status', ['published', 'archived'])
  .is('deleted_at', null)       // null check
  .order('created_at', { ascending: false })
  .range(0, 9)                  // pagination (first 10)
  .limit(10)
  .single()                     // expect exactly one row

Database Functions and RPC

create or replace function get_posts_by_author(author_id uuid)
returns setof posts language sql security definer as $$
  select * from posts where user_id = author_id order by created_at desc;
$$;
const { data } = await supabase.rpc('get_posts_by_author', { author_id: userId })

security definer bypasses RLS. security invoker (default) respects caller RLS policies.

CLI Reference

supabase start           # start local stack (Postgres, Auth, Storage, Studio)
supabase stop            # stop local stack
supabase status          # show local URLs and keys
supabase db push         # apply migrations to remote
supabase db pull         # pull remote schema
supabase db reset        # reset local DB
supabase db lint         # lint SQL
supabase db diff         # diff local vs remote
supabase migration new <name>
supabase migration list
supabase gen types typescript --linked > types/database.ts
supabase functions new <name>
supabase functions serve
supabase functions deploy <name>
supabase secrets set KEY=value
supabase secrets list

Local Development

supabase start launches the full stack. Studio runs at http://localhost:54323. Use local keys in .env.local:

SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=<anon-key-from-supabase-start>

Common Patterns

User Profiles with Auto-Create Trigger

create table public.profiles (
  id uuid references auth.users(id) on delete cascade primary key,
  display_name text, avatar_url text, updated_at timestamptz default now()
);
alter table public.profiles enable row level security;

create policy "Public read" on public.profiles for select using (true);
create policy "Own update" on public.profiles for update to authenticated using (auth.uid() = id);

create or replace function public.handle_new_user() returns trigger
language plpgsql security definer set search_path = '' as $$
begin
  insert into public.profiles (id, display_name)
  values (new.id, new.raw_user_meta_data ->> 'full_name');
  return new;
end;
$$;

create trigger on_auth_user_created after insert on auth.users
  for each row execute function public.handle_new_user();

File Uploads with Auth

async function uploadAvatar(userId: string, file: File) {
  const path = `${userId}/${Date.now()}-${file.name}`
  const { error } = await supabase.storage.from('avatars').upload(path, file, { upsert: true })
  if (error) throw error
  const { data } = supabase.storage.from('avatars').getPublicUrl(path)
  await supabase.from('profiles').update({ avatar_url: data.publicUrl }).eq('id', userId)
  return data.publicUrl
}

Realtime Chat

async function sendMessage(channelId: string, content: string) {
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')
  return supabase.from('messages').insert({ channel_id: channelId, content, user_id: user.id })
}

function subscribeToMessages(channelId: string, onMessage: (msg: any) => void) {
  return supabase.channel(`messages:${channelId}`)
    .on('postgres_changes', {
      event: 'INSERT', schema: 'public', table: 'messages',
      filter: `channel_id=eq.${channelId}`,
    }, (payload) => onMessage(payload.new))
    .subscribe()
}