Next.jsでsupabaseを使って、認証からデータベースの更新までをやってみる
Supabaseは、「The Open Source Firebase Athlternative」を謳っています。プロプライエタリなFirebaseの代わりにオープンな技術スタックで構成されてバックエンドのサービスです。
Supabaseで提供している認証とデータベースを使って、一連の基本的な機能をさわってみます。
Supabaseの新規プロジェクト作成
Supabaseのサイトより、サインアップします。
ダッシュボードから、プロジェクトを作成しておきます。
GitHubのOauthアプリケーション作成
GitHubでこちらから新しくアプリケーションを作成します。
- Application name
アプリケーションの名称を入力します。 - Homepage URL
ホームページのURLを入力します。有効なURLを入力する必要があるので、ない場合はhttp://locahost:3000/
などを入力します。 - Authorization callback URL
Supabaseのダッシュボードから、プロジェクトのサイドメニューの「Settings」を選びます。「API」の「Config」欄にURLがあるので、そのURLに/auth/v1/callback
を追加して入力します。
「Registration application」ボタンをクリックして、アプリケーションを作成します。
アプリケーションの詳細が表示されるので、「Client secrets」欄の「Generate a new client secret」ボタンをクリックしてシークレットキーを生成します。
Supabase側の設定
Authentication設定
作成したプロジェクトのサイドメニューの「Authentication」を選んで、「Settings」を選択します。
「External OAuth Providers」欄の「GitHub enabled」のトグルをオンにします。
そうすると、「GitHub client ID」と「GitHub secret」のテキスト入力欄が表示されます。先ほど保持したGitHubの「Client ID」と「Client secrets」をそれぞれに入力します。「Save」ボタンをクリックして、設定を保存します。
テーブル作成
ユーザーの情報を保持するテーブルを作成します。
create table users (
id uuid references auth.users not null primary key,
fullname text,
avatarurl text,
nickname text
);
alter table users enable row level security;
create policy "Can view own user data." on users for select using (auth.uid() = id);
create policy "Can update own user data." on users for update using (auth.uid() = id);
/**
* Supabase Authでサインアップしたら自動でユーザーを作成するためのトリガー
*/
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.users (id, fullname, avatarurl)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
認証のテーブルに新しくレコードが作成されたら、認証テーブルのuuidをキーにGitHubから取得した情報を付与して、このユーザーテーブルにレコードを保存するようにします。
「SQL」をサイドメニューから開いて、「Open queries」の「Query-1」を開きます。クエリーエディターが開くので、上記のSQLを入力して、「RUN」をクリックしてSQLを実行します。
Next.jsセットアップ
アプリケーション作成
TypescriptをベースにNext.jsプロジェクトを作成します。
npx create-next-app --typescript --use-npm supabase-nextjs-auth
cd supabase-nextjs-auth
npm install --save-dev typescript @types/react
touch tsconfig.json
Supabaseクライアントをインストールします。
npm install @supabase/supabase-js
Supabaseクライアント
「Settings」から「API」を選択します。
URLとパブリックキーを控えます。
.env.local
というファイルを作成して、NEXT_PUBLIC_SUPABASE_ANON_KEY
にパブリックキー、NEXT_PUBLIC_SUPABASE_URL
にURLをセットします。
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
Supabaseクライアントを初期化するファイルを作成します。
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
認証機能
まず、認証の処理をする関数を定義します。
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";
export default function userUser() {
const [session, setSession] = useState();
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
setSession(session);
}
);
return () => {
authListener.unsubscribe();
};
}, []);
function signInWithGithub() {
supabase.auth.signIn({ provider: "github" });
}
function signOut() {
supabase.auth.signOut();
}
return {
session,
signInWithGithub,
signOut,
};
}
アプリケーション作成時にpages/index.js
というファイルがに作成されるので、そのファイルを削除します。
新しくpages/index.tsx
というファイルを下記の内容で作成して、先ほどの関数をコールするようにします。
import useUser from "../hooks/useUser";
export default function Home() {
const { session, signOut, signInWithGithub } = useUser();
return (
<>
{session ? (
<button onClick={() => signOut()}>サインアウト</button>
) : (
<button onClick={() => signInWithGithub()}>GitHubでログイン</button>
)}
</>
);
}
「GitHubでログイン」ボタンだけがある簡単なページになります。ボタンをクリックしたらSupabaseの認証します。
Next.jsの開発サーバーを起動します。
npm run dev
動作確認
ブラウザーで、http://localhost:3000
を開きます。
「GitHubでログイン」ボタンをクリックすると、GitHub側の認証処理が実行されます。
GitHubアプリケーションでのログインを許可するか確認されるので、「Authorize xxx」(xxxはそれぞれのGitHubユーザーや組織名がはいります)ボタンをクリックします。
これで、Supabaseで認証処理が実行され、ユーザーテーブルにレコードが作成されます。
supabaseで、「Authentication」の「Users」で確認します。
「Tables」のusers
テーブルにもレコードができているのを確認できます。
ユーザー情報
ユーザーテーブルからユーザー情報を取得して、更新できるようにします。
取得
ユーザー情報をステートにセットします。
export default function userUser() {
const [session, setSession] = useState();
+ const [user, setUser] = useState(null);
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(
セッションがはれたら、ユーザー情報を取得するためのuseEffect
を追加します。
};
}, []);
+ useEffect(() => {
+ const setupUser = async () => {
+ if (session?.user.id) {
+ const { data: user } = await supabase
+ .from("users")
+ .select("*")
+ .eq("id", session.user.id)
+ .single();
+ setUser(user);
+ }
+ };
+ setupUser();
+ }, [session]);
function signInWithGithub() {
supabase.auth.signIn({ provider: "github" });
}
user
を参照できるようにreturn
します。
return {
session,
+ user,
signInWithGithub,
signOut,
};
ファイル全体
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";
export default function userUser() {
const [session, setSession] = useState();
const [user, setUser] = useState(null);
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
setSession(session);
}
);
return () => {
authListener.unsubscribe();
};
}, []);
useEffect(() => {
const setupUser = async () => {
if (session?.user.id) {
const { data: user } = await supabase
.from("users")
.select("*")
.eq("id", session.user.id)
.single();
setUser(user);
}
};
setupUser();
}, [session]);
function signInWithGithub() {
supabase.auth.signIn({ provider: "github" });
}
function signOut() {
supabase.auth.signOut();
}
return {
session,
signInWithGithub,
signOut,
};
}
次に、取得したユーザー情報をページに表示します。
import useUser from "../hooks/useUser";
export default function Home() {
- const { session, signOut, signInWithGithub } = useUser();
+ const { session, user, signOut, signInWithGithub } = useUser();
return (
<>
{session ? (
- <button onClick={() => signOut()}>サインアウト</button>
+ <div>
+ <p>Hello, {user && user.fullname}</p>
+ <button onClick={() => signOut()}>サインアウト</button>
+ </div>
) : (
<button onClick={() => signInWithGithub()}>GitHubでログイン</button>
)}
ファイル全体
import useUser from "../hooks/useUser";
export default function Home() {
const { session, user, signOut, signInWithGithub } = useUser();
return (
<>
{session ? (
<div>
<p>Hello, {user && user.full_name}</p>
<button onClick={() => signOut()}>サインアウト</button>
</div>
) : (
<button onClick={() => signInWithGithub()}>GitHubでログイン</button>
)}
</>
);
}
更新
次にユーザーのニックネームを更新できるようにします。
Supabaseでレコードを更新する関数を追加して、関数をreturn
に含めます。
supabase.auth.signOut();
}
+ async function updateNickname(nickname) {
+ const { data: newUser } = await supabase
+ .from("users")
+ .update({ nickname })
+ .match({ id: user.id })
+ .single();
+ setUser(newUser);
+ }
+
return {
session,
user,
signInWithGithub,
signOut,
+ updateNickname,
};
}
ファイル全体
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase-client";
export default function userUser() {
const [session, setSession] = useState();
const [user, setUser] = useState(null);
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
setSession(session);
}
);
return () => {
authListener.unsubscribe();
};
}, []);
useEffect(() => {
const setupUser = async () => {
if (session?.user.id) {
const { data: user } = await supabase
.from("users")
.select("*")
.eq("id", session.user.id)
.single();
setUser(user);
}
};
setupUser();
}, [session]);
function signInWithGithub() {
supabase.auth.signIn({ provider: "github" });
}
function signOut() {
supabase.auth.signOut();
}
async function updateNickname(nickname) {
const { data: newUser } = await supabase
.from("users")
.update({ nickname })
.match({ id: user.id })
.single();
setUser(newUser);
}
return {
session,
user,
signInWithGithub,
signOut,
updateNickname,
};
}
ページにフォームを追加して、そのサブミットした内容を処理するための記述を追加します。
+import { useEffect, useState, useRef } from "react";
import useUser from "../hooks/useUser";
export default function Home() {
- const { session, user, signOut, signInWithGithub } = useUser();
+ const { session, user, signOut, signInWithGithub, updateNickname } =
+ useUser();
+ const [editingNickname, setEditingNickname] = useState(false);
+ const newNickname = useRef();
+
+ async function setNickname(evt) {
+ evt.preventDefault();
+
+ try {
+ updateNickname(newNickname.current.value);
+ setEditingNickname(false);
+ newNickname.current.value = "";
+ } catch (err) {
+ console.log("エラーが発生しました");
+ }
+ }
return (
<>
{session ? (
<div>
- <p>Hello, {user && user.full_name}</p>
+ <p>Hello, {user?.nickname ? user.nickname : user?.full_name}</p>
+ <div>
+ {editingNickname ? (
+ <form onSubmit={setNickname}>
+ <input
+ type="text"
+ required
+ ref={newNickname}
+ placeholder="新しいニックネーム"
+ />
+ <button type="submit">設定</button>
+ </form>
+ ) : (
+ <div>
+ <button onClick={() => setEditingNickname(true)}>
+ ニックネームを更新
+ </button>
+ </div>
+ )}
+ </div>
<button onClick={() => signOut()}>サインアウト</button>
</div>
) : (
ファイル全体
import { useEffect, useState, useRef } from "react";
import useUser from "../hooks/useUser";
export default function Home() {
const { session, user, signOut, signInWithGithub, updateNickname } =
useUser();
const [editingNickname, setEditingNickname] = useState(false);
const newNickname = useRef();
async function setNickname(evt) {
evt.preventDefault();
try {
updateNickname(newNickname.current.value);
setEditingNickname(false);
newNickname.current.value = "";
} catch (err) {
console.log("エラーが発生しました");
}
}
return (
<>
{session ? (
<div>
<p>Hello, {user?.nickname ? user.nickname : user?.full_name}</p>
<div>
{editingNickname ? (
<form onSubmit={setNickname}>
<input
type="text"
required
ref={newNickname}
placeholder="新しいニックネーム"
/>
<button type="submit">設定</button>
</form>
) : (
<div>
<button onClick={() => setEditingNickname(true)}>
ニックネームを更新
</button>
</div>
)}
</div>
<button onClick={() => signOut()}>サインアウト</button>
</div>
) : (
<button onClick={() => signInWithGithub()}>GitHubでログイン</button>
)}
</>
);
}
「ニックネームを更新」ボタンをクリックして、ニックネームを入力して「設定」ボタンをクリックすることでニックネームを更新できます。
おわりに
様々なクラウドのサービスをバックエンドに使ってきましたが、SQLでいろいろできるのはなんか新鮮な感じがしました。
NoSQL系のデータベースは最初馴染みやすく、さくっとつくるにはいいのですが、設計技法をちゃんとしていないとスケールに苦労することが多かったです。
Supabaseの場合、PostgreSQLベースなので、技術がこなれているので安心して使えます。
公式で提供しているJavascriptのライブラリもジョイン文など簡潔に書けるので、よりフロントエンドの開発に集中できる感じがしました。
参考
Supbaseの知識を深めるために、ドキュメントの翻訳に取り組んでいます。
Next.jsのクイックスタートも参考にしてください。認証から、Storageを使って、プロフィール画像まで登録できます。
Discussion
大変わかりやすい記事を書いて頂きありがとうございました。とても参考になりました。
現在、supabase-js のバージョンが2に上がったようで、supabase.auth.signIn({ provider: "github" }); が supabase.auth.signInWithOAuth({ provider: "github" }); に変わっているそうです。
僕の環境だとその変更をしたところ動いたので、時間があるときに編集していただけるとありがたいです。