メインコンテンツまでスキップ

クイックスタート:Next.js

イントロ

この例では、SupabaseとNext.jsを使って、シンプルなユーザー管理アプリを(ゼロから)構築する手順を紹介します。内容は以下のとおりです。

  • Supabase Database:ユーザーデータを保存するためのPostgresデータベースです。
  • Supabase Auth:ユーザーはマジックリンクでサインインできます(パスワードは不要、メールのみ)。
  • Supabase Storage:ユーザーは写真をアップロードできます。
  • 行単位セキュリティー:データは保護されており、個人が自分のデータにしかアクセスできないようになっています。
  • インスタントAPI:データベースのテーブルを作成すると、APIが自動的に生成されます。

このガイドの最後には、ユーザーがログインして基本的なプロフィール情報を更新できるアプリが完成します。

Supabaseユーザー管理の例

プロジェクトのセットアップ

ビルドを開始する前に、データベースとAPIをセットアップします。Supabaseで新しいプロジェクトを立ち上げ、データベース内に「スキーマ」を作成するだけです。

プロジェクトの作成

  1. app.supabase.comにアクセスします。
  2. 「New Project」をクリックします。
  3. プロジェクトの詳細を入力します。
  4. 新しいデータベースが起動するのを待ちます。

データベース・スキーマの設定

これからデータベース・スキーマを設定します。SQLエディターの「User Management Starter」クイック・スターターを使用するか、下記のSQLをコピー/ペーストして自分で実行できます。

1. 「SQL」セクションに移動します。
2. 「User Management Starter」をクリックします。
3. 「Run」をクリックします。

APIキーを取得

データベースのテーブルをいくつか作成したので、自動生成されたAPIを使ってデータを挿入する準備ができました。API設定からURLとanonキーを取得する必要があります。

1. 「Settings」セクションに移動します。
2. サイドバーの「API」をリックします。
3. そのページでAPI URLを探します。
4. 「anon」と「service_role」キーを探します。

アプリの構築

それでは早速、Next.jsアプリを一から作ってみましょう。

Next.jsアプリの初期化

create-next-appを使って、supbase-nextjsというアプリを初期化します。

npx create-next-app supabase-nextjs
cd supabase-nextjs

そして、唯一追加する必要がある依存関係のsupbase-jsをインストールしましょう。

npm install @supabase/supabase-js

最後に、環境変数を.env.localに保存します。 必要なのは、APIのURLと、先ほどコピーしたanonキーだけです。

.env.local
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

API認証ができたので、Supabaseクライアントを初期化するためのヘルパーファイルを作成しましょう。 これらの変数はブラウザー上で公開されますが、データベースに行単位セキュリティーが設定されているので、全く問題ありません。

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)

また、オプションとして、CSSファイルのstyles/globals.cssを更新して、アプリの外観を整えます。 こちらにあるファイルを参照してください。

ログイン・コンポーネントの設定

ログインとサインアップを管理するReactコンポーネントを設定しましょう。マジック・リンクを使用することで、ユーザーはパスワードを使わずに電子メールでサインインできます。

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

アカウント・ページ

ユーザーがログインした後、プロフィールの詳細を編集したり、アカウントを管理できるようにします。

そのための新しいコンポーネント、Account.jsを作りましょう。

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を更新しましょう。

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

更新が完了したら、ターミナル・ウィンドウでこれを実行します。

npm run dev

そして、ブラウザーでlocalhost:3000を開くと、完成したアプリを見ることができます。

Supabase User demo

おまけ:プロフィール写真

Supabaseのプロジェクトには、写真や動画などの大容量ファイルを管理するためのストレージが用意されています。

アップロード・ウィジェットの作成

ユーザーがプロフィール写真をアップロードできるように、ユーザーのアバターを作成しましょう。まず、新しいコンポーネントを作成します。

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

export default function Avatar({ url, size, onUpload }) {
const [avatarUrl, setAvatarUrl] = useState(null)
const [uploading, setUploading] = useState(false)

useEffect(() => {
if (url) downloadImage(url)
}, [url])

async function downloadImage(path) {
try {
const { data, error } = await supabase.storage.from('avatars').download(path)
if (error) {
throw error
}
const url = URL.createObjectURL(data)
setAvatarUrl(url)
} catch (error) {
console.log('Error downloading image: ', error.message)
}
}


async function uploadAvatar(event) {
try {
setUploading(true)

if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}

const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`

let { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)

if (uploadError) {
throw uploadError
}

onUpload(filePath)
} catch (error) {
alert(error.message)
} finally {
setUploading(false)
}
}

return (
<div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="avatar image"
style={{ height: size, width: size }}
/>
) : (
<div className="avatar no-image" style={{ height: size, width: size }} />
)}
<div style={{ width: size }}>
<label className="button primary block" htmlFor="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
)
}

新しいウィジェットの追加

そして、このウィジェットをアカウント・ページに追加します。

components/Account.js
// Import the new component
import Avatar from './Avatar'

// ...

return (
<div className="form-widget">
{/* Add to the body */}
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}
/>
{/* ... */}
</div>
)

次のステップ

この段階で、完全に機能するアプリケーションが完成しました。