話題の記事

オープンソースで話題のBaaS「Supabase」を使ってみた

オープンソースでありながら公式サイトで「Firebase Alternative」と謳っている話題のBaaS (Backend As A Service)の「Supabase」をNext.jsで試してみました。
2021.10.25

はじめに

こんにちは、CX事業本部MAD事業部の森茂です。

オープンソースでありながら公式サイトで「Firebase Alternative」と謳っている巷で話題のBaaS (Backend As A Service)の「Supabase」をNext.jsで試してみました。

Supabaseで利用できるサービスは2021年10月現在で3種類。Functionsはcoming soonでまだalpha版の状態です。Firebaseと比較されることが多いSupabaseですが、FirebaseのFirestoreがNoSQLなデータベースに対して、SupabaseではデータベースとしてRDBであるPostgreSQLを利用している部分が大きく異なります。

  • Database
  • Authentication
  • Storage
  • Functions(coming soon!)

今回はFirebaseと一番の大きな違いであるデータベースの機能をちょっとだけ試してみました。

*なお、Supabaseはオープンソースなためセルフホスティングでの運用も可能です。

Supabase

料金プラン

気になる料金プランですが、FreeProPay as you goの3種類。 データベースについて、プランごとの違いは下記のようになるようです。

- Free Pro Pay as you go
月額 無料 $25 $25+従量課金
プロジェクト数 2 1 1
DB容量 500MB 8GB $0125/GB
転送量(下りのみ) 2GB 50GB $0.09/GB
備考 1週間未使用で一時停止

今回はホビー用途となっているFreeプランを使います。無料でプロジェクトを2つ作ることができますが、APIを1週間未使用の場合はデータベースが一時停止となるようで再開する場合はダッシュボードから起動し直す必要があるようです。

サービスへの登録

では、早速Supabaseのサービスに登録を行っていきます。

Supabase.ioへの登録はGitHubアカウントを利用するようです。

「New Project」から新規プロジェクトを作成します。

プロジェクト名とデータベースのパスワード、リージョンを指定します。今回は「Tokyo」リージョンを選択しました。

「Create new project」を押下後、数分待つとプロジェクトが作成されます。

完了後に画面に表示されるProject API keyURLを後ほど利用するのでメモしておきます。(あとからでも管理画面より確認可能です)

データベースの作成

次にデータベースを作成します。左のデータベースメニューから「Create a new table」へ進みます。

今回はテーブル名をsampleにし、スキーマとしてはデフォルトで用意されるカラム(idとcreated_at)とは別にtitletextとして追加しました。

Next.js環境の構築

Supabaseを試すにあたって今回はNext.jsを利用しました。まずはTypeScriptのテンプレートを利用してプロジェクトを作成します。

$ yarn create next-app --typescript supabase-sample
$ cd supabase-sample

Supabaseのjsライブラリをインストールします。

$ yarn add @supabase/supabase-js

その他にも公式ライブラリではありませんがreact-supabaseというReact Hooksなライブラリもあり、Hooksですっきりとまとめることもできます。今回はsupabaseの仕組みを見てみたかったので公式のjsライブラリのみで構築しています。

プロジェクト作成時にSupabaseより割り当てられたURLAPI keyを環境変数として利用するため.env.localを新規作成して記載します。

.env.local

NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Supabaseクライアントの作成

Supabaseを利用するためのクライアントを用意します。

lib/supabaseClient.ts

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);

サンプルデータの用意

Supabaseの管理画面から、あらかじめ用意していたsampleテーブルにフロントエンド側から参照するためにサンプルデータをいくつか入れておきます。

SupabaseではGUIだけでなくSQLでもテーブルを作成できるためRDSで運用している場合など既存のリソースからの移行も容易かもしれません。

表示ページの作成

サンプルデータを投入したところで、データを表示するためのページを用意してきます。今回はトップページでそのまま一覧を出力するだけのシンプルなページです。

pages/index.tsx

import type { NextPage } from 'next';
import styles from '../styles/Home.module.css';
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabaseClient';

// DBのカラムにあわせた型情報
type List = {
  id: string;
  title: string;
  created_at: string;
};

const Home: NextPage = () => {
  const [list, setList] = useState<List[]>([]);
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      setLoading(true);
      // sampleテーブルから全カラムのデータをid順に取得
      // dataに入る型はそのままだとany[]となるため.from<T>で指定
      const { data, error } = await supabase.from<List>('sample').select('*').order('id');

      if (error) {
        throw error;
      }
      if (data) {
        setList(data);
      }
    } catch (error: any) {
      alert(error.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    // supabaseからデータを取得
    fetchData();
  }, []);

  if (loading) return <div>loading...</div>;
  if (!list.length) return <div>missing data...</div>;

  return (
    <div className={styles.container}>
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>TITLE</td>
            <td>CREATED_AT</td>
          </tr>
        </thead>
        <tbody>
          {list.map((item) => (
            <tr key={item.id}>
              <td>{item.id}</td>
              <td>{item.title}</td>
              <td>{item.created_at}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default Home;

さて、準備ができてところで早速試してみます。

$ yarn dev

たったこれだけでRDBからデータを出力することができました。準備の時間を含めてもかなりの速度で開発が進められそうです。

リアルタイム更新

SupabaseではRealtimeという機能があり、websocketを利用してサブスクライブすることでデータベースの反映をリアルタイムに取得することも可能です。

なお、初期状態設定ではテーブルに対してRealtimeでのサブスクリプションが認可されていないためテーブルの設定を変更します。

管理画面のSQLメニューから該当のテーブルに対してクエリを送ることで認可することができます。

alter publication supabase_realtime add table sample;

また少し見つけづらいですがGUIからも設定することができます。

なお、Realtime機能についてはセキュリティ的に気をつける点もあり、公式サイトでも利用方法について考慮するよう言及しています。

テーブルの設定が完了しところで次にNext.js側のソースを編集してきます。pages/index.tsxファイルのuseEffect内にサブスクリプションを追記していきます。

pages/index.tsx

  ...
  
  useEffect(() => {
    // supabaseからデータを取得
    fetchData();

    // subscriptionを生成
    const subscription = supabase
      .from('sample')
      // .onの第一引数には'INSERT'や'UPDATE'などアクションを限定して指定することも可能
      .on('*', (payload) => {
        fetchData();
        console.log('Change received!', payload);
      })
      .subscribe();

    return () => {
      // アンマウント時にsubscriptionを解除
      if (subscription) {
        supabase.removeSubscription(subscription);
      }
    };
  }, []);
  
  ...

用意できたところでSupabaseの管理画面からデータを更新してみるとリアルタイムで更新が反映されました。

おまけ

ついでにNext.jsの特徴のひとつでもあるSG(Static Generation)とSSR(Server-side Rendering)についても同様に利用できるか試してみました。試した限りでは特に意識することなくCSR(Client-side Rendering)と同じ用に利用できそうです。

SG(Static Generation)

http://localhost:3000/sg/[id]でアクセスした際に該当のIDの投稿が表示されるという構成にしています。

pages/sg/[id].tsx

import { NextPage, GetStaticProps, GetStaticPaths } from 'next';
import styles from '../../styles/Home.module.css';
import { supabase } from '../../lib/supabaseClient';

type List = {
  id: string;
  title: string;
  created_at: string;
};

type Props = {
  list?: List[];
  errors?: string;
};

const SgPage: NextPage<Props> = ({ list, errors }) => {
  if (errors) return <div>Error...</div>;
  if (!list?.length) return <div>missing data...</div>;

  return (
    <div className={styles.container}>
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>TITLE</td>
            <td>CREATED_AT</td>
          </tr>
        </thead>
        <tbody>
          {list.map((item) => (
            <tr key={item.id}>
              <td>{item.id}</td>
              <td>{item.title}</td>
              <td>{item.created_at}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default SgPage;

export const getStaticPaths: GetStaticPaths = async () => {
  try {
    const { data, error } = await supabase.from('sample').select('id');
    if (error) {
      throw error;
    }
    if (data) {
      const paths = data.map((post) => ({ params: { id: JSON.stringify(post.id) } }));
      return {
        paths: paths,
        fallback: "blocking",
      };
    }
    return {
      paths: [],
      fallback: false,
    };
  } catch (error) {
    return {
      paths: [],
      fallback: false,
    };
  }
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  try {
    const id = params?.id;
    const { data } = await supabase.from<List>('sample').select('*').filter('id', 'eq', id);

    return {
      props: {
        list: data,
      },
    };
  } catch (error: any) {
    return { props: { errors: error.message } };
  }
};

SSR(Server-side Rendering)

http://localhost:3000/ssr/[id]でアクセスした際に該当のIDの投稿が表示されるという構成にしています。SG版とほとんど同じです。。

pages/ssr/[id].tsx

import { NextPage, GetServerSideProps } from 'next';
import styles from '../../styles/Home.module.css';
import { supabase } from '../../lib/supabaseClient';

type List = {
  id: string;
  title: string;
  created_at: string;
};

type Props = {
  list?: List[];
  errors?: string;
};

const SgPage: NextPage<Props> = ({ list, errors }) => {
  if (errors) return <div>Error...</div>;
  if (!list?.length) return <div>missing data...</div>;

  return (
    <div className={styles.container}>
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>TITLE</td>
            <td>CREATED_AT</td>
          </tr>
        </thead>
        <tbody>
          {list.map((item) => (
            <tr key={item.id}>
              <td>{item.id}</td>
              <td>{item.title}</td>
              <td>{item.created_at}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default SgPage;

export const getServerSideProps: GetServerSideProps = async (context) => {
  try {
    const id = context.params?.id as string;
    const { data } = await supabase.from<List>('sample').select('*').filter('id', 'eq', id);

    return {
      props: {
        list: data,
      },
    };
  } catch (error: any) {
    return { props: { errors: error.message } };
  }
};

さいごに

今回試した部分はデータの取得だけですが、あっという間にリアルタイムのデータ取得まで構築することができました。開発速度を高めることはもちろん、リレーショナル・データベースを利用できることで複数のテーブルとも紐付けることもできるので仕様の変更にも対応しやすいのかなと感じました。既存サービスからも要件さえあえば移行を比較的容易に進めることができるのではないかと思います。

また、Next.jsとも相性がよさそうです。認証機能サービスのAuthenticationとも組み合わせることで爆速なアプリ開発ができるのではないでしょうか。

まだ進化途中でプロダクション利用では未知数なところも多いサービスではありますが今後の進化がとても楽しみです。 個人的にもさらに深堀りしていきたいと思います。