Supabase入門 - AuthとRLSで自分のプロフィールだけ読み書きする

中級 | 25分 で読める | 2025.12.02

公式ドキュメント

今回やること

Supabaseは、PostgreSQL、認証、ストレージなどをまとめて利用できる開発基盤です。

この記事では機能を広く試すのではなく、次の1本の流れだけを作ります。

  1. メールアドレスとパスワードで認証する
  2. profilesテーブルへ表示名を保存する
  3. 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
  1. メールアドレス、6文字以上のパスワード、表示名を入力する
  2. 「登録」を押す
  3. セッションが作られた場合は、そのまま「保存」→「読込」を押す
  4. 確認メールを求められた場合は、ローカルのMailpit URLをnpx supabase statusで確認し、メール内のリンクを開いてからログインする
  5. 保存した表示名が読み込まれれば成功

ホストされたSupabaseでは、Email Confirmationsが有効だとsignUp後に利用者情報は返ってもsessionnullです。確認前にプロフィール保存を実行してはいけません。この例は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設定とメール送信設定も確認してください。

練習

  1. 2人の利用者を作り、それぞれ別の表示名を保存する
  2. ログインを切り替え、別の利用者の表示名が見えないことを確認する
  3. 表示名を51文字にして、データベースの制約で拒否されることを確認する

成功する操作だけでなく、許可されない操作が失敗することも認可テストです。

後片付け

Viteの開発サーバーを起動しているターミナルでは、Ctrl+Cを押して停止します。続いて、Supabaseを初期化したプロジェクトのフォルダーで次を実行します。

npx supabase stop

これでローカルSupabase用のDockerコンテナが停止します。通常のstopではローカルデータが保持されるため、次回npx supabase startを実行すると学習を再開できます。

次のステップ

SSRフレームワークで認証Cookieを扱う場合は、ブラウザだけのcreateClientをそのまま流用せず、公式の@supabase/ssrガイドに従ってブラウザ用とサーバー用のクライアントを分けます。

次は、プロフィール作成をデータベーストリガーで自動化する方法、またはRLSのテスト方法を学びましょう。Realtime、Storage、Edge Functionsは、それぞれ別の学習ゴールとして追加するのが安全です。

参考リソース

← 一覧に戻る
PR
PR
PR
PR