今回やること
Supabaseは、PostgreSQL、認証、ストレージなどをまとめて利用できる開発基盤です。
この記事では機能を広く試すのではなく、次の1本の流れだけを作ります。
- メールアドレスとパスワードで認証する
profilesテーブルへ表示名を保存する- Row Level Security(RLS)により、自分の行だけを読み書きする
RLSは、データベースが行ごとにアクセスを判定する仕組みです。ブラウザからデータベースAPIを呼ぶ構成では、RLSが認可の中心になります。
前提条件
- Node.js 24 LTSとnpmが使える(推奨)
- Docker Desktopなど、Docker互換の実行環境が起動している
- HTML、TypeScript、SQLの基礎を知っている
Supabase CLIはNode.js 20以上を必要とし、この記事で使うVite 8の最低要件はNode.js 20.19以上または22.12以上です。両方を迷わず動かすため、初学者にはNode.js 24 LTSを推奨します。
今回はローカル開発環境だけを使います。ローカル環境には本番用の安全設定がないため、外部へ公開しないでください。
1. プロジェクトとSupabase CLIを準備する
npm create vite@latest supabase-profile -- --template vanilla-ts
cd supabase-profile
npm install
npm install @supabase/supabase-js
npm install --save-dev supabase
Supabase CLIをプロジェクトの開発依存へ入れたため、以降はnpx supabaseで実行します。
npx supabase init
npx supabase start
初回はDockerイメージの取得に時間がかかります。起動後に表示されるAPI URLとクライアント用キーを後で使います。
2. profilesテーブルとRLSを作る
マイグレーションファイルを作ります。
npx supabase migration new create_profiles
作成されたsupabase/migrations/<日時>_create_profiles.sqlへ、次のSQLを書きます。
create table public.profiles (
id uuid primary key references auth.users (id) on delete cascade,
display_name text not null
check (char_length(trim(display_name)) between 1 and 50),
updated_at timestamptz not null default now()
);
alter table public.profiles enable row level security;
create policy "Users can read their own profile"
on public.profiles
for select
to authenticated
using ((select auth.uid()) = id);
create policy "Users can insert their own profile"
on public.profiles
for insert
to authenticated
with check ((select auth.uid()) = id);
create policy "Users can update their own profile"
on public.profiles
for update
to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
ローカルデータベースを作り直し、マイグレーションを適用します。
npx supabase db reset
UPDATEには対応するSELECTポリシーも必要です。この例は、読込・新規作成・更新のすべてで、ログイン中の利用者IDとprofiles.idが一致する行だけを許可します。
3. ブラウザ用クライアントを作る
プロジェクト直下に.env.localを作ります。値はnpx supabase startの出力からコピーしてください。
VITE_SUPABASE_URL=http://127.0.0.1:54321
VITE_SUPABASE_PUBLISHABLE_KEY=ここにローカルのPublishableキー
CLIのバージョンによっては、低権限の従来キーがanon keyと表示されます。その場合は、その値をVITE_SUPABASE_PUBLISHABLE_KEYへ設定して、この学習例を進められます。
Publishableキーと従来のanonキーは、ブラウザに含めることを想定した低権限キーです。ログイン後は利用者のJWTとRLSによりアクセスが判定されます。キーを公開できることと、データを無制限に公開してよいことは別です。
一方、Secretキーと従来のservice_roleキーはRLSを迂回する高権限の秘密情報です。ブラウザ、ViteのVITE_*変数、Git、記事、チャットへ絶対に入れないでください。 今回は使用しません。
src/supabase.tsを作ります。
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabasePublishableKey =
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;
if (!supabaseUrl || !supabasePublishableKey) {
throw new Error("SupabaseのURLまたはPublishableキーが未設定です");
}
export const supabase = createClient(
supabaseUrl,
supabasePublishableKey,
);
.env.localを変更した後は、Viteの開発サーバーを再起動します。実際のプロジェクトでは.env.localをGitへ追加しません。
4. 最小画面を作る
index.htmlを次の内容に置き換えます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supabase Profile</title>
</head>
<body>
<main>
<h1>Supabase Profile</h1>
<label>
メールアドレス
<input id="email" type="email" autocomplete="email" />
</label>
<label>
パスワード
<input id="password" type="password" autocomplete="current-password" />
</label>
<label>
表示名
<input id="display-name" maxlength="50" />
</label>
<div>
<button id="sign-up" type="button">登録</button>
<button id="sign-in" type="button">ログイン</button>
<button id="save" type="button">保存</button>
<button id="load" type="button">読込</button>
<button id="sign-out" type="button">ログアウト</button>
</div>
<p id="message" role="status" aria-live="polite"></p>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
src/main.tsを次の内容に置き換えます。
import "./style.css";
import { supabase } from "./supabase";
const emailInput = document.querySelector<HTMLInputElement>("#email")!;
const passwordInput =
document.querySelector<HTMLInputElement>("#password")!;
const nameInput =
document.querySelector<HTMLInputElement>("#display-name")!;
const message = document.querySelector<HTMLParagraphElement>("#message")!;
function show(text: string): void {
message.textContent = text;
}
document.querySelector("#sign-up")!.addEventListener("click", async () => {
const { data, error } = await supabase.auth.signUp({
email: emailInput.value,
password: passwordInput.value,
});
if (error) {
show(error.message);
return;
}
if (!data.session) {
show("確認メールを開いた後、ログインしてください。");
return;
}
show("登録とログインが完了しました。");
});
document.querySelector("#sign-in")!.addEventListener("click", async () => {
const { error } = await supabase.auth.signInWithPassword({
email: emailInput.value,
password: passwordInput.value,
});
show(error ? error.message : "ログインしました。");
});
document.querySelector("#save")!.addEventListener("click", async () => {
const displayName = nameInput.value.trim();
if (!displayName) {
show("表示名を入力してください。");
return;
}
const { data: authData, error: authError } =
await supabase.auth.getUser();
if (authError || !authData.user) {
show("先にログインしてください。");
return;
}
const { error } = await supabase.from("profiles").upsert({
id: authData.user.id,
display_name: displayName,
updated_at: new Date().toISOString(),
});
show(error ? error.message : "自分のプロフィールを保存しました。");
});
document.querySelector("#load")!.addEventListener("click", async () => {
const { data, error } = await supabase
.from("profiles")
.select("display_name")
.single();
if (error) {
show(error.message);
return;
}
nameInput.value = data.display_name;
show("自分のプロフィールを読み込みました。");
});
document.querySelector("#sign-out")!.addEventListener("click", async () => {
const { error } = await supabase.auth.signOut();
show(error ? error.message : "ログアウトしました。");
});
既存のsrc/style.cssはそのままでも構いません。見やすくしたい場合は、ラベルとボタンへ余白を追加してください。
成功確認
npm run dev
- メールアドレス、6文字以上のパスワード、表示名を入力する
- 「登録」を押す
- セッションが作られた場合は、そのまま「保存」→「読込」を押す
- 確認メールを求められた場合は、ローカルのMailpit URLを
npx supabase statusで確認し、メール内のリンクを開いてからログインする - 保存した表示名が読み込まれれば成功
ホストされたSupabaseでは、Email Confirmationsが有効だとsignUp後に利用者情報は返ってもsessionはnullです。確認前にプロフィール保存を実行してはいけません。この例はdata.sessionを確認し、その場合はログインへ進むよう案内します。
RLSを確認する
「ログアウト」を押してから「読込」を押してください。ログインしていないリクエストにはauthenticatedロール向けポリシーが適用されないため、プロフィールは取得できません。
さらに別の利用者を作ると、その利用者は自分と同じIDの行だけを取得できます。画面側でIDを隠すことではなく、データベース側のRLSが境界になっています。
よくあるつまずき
プロジェクト作成直後にNode.jsのバージョンエラーが出る
Vite 8はNode.js 20.19以上または22.12以上を必要とします。Node.js 18や20.18以前では、npm create vite@latestまたはnpm run devが失敗します。
node --version
古い場合はNode.js 24 LTSへ更新し、ターミナルを開き直してからプロジェクトを作り直してください。
npx supabase startが失敗する
- Node.js 24 LTSを推奨。最低でもCLIとViteの両方の要件を満たしているか確認する
- Docker Desktopなどが起動しているか確認する
supabase initを実行したフォルダーにいるか確認する
node --version
npx supabase status
Invalid API keyと表示される
.env.localのキーを確認し、npm run devを再起動します。Secretキーやservice_roleキーへ置き換えてはいけません。
保存時にRLSエラーになる
- ログインが完了しているか
npx supabase db resetでポリシーを適用したかprofiles.idへログイン中の利用者IDを設定しているか
RLSを無効化して回避するのではなく、ポリシーと認証状態を確認してください。
確認メールが届かない
ローカル環境のメールは実際の受信箱ではなくMailpitで確認します。ホスト環境では、DashboardのAuth URL設定とメール送信設定も確認してください。
練習
- 2人の利用者を作り、それぞれ別の表示名を保存する
- ログインを切り替え、別の利用者の表示名が見えないことを確認する
- 表示名を51文字にして、データベースの制約で拒否されることを確認する
成功する操作だけでなく、許可されない操作が失敗することも認可テストです。
後片付け
Viteの開発サーバーを起動しているターミナルでは、Ctrl+Cを押して停止します。続いて、Supabaseを初期化したプロジェクトのフォルダーで次を実行します。
npx supabase stop
これでローカルSupabase用のDockerコンテナが停止します。通常のstopではローカルデータが保持されるため、次回npx supabase startを実行すると学習を再開できます。
次のステップ
SSRフレームワークで認証Cookieを扱う場合は、ブラウザだけのcreateClientをそのまま流用せず、公式の@supabase/ssrガイドに従ってブラウザ用とサーバー用のクライアントを分けます。
次は、プロフィール作成をデータベーストリガーで自動化する方法、またはRLSのテスト方法を学びましょう。Realtime、Storage、Edge Functionsは、それぞれ別の学習ゴールとして追加するのが安全です。
参考リソース
- Supabase公式: CLIでローカル開発を始める
- Supabase公式: Row Level Security
- Supabase公式: APIキーの種類
- Supabase JavaScript: signUp
- Supabase公式: Server-Side Auth
- Vite公式: Getting Started