SupabaseはPostgreSQLを中心に、認証・ストレージ・Realtime・Edge Functionsをまとめて提供するBaaS(Backend as a Service)。 Firebaseと同じ立ち位置だが、FirebaseがNoSQL(ドキュメント型)なのに対し、SupabaseはリレーショナルDB(PostgreSQL)を使う。 OSSなので自分でホストも可能だが、通常はSupabase社のクラウドを利用する。
あなたのアプリ
↓
Supabase(認証 / DB / Storage / Realtime / Edge Functions)
↓
PostgreSQL(実体)
💡 ポイント: 「SupabaseはPostgresのラッパー」と覚えると本質を見失わない。JoinもトランザクションもRLSも使えるのはPostgreSQLだから。
| 用語 | 説明 |
|---|---|
| BaaS | Backend as a Service。DB・認証・ストレージなどをまとめて提供するサービス |
| Firebase | GoogleのBaaS。NoSQL(Firestore)ベース |
| OSS | オープンソースソフトウェア。自分でホストも可能 |
各機能は独立したOSSプロダクトの組み合わせ。テーブルを作るだけで自動的にREST APIが生えるのはPostgRESTのおかげ。
┌─────────────────────────────────┐
│ あなたのアプリ │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ Supabase API層 │
│ PostgREST │ GoTrue │ Realtime │
│ Storage │ Edge Functions │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ PostgreSQL │
└─────────────────────────────────┘
| コンポーネント | 役割 | OSSプロダクト |
|---|---|---|
| REST API | DBをHTTP経由で操作 | PostgREST |
| 認証 | ユーザー管理・JWT発行 | GoTrue |
| Realtime | WebSocket配信 | Supabase Realtime |
| Storage | ファイル管理 | Supabase Storage |
| Edge Functions | サーバーレス関数 | Deno |
💡 ポイント:
supabase-jsのクエリは内部的にPostgRESTへのHTTPリクエスト。DjangoのModelViewSetが自動生成されるイメージ。
// これは内部的にPostgRESTへのGETリクエスト
const { data } = await supabase.from('posts').select('*')
| 用語 | 説明 |
|---|---|
| PostgREST | PostgreSQLのテーブルを自動でREST APIに変換するOSS |
| GoTrue | JWT発行・ユーザー管理を担当する認証OSS |
ローカル開発ではsupabase startでDockerコンテナが自動起動し、本番と同じ環境が再現される。
Dockerは「PCの中に小さい仮想マシンを立てるツール」で、Docker Desktopをインストールして起動しておくだけでよい。
スキーマ変更はSQLファイルで管理し、db diff→db pushの流れで本番に反映する。
supabase/
├── config.toml # ローカル設定
├── migrations/ # マイグレーションSQL
│ └── 20240101_init.sql
└── seed.sql # 初期データ
# セットアップ
brew install --cask docker # Docker Desktopをインストール
supabase init # プロジェクト初期化
supabase start # Docker起動(DB・Auth・Storageが全部立ち上がる)
# 開発フロー
supabase db diff -f add_posts_table # 差分からマイグレーション生成
git add supabase/migrations/
git commit -m "add posts table"
supabase db push # 本番DBに適用
開発フロー:
ローカルDB(Docker)でスキーマ変更(GUIかSQL直書き)
↓ db diff
migrations/20240101_add_posts.sql(自動生成)
↓ git commit
Gitリポジトリ
↓ db push
本番DB(Supabase Cloud)
💡 ポイント:
supabase linkで本番プロジェクトと紐づけておく必要がある(初回のみ)。GUIで作ったテーブルもdb diffで拾えるので「まずGUIで試してからSQLに落とす」順番もアリ。
| Django | Supabase |
|---|---|
makemigrations | supabase db diff |
migrate | supabase db push |
| 用語 | 説明 |
|---|---|
| Docker | コンテナ仮想化ツール。supabase startが必要なコンテナを自動で起動する |
supabase start | PostgreSQL・Auth・StorageなどのDockerコンテナを一括起動するコマンド |
db diff | ローカルDBと適用済みマイグレーションの差分を検出してSQLファイルを生成 |
db push | マイグレーションファイルを本番DBに適用 |
テーブル作成はダッシュボードのGUIか直接SQLで行う。 Djangoと違いモデル定義ファイルは存在せず、マイグレーションはSQLファイルで管理する。 確認・閲覧はダッシュボードのTable EditorでGUI確認するのが基本。
-- supabase/migrations/20240101_create_posts.sql
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
💡 ポイント:
auth.usersはSupabaseが自動で作るテーブル。外部キーで参照することでユーザーとデータを紐づけられる。スキーマはSQLファイルで管理(Git管理)し、確認はダッシュボードのTable Editorで行うのが実用的。
| Djangoの概念 | Supabaseの対応 |
|---|---|
models.py | マイグレーションSQL |
AutoField | UUID or BIGSERIAL |
ForeignKey | REFERENCES |
DateTimeField(auto_now_add) | TIMESTAMPTZ DEFAULT NOW() |
RLSを有効にするとデフォルトで全行へのアクセスが拒否される(ホワイトリスト方式)。 ポリシーを追加することで「自分の行だけ読める」などのルールを定義する。 アプリ側でフィルタリングするより安全で、バグによる情報漏洩を防げる。
-- RLSを有効化
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 自分の投稿だけ読めるポリシー
CREATE POLICY "自分の投稿を読める"
ON posts FOR SELECT
USING (auth.uid() = user_id);
RLS ON + ポリシーなし → 全拒否
RLS ON + ポリシーあり → ポリシーに合致した行だけ許可
RLS OFF → 全員全行アクセス可能(危険)
💡 ポイント: 新しいテーブルを作ったらまずRLSをONにするを習慣にする。
service_role_keyはRLSをバイパスできるため、絶対にフロントエンドに渡してはいけない(NEXT_PUBLIC_禁止)。
| 用語 | 説明 |
|---|---|
ENABLE ROW LEVEL SECURITY | テーブル単位でRLSをON |
POLICY | 行へのアクセス条件を定義 |
auth.uid() | ログイン中のユーザーIDを返すSupabase関数 |
USING | SELECT/DELETE時の条件 |
WITH CHECK | INSERT/UPDATE時の条件 |
メール・パスワード、OAuth(Google・GitHubなど)に対応。
ログインするとJWTが発行され、以降のリクエストにそのトークンが自動付与される。
RLSのauth.uid()はJWTに埋め込まれたsubフィールド(ユーザーID)を取り出して使っている。
// サインアップ
await supabase.auth.signUp({ email, password })
// ログイン
await supabase.auth.signInWithPassword({ email, password })
// 現在のユーザー取得
const { data: { user } } = await supabase.auth.getUser()
ログイン
↓
JWT発行(中にuser_idが埋め込まれている)
↓
以降のリクエストにJWTを付与
↓
PostgreSQL側でauth.uid()がJWTからIDを取り出す
↓
RLSポリシーで照合
💡 ポイント: Next.jsのServer ComponentsでJWTを使う場合はCookie経由でトークンを渡す必要がある。LocalStorageはサーバー側からアクセスできないため。
| Djangoの概念 | Supabaseの対応 |
|---|---|
request.user | supabase.auth.getUser() |
@login_required | RLSポリシー or ミドルウェア |
auth.User | auth.usersテーブル |
| セッション(Cookie) | JWT(LocalStorageまたはCookie) |
画像・動画・PDFなどのファイルをSupabase上に保存できる。 バケット単位でパブリック・プライベートを選択でき、アクセス制御はRLSポリシーと同じ構文で書ける。 AWSのS3と役割は同じだが、PostgreSQLのRLSと統合されているため一貫したアクセス制御が可能。
// ファイルアップロード
await supabase.storage
.from('avatars')
.upload('user123/avatar.png', file)
// 公開URLの取得(期限なし)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user123/avatar.png')
// 期限付きURL(60秒間有効)
const { data } = await supabase.storage
.from('documents')
.createSignedUrl('secret.pdf', 60)
💡 ポイント:
getPublicUrlは期限なし(パブリックバケット用)、createSignedUrlは期限あり(プライベートバケット用)。有料コンテンツの一時配布などはcreateSignedUrlを使う。
| 用語 | 説明 |
|---|---|
| バケット | ファイルの入れ物(S3のバケットと同じ概念) |
| パブリックバケット | URLさえ知っていれば誰でもアクセス可 |
| プライベートバケット | RLSポリシーで制御 |
getPublicUrl | 期限なしの公開URLを生成 |
createSignedUrl | 期限付きURLを生成 |
TypeScriptで書けてDenoランタイムで動作する。Vercelのサーバーレス関数に近い存在。 Next.js + Honoスタックがある場合、APIルートはそちらに書くのが自然で、Edge Functionsの出番は限られる。 「Supabaseのトリガーと連携」「Stripe Webhookの受信」「定期バッチ処理」など、Supabase内で完結する処理に絞るのが適切。
// supabase/functions/hello/index.ts
Deno.serve(async (req) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}` }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
supabase functions serve hello # ローカルで実行
supabase functions deploy hello # 本番デプロイ
💡 ポイント: Next.js + Honoがある場合、Edge Functionsは「Supabaseのイベントに反応する処理の受け口」と位置づけると役割が明確になる。
| 用途 | 説明 |
|---|---|
| DBトリガー連携 | DB更新時に自動で関数を呼びたい |
| Stripe Webhook受信 | サービスキーを使ってDB更新したい |
| 定期バッチ処理 | Supabase内のデータだけで完結する処理 |
PostgreSQLの変更(INSERT・UPDATE・DELETE)をリアルタイムで検知してクライアントに届ける。 Next.jsではServer Componentsがリクエスト単位で終わるため、WebSocket接続を維持できるClient Componentで実装する必要がある。 RLSが有効な場合、Realtimeも自動でRLSに従い、自分に関係ない行の変更は届かない。
'use client' // これが必須
import { useEffect } from 'react'
import { supabase } from '@/lib/supabase'
export default function PostList() {
useEffect(() => {
const channel = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('新しい投稿:', payload.new)
}
)
.subscribe()
return () => { channel.unsubscribe() } // クリーンアップ必須
}, [])
}
💡 ポイント:
useEffectの返り値でクリーンアップを忘れると、コンポーネントが消えてもWebSocket接続が残り続けてしまう。
| イベント | タイミング |
|---|---|
INSERT | 行が追加されたとき |
UPDATE | 行が更新されたとき |
DELETE | 行が削除されたとき |
* | すべての変更 |
supabase-jsはPostgREST・Auth・Storage・RealtimeをまとめてラップしたSDK。Next.jsでは「サーバー側」と「クライアント側」でクライアントの作り方を分ける必要がある。
サーバー側はLocalStorageにアクセスできないため、@supabase/ssrパッケージを使いCookie経由でセッションを引き継ぐ。
// lib/supabase/server.ts(Server Components・Route Handlers用)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createClient = () => createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookies().getAll() } }
)
// lib/supabase/client.ts(Client Components用)
import { createBrowserClient } from '@supabase/ssr'
export const createClient = () => createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
💡 ポイント:
NEXT_PUBLIC_が付くのはURLとANON_KEYだけ。SERVICE_ROLE_KEYは絶対にNEXT_PUBLIC_にしてはいけない(RLSをバイパスできるため)。
| 用途 | 使うクライアント | セッション管理 |
|---|---|---|
| Server Components | createServerClient | Cookie |
| Client Components | createBrowserClient | LocalStorage/Cookie |
| Route Handlers | createServerClient | Cookie |
| Middleware | createServerClient | Cookie |
無料枠(Nanoインスタンス)は共有CPUのため、他ユーザーの影響を受けてレスポンスが遅くなることがある。 本番ではProプラン(Microインスタンス・専有CPU)以上が必須。非アクティブ停止は本番では致命的。
無料枠の主な制約:
├── プロジェクト数:最大2つまで
├── DB容量:500MB
├── Storage:1GB
├── Edge Functions:500,000回/月
├── 非アクティブ停止:7日間で一時停止
└── CPU・メモリ:共有CPU / 500MB RAM(Nanoインスタンス)
| プラン | 月額 | CPU・メモリ | 用途 |
|---|---|---|---|
| Free | $0 | 共有CPU / 500MB RAM | 学習・MVP |
| Pro | $25〜 | 専有1vCPU / 1GB RAM(Micro) | 本番 |
| Large以上 | $110〜 | 専有CPU・高IOPS | 高負荷本番 |
💡 ポイント: Proには$10のコンピュートクレジットが付くので実質$25でMicroインスタンスが使える。2プロジェクトは本番・開発で1つずつ使い切るのが現実的。
| ミス | 原因 | 改善策 |
|---|---|---|
NEXT_PUBLIC_SERVICE_ROLE_KEYを設定 | フロントに公開してしまう | SERVICE_ROLE_KEYはNEXT_PUBLIC_をつけない |
| RLSを有効化せずに本番運用 | 全ユーザーが全データにアクセス可能 | テーブル作成後すぐにRLSをONにする習慣を |
getPublicUrlで期限付きURLを発行しようとする | メソッドの混同 | 期限付きはcreateSignedUrlを使う |
| Server ComponentでRealtimeを使う | WebSocket接続を維持できない | 'use client'のClient Componentで実装する |
| 無料枠で本番運用 | 非アクティブ停止・共有CPUの影響 | ユーザーがいる本番はProプランへ移行 |
useEffectでクリーンアップ未実装 | WebSocket接続が残り続ける | 返り値でchannel.unsubscribe()を呼ぶ |
supabase-jsはそのHTTPラッパーsupabase startはDockerで本番と同じ環境をローカルに再現する。Docker Desktopを入れておくだけでよく、Docker知識はほぼ不要makemigrations→migrateに相当するdb diff→db pushで管理する。出力がSQLそのものなので読みやすいdb diffで拾えるので「まずGUIで試してからSQLに落とす」順番もアリauth.uid()はJWTのsubフィールドからユーザーIDを取り出しているcreateServerClient)、Client ComponentはLocalStorage/Cookie(createBrowserClient)でセッション管理が異なる