Node.js

【簡単】Next.js + supabase(完全無料ダウンロード版)環境構築!

どうも、katです。

今回は、Next.jssupabase(ローカルダウンロード版)を使った開発環境の構築を行っていきます!

エラーが出た場合の対処法についても書いていきますので、ぜひ参考にしていただけたらと思います。

Next.jsとは?

React.jsベースのサーバーサイドJavascript(Node.js)のフレームワークになります。

要は、PHPなどと同じく、サーバーを立ち上げて、サイトの裏側で動くJavascriptのフレームワークです。

特徴としては、リクエスト時だけでなく、ソースのビルド時に静的HTMLを生成してしまう機能があるなど、レンダリング周りの自由度が高かったりします。

また、高いシェア率を誇るフロントエンドのJSフレームワークであるReact.jsをベースにしていることもあり、元々React.jsを使っていたエンジニアにとってはハードルが低いフレームワークと言えます。(Next.js自体も、サーバーサイドのフレームワークでは高いシェア率を誇っています

supabaseとは?

supabaseは、RDB(postgresql)が使えるBaaS(Backend as a Service)となっており、データ更新と同時に画面も変更するなどが簡単に行えるという特徴もあり、リアルタイムのサービスによく使われているようです。

BaaSとは、基本的に提供している会社のクラウドにデータがあり、そことクライアントサーバー間で通信をしてデータをやり取りするサービスであり、通常は有料です。

supabaseも、基本有料ですが、機能制限付きの無料プランもあります。

しかし、supabase自体をローカルにダウンロードすることで、完全無料で利用する方法もあり、今回はそちらを使って構築を進めていきます。

supabaseインストール

今回は、タイトルにもあるとおり、完全無料のダウンロード版を使用します。

ここでは記事が少し長くなるので、別の記事でsupabaseのインストール手順を記載していますので、まずは以下の記事の手順でsupabaseをインストールしていただけたらと思います。

【完全無料】supabaseをローカルにダウンロードして無料で利用する方法どうも、katです。 今回は、firebaseとよく比較される、RDBのBaaS(Backend as a Service)であるsup...

必要なテーブルなど作成

supabaseのインストールが完了したら、supabaseのDB(postgresql)にログインし、以下のSQLを実行して必要なテーブルなどを作成します。

-- Create a table for public "profiles"
create table public.profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  username text unique,
  avatar_url text,

  primary key (id),
  unique(username)
);

alter table public.profiles enable row level security;

create policy "Public profiles are viewable by everyone."
  on public.profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on public.profiles for insert
  with check ( auth.uid() = id );

create policy "Users can update own profile."
  on public.profiles for update
  using ( auth.uid() = id );

-- Set up Realtime!
begin;
  drop publication if exists supabase_realtime;
  create publication supabase_realtime;
commit;
alter publication supabase_realtime add table profiles;

-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');

create policy "Avatar images are publicly accessible."
  on storage.objects for select
  using ( bucket_id = 'avatars' );

create policy "Anyone can upload an avatar."
  on storage.objects for insert
  with check ( bucket_id = 'avatars' );

Next.js構築

supabaseのインストールとテーブルの作成が完了したら、次にNext.jsの環境を作っていきます。

Next.jsプロジェクト作成

まずは、下記のコマンドでNext.jsのプロジェクトを作成します。

$ npx create-next-app {プロジェクト名}

{プロジェクト名}の部分は任意の文字列になります。

例えば「supabase-nextjs」という名前のプロジェクトにしたい場合は、

$ npx create-next-app supabase-nextjs

とします。(以後、上記のプロジェクト名で作成した前提でコマンドを記載していきますが、別のプロジェクト名にした場合は適宜読み替えてください)

supabaseとの連携設定

次に、Next.jsとsupabaseを接続するための設定をしていきます。

まずは先ほどのプロジェクト作成で作成されたプロジェクト名のフォルダに移動し、必要なパッケージをインストールします。

$ cd supabase-nextjs    ← プロジェクトフォルダに移動
$ npm install @supabase/supabase-js

次に、プロジェクトフォルダ内の.env.localファイルを書き換えていきます。(ファイルがない場合は新規作成します。)

# ファイルがない場合
touch .env.local

# ファイルを開く
vi .env.local

# 下記を記載する
NEXT_PUBLIC_SUPABASE_URL={supabaseのURL}
NEXT_PUBLIC_SUPABASE_ANON_KEY={supabaseのANON_KEY}

supabaseのURLとANON_KEYについては、supabase構築時に作成したsupabaseフォルダ内の、.supabase/docker/docker-compose.ymlの、下記の行に記載があります。

# 23行目あたり
API_EXTERNAL_URL: http://{設定したIP or ドメイン}:8000    ← supabaseのURL

# 82行目あたり
ANON_KEY: {ランダムな文字列}    ← supabaseのANON_KEY

動作確認用の認証ページ作成

うまく連携できているか確認するため、実際に簡単な認証ページを作ってみます。

下記のファイルを全てコピペして作成します。

■utils/supabaseClient.js

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)

■components/Auth.js

import { useState } from 'react'
import { supabase } from '../utils/supabaseClient'

export default function Auth() {
  const [loading, setLoading] = useState(false)
  const [email, setEmail] = useState('')

  const handleLogin = async (email) => {
    try {
      setLoading(true)
      const { error } = await supabase.auth.signIn({ email })
      if (error) throw error
      alert('Check your email for the login link!')
    } catch (error) {
      alert(error.error_description || error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="row flex flex-center">
      <div className="col-6 form-widget">
        <h1 className="header">Supabase + Next.js</h1>
        <p className="description">Sign in via magic link with your email below</p>
        <div>
          <input
            className="inputField"
            type="email"
            placeholder="Your email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <button
            onClick={(e) => {
              e.preventDefault()
              handleLogin(email)
            }}
            className="button block"
            disabled={loading}
          >
            <span>{loading ? 'Loading' : 'Send magic link'}</span>
          </button>
        </div>
      </div>
    </div>
  )
}

■components/Account.js

import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'

export default function Account({ session }) {
  const [loading, setLoading] = useState(true)
  const [username, setUsername] = useState(null)
  const [website, setWebsite] = useState(null)
  const [avatar_url, setAvatarUrl] = useState(null)

  useEffect(() => {
    getProfile()
  }, [session])

  async function getProfile() {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      let { data, error, status } = await supabase
        .from('profiles')
        .select(`username, website, avatar_url`)
        .eq('id', user.id)
        .single()

      if (error && status !== 406) {
        throw error
      }

      if (data) {
        setUsername(data.username)
        setWebsite(data.website)
        setAvatarUrl(data.avatar_url)
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  async function updateProfile({ username, website, avatar_url }) {
    try {
      setLoading(true)
      const user = supabase.auth.user()

      const updates = {
        id: user.id,
        username,
        website,
        avatar_url,
        updated_at: new Date(),
      }

      let { error } = await supabase.from('profiles').upsert(updates, {
        returning: 'minimal', // Don't return the value after inserting
      })

      if (error) {
        throw error
      }
    } catch (error) {
      alert(error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="form-widget">
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session.user.email} disabled />
      </div>
      <div>
        <label htmlFor="username">Name</label>
        <input
          id="username"
          type="text"
          value={username || ''}
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="website">Website</label>
        <input
          id="website"
          type="website"
          value={website || ''}
          onChange={(e) => setWebsite(e.target.value)}
        />
      </div>

      <div>
        <button
          className="button block primary"
          onClick={() => updateProfile({ username, website, avatar_url })}
          disabled={loading}
        >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>

      <div>
        <button className="button block" onClick={() => supabase.auth.signOut()}>
          Sign Out
        </button>
      </div>
    </div>
  )
}

■pages/index.js(上書き)

import { useState, useEffect } from 'react'
import { supabase } from '../utils/supabaseClient'
import Auth from '../components/Auth'
import Account from '../components/Account'

export default function Home() {
  const [session, setSession] = useState(null)

  useEffect(() => {
    setSession(supabase.auth.session())

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])

  return (
    <div className="container" style={{ padding: '50px 0 100px 0' }}>
      {!session ? <Auth /> : <Account key={session.user.id} session={session} />}
    </div>
  )
}

Next.js起動

ファイルの作成、編集が完了したら、Next.jsを下記のコマンドで起動します。

$ npm run dev

http://{サーバーのドメイン}:3000 にアクセスします。

※サーバーのドメインは、next.jsを立ち上げたサーバーのドメインやIPアドレスになります(localhostや192.168.33.10など)

下記のような表示がされればOKです。

ここで以下の画像のように「このサイトにアクセスできません」といったエラーになる場合は、ファイアフォールの設定が必要かもしれません。

その場合は、ファイアフォールで3000番ポートを許可してあげる必要があるため、下記のコマンドを実行します。

$ ufw allow 3000
$ ufw reload

動作確認

それではうまくNext.jsとSupabaseが連携できているか確認しましょう。

ログインメール送信

まずはYour emailの欄にメールアドレスを入力します。

そして「Send magic link」をクリックします。(メール自体は後述する、supabase付属のテスト用メールサーバーに送信されるため、実際のgmailなどには送信されません)

ここで、以下のように「Request Failed」と表示される場合があります。

その場合は、supabaseが立ち上がっていない場合がありますので、supabaseを構築したディレクトリに移動し、以下のコマンドを実行してsupabaseを立ち上げます。

$ cd /var/www/supabase  ←supabaseを構築した際のディレクトリに移動
$ supabase start

上記を実施してもうまくいかない場合は、Next.jsの.env.localのNEXT_PUBLIC_SUPABASE_URLのドメイン(IPアドレス)が間違えている可能性がありますので、再度、サーバーのものと一致しているかどうか確認して見てください。

メール確認

デフォルトの設定では、メールはgmailなどではなく、supabaseに付属するメールサーバーに送信されます。

以下のURLをブラウザで表示します。

http://{サーバーのドメイン}:9000

※サーバーのドメインはNext.jsの画面を表示した際と同様です。

すると以下のような画面が表示されます。

表示されたら、画面上部にある「Monitor」をクリックし、該当のメールをクリックします。

するとメールの内容が表示されるので、メール内の「Confirm your email address」リンクをクリックします。

下記の画面が表示されるので「Name」に適当に入力して「Update」ボタンを押下してみます。

profilesテーブルのusernameカラムに、入力された値が登録されていれば、動作確認は完了です。

もし、メールに記載のリンクをクリックしてもページに遷移できない場合は、supabaseの「.supabase/docker/docker-compose.yml」ファイルの以下の箇所の設定が間違っている可能性がありますので、再度確認していただけたらと思います。

API_EXTERNAL_URL: http://{ドメイン or IPアドレス}:8000
GOTRUE_SITE_URL: http://{ドメイン or IPアドレス}:8000

最後に

いかがだったでしょうか?

多少やることは多かったかと思いますが、私も全部合わせて20分ほどで終わりましたので、難しくはないかと思います。

今回はsupabaseに関しては、DBに登録、参照するくらいしか使いませんでしたが、今後、リアルタイムサービスなども作ってみたいと思います。

以上、「Next.js + supabase(完全無料ダウンロード版)環境構築!」でした〜

参考

https://kohsuk.tech/2021/8/24/

https://supabase.com/docs/guides/with-nextjs

ABOUT ME
kat
プログラマー歴7年、2歳の子供を持つパパです。 興味のあることはプログラミングや今後のIT技術などです。 趣味でオンラインカードゲームのサイトを運営しております。 プログラミングを通して社会に貢献していきたいです。