「スキーマ変更で障害が起きたら...」から解放!Supabase CLIで安心チーム開発
投稿日: 2025年06月21日
この記事では、チーム開発で Supabase を利用する際、開発用としてローカル環境に Supabase を構築して利用する方法について解説します。
アプリ開発の初期には、テーブルレコードの追加や削除だけでなく、データベースの初期化やスキーマの変更といった破壊的な操作を繰り返し行なう場面が多々あります。しかし、これをチーム共有の Supabase で行なってしまうと、他のメンバーの開発作業に思わぬ影響を与えてしまうことがあります。こうしたリスクを避けつつ、安心して開発を進める方法として、自分の PC 上に Supabase を構築してしまう、という手があります(無料アカウントでのプロジェクト数の制限も気にせずに済みます)。
公式が提供している Supabase CLI を使えば、比較的かんたんにローカル環境に Supabase を立ち上げることができます。この資料では、その手順をチュートリアル形式で説明していきます。
なお、ここでは Windows 環境を想定し、WSL2 (Windows Subsystem for Linux)と Docker Desktop がすでにインストールされ有効化されていること、また Next.js を開発フレームワークとして使用していることを前提とします。
WSL2 と Docker Desktop のインストール方法については、丁寧な解説記事がウェブに多数あるので、そちらを参照してください。
ここでは例として、プロジェクト名を「fuga」として進めていきます。まず、以下の手順で Next.js プロジェクトを作成します。
PS> npx create-next-app@latest fuga --typescript
PS> cd fuga
PS> npm i
次に、追加で以下のライブラリをインストールします。
PS> npm i -D supabase prisma tsx
PS> npm i @supabase/supabase-js
このなかの supabase
が、今回の主役である Supabase CLI となります(CLI は Command Line Interface の略です)。このツールを使って、ローカル環境に開発用の Supabase を構築していきます。
念のため、この段階で「開発モードの起動」と「ビルド」が正常に動くことを確認しておいてください。
PS> npm run dev
PS> npm run build
まず、バージョンを確認しておきます。
PS> npx supabase --version
2.26.9
つづいて、次のコマンドでローカル Supabase を構築するための初期化を実行します。途中、設定に関する質問が表示されますが、すべて「N(デフォルト)」で問題ありません。
PS> npx supabase init
Generate VS Code settings for Deno? [y/N]
Generate IntelliJ Settings for Deno? [y/N]
Finished supabase init.
処理が完了するとプロジェクトのルートに supabase
というフォルダが作成されます。このフォルダ内の config.toml
という設定ファイルを開いて、次のように project_id
を fuga
から fuga-supabase
に変更しておきます。
project_id = "fuga-supabase"
これは、アプリ本体(fuga
)の Docker コンテナと混同しないための工夫です(今回はアプリ本体を Docker で動かすわけではありませんが…)。
次のコマンドで、ローカル Supabase を起動します。
PS> npx supabase start
初回は構築処理も含まれるため、かなり大きめの Docker イメージをダウンロードと、その展開が必要なためそこそこの時間がかかります。
もし Docker や WSL2 のインストールや設定が正しくできていない場合は、ここでエラーになります(例として、次のようなメッセージが表示されます)。
failed to inspect service: error during connect: Get "http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/v1.50/containers/supabase_db_fuga-supabase/json": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.
Docker Desktop is a prerequisite for local development. Follow the official docs to install: https://docs.docker.com/desktop
ローカル Supabase 起動に成功すると、最終的に以下のようなメッセージが表示されます。
Started supabase local development setup.
API URL: http://127.0.0.1:54321
GraphQL URL: http://127.0.0.1:54321/graphql/v1
S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
http://127.0.0.1:54323
Inbucket URL: http://127.0.0.1:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
anon key: eyJhbGc....
service_role key: eyJhbGc....
S3 Access Key: 625729....
S3 Secret Key: 850181....
S3 Region: local
Studio URL
として表示される http://127.0.0.1:54323 にブラウザでアクセスすると、クラウド版の Supabase で見慣れた管理画面が開きます(ただし、クラウド版と完全に同じというわけではなく、一部の機能に違いがあります)。
ローカル Supabase を「停止」するには、以下のコマンドを実行します。CPU やメモリの使用量がそこそこ大きいので、開発作業が終わったら必ず明示的に停止してください。
PS> npx supabase stop
Stopped supabase local development setup.
Local data are backed up to docker volume. Use docker to show them: docker volume ls --filter label=com.supabase.cli.project=fuga-supabase
VSCode を閉じても、ローカル Supbase が自動で停止はされるわけではない点に注意してください。コマンドを使って明示的に指定が必要です。
再びローカル Supabase を起動するために npx supabase start
を実行すれば OK なのですが、Windows 環境では次のようなエラーが出て失敗することがあります(僕の環境では再現率100%です🫠)。
PS> npx supabase start
failed to create docker container: Error response from daemon: Conflict. The container name "/supabase_vector_fuga-supabase" is already in use by container "e83ed8c92100c6ea865b87477e420d347c3479c74af034a9178f69835e2d7e7a". You have to remove (or rename) that container to be able to reuse that name.
これは npx supabase stop
の終了処理が不完全な場合に発生し、本来削除されるはずのコンテナが残ってしまっていることが原因です。状況を確認するには、docker ps -a
というコマンドを使います。
PS> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
df9695df9ea9 public.ecr.aws/supabase/vector:0.28.1-alpine "sh -c 'cat <<'EOF' …" About a minute ago Exited (0) About a minute ago supabase_vector_fuga-supabase
残っているコンテナは、以下のコマンドで削除します(name=supabase_vector_fuga-supabase
の部分はプロジェクトに合わせてください)。
docker rm $(docker ps -a -q --filter "name=supabase_vector_fuga-supabase")
もう一度状況を確認し、以下のようにコンテナが残っていなければ OK です。
PS> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
この状態になれば、npx supabase start
でローカル Supabase を問題なく起動できます。ハマりがちなポイントなので覚えておいてください。
以降、ローカル Supabase が起動している状態で作業してください。
ここからは、ローカル Supabase と Prisma を接続するための設定を進めていきます。
まず、ローカル Supabase への接続情報を .env
ファイルに環境変数として書き込みます。設定する値は、ローカル Supabase を起動したときに表示される DB URL
(例: postgresql://postgres:postgres@127.0.0.1:54322/postgres
)です。
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
DIRECT_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
次に、Prisma の初期化コマンドを実行します。
PS> npx prisma init --datasource-provider postgresql
これにより、プロジェクトのルートに prisma
というフォルダが作成され、そのなかに schema.prisma
というファイルが作成されます。このファイルを次のように編集します。
// 1. npx prisma db push
// 2. npx prisma generate
generator client {
provider = "prisma-client-js"
// output = "../src/generated/prisma" // 要コメントアウト
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
model Post {
id String @id @default(uuid())
title String
content String
createdAt DateTime @default(now()) @map("created_at")
@@map("posts")
}
次のコマンドで、スキーマに基づき、ローカル Supabase にテーブルを作成されるはずです。
PS> npx prisma db push
PS> npx prisma generate
問題なく処理できているかを確認してみます。http://127.0.0.1:54323 にアクセスしてください。次のようにテーブルが作成できていればOKです。
データベースに初期データを投入するスクリプトを作成します。prisma
フォルダのなかに seed.ts
という名前でファイルを作成して、以下の内容を記述します。
// npx prisma db seed
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const main = async () => {
// Postテーブルから既存の全レコードを削除
await prisma.post.deleteMany();
// Postレコードの挿入
const p1 = await prisma.post.create({
data: {
title: "投稿1",
content: "投稿1の本文。<br/>投稿1の本文。投稿1の本文。",
},
});
const p2 = await prisma.post.create({
data: {
title: "投稿2",
content: "投稿2の本文。<br/>投稿2の本文。投稿2の本文。",
},
});
console.log(JSON.stringify(p1, null, 2));
console.log(JSON.stringify(p2, null, 2));
};
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
このプログラムを実行するために、package.json
に次の内容(prisma
のセクション)を追加してください。
// ...略...
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
// ...略...
次のコマンドで seed.ts
を実行して、初期データを投入します。
PS> npx prisma db seed
ブラウザから http://127.0.0.1:54323 にアクセスして、次のようにテーブルにレコードが追加されていればOKです。
ローカル Supabase と Prisma を接続し、Next.js のバックエンドでデータが正しく取得できるかを確認してみます。
まずは ./src/lib/prisma.ts
を新規作成し、以下のコードを記述してください(クラウドの Supabase を使うときと同じ設定でOKです。ローカルに対応させるための特別な設定は含んでいません)。
import { PrismaClient } from "@prisma/client";
// データベース接続用のインスタンスを作成する関数
const prismaClientSingleton = () => {
return new PrismaClient();
};
// グローバルスコープに prismaGlobal という変数が存在することをTypeScriptに伝える型定義
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
// 既存の prismaGlobal があればそれを使用し、なければ新しくインスタンスを作成
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
// 作成した prisma インスタンスを他から利用できるようにエクスポート
export default prisma;
// 開発環境の場合のみ、作成したインスタンスをグローバルスコープに保存し、
// ホットリロード時の再利用を可能にする
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
つづいて、./src/app/api/posts/route.ts
を新規作成し、次のコードを記述してください。
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { Post } from "@prisma/client";
export const dynamic = "force-dynamic";
export const GET = async () => {
try {
const posts: Post[] = await prisma.post.findMany({
orderBy: {
createdAt: "desc",
},
});
return NextResponse.json(posts);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "投稿記事の一覧の取得に失敗しました" },
{ status: 500 }
);
}
};
動作を確認してみます。次のようになればOKです。
ローカル Supabase の認証機能が利用できることを確認していきます。
まずは、.env
に以下のように環境変数を設定します。設定する値は、ローカル Supabase を起動したときに表示される API URL
と anon key
の値となります。
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
DIRECT_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJxxxxxxxxxxxx
./src/lib/supabase.ts
を新規作成し、以下のコードを記述します。
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
./src/app/signup/page.tsx
を新規作成し、以下のコードを記述します。
"use client";
import { supabase } from "@/lib/supabase";
import { useState } from "react";
export default function Page() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: "http://localhost:3000/",
},
});
if (error) {
alert("登録に失敗しました。");
} else {
setEmail("");
setPassword("");
alert("登録に成功しました。");
}
};
return (
<div className="flex justify-center pt-12">
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-[400px]">
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900"
>
メールアドレス
</label>
<input
type="email"
name="email"
id="email"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="name@example.com"
autoComplete="email"
required
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900"
>
パスワード
</label>
<input
type="password"
name="password"
id="password"
placeholder="••••••••"
autoComplete="off"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
</div>
<div>
<button
type="submit"
className="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
>
登録
</button>
</div>
</form>
</div>
);
}
実行します。
確認します。
ローカル環境での Supabase では、ユーザ確認用のメール認証は最初から「オフ」になっています。supabase/config.toml
の enable_confirmations = false
がそれにあたります。
お疲れ様でした😆
開発環境(ローカル Supabase)と本番環境(クラウド Supabase)は、環境変数を切り替えるだけで使い分けられます。
これでもう「誰かがスキーマ変えてテーブル消えた!😱」なんて大惨事とは無縁です。ローカル環境では失敗を気にせず、大胆にトライしてテーブルを何回でも葬り去ってください。最高の状態だけをクラウドに😶🌫️。