0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

一番分かりやすいsupabase x Next.js環境構築 with google認証

Last updated at Posted at 2025-07-19
  • supabase x next.jsのセットアップや主要機能の実装

    • google認証でのログイン, セッション取得, DB CRUD, ストレージへのアップロード
  • アーキテクチャ

    • next.js(docker), supabase
    • 2025/7現在の最新バージョンを使用
  • 公式ドキュメント

  • 環境設定&セットアップ
  • docker-compose
version: "3.8"

services:  
	nextjs:    
		image: node:22    
		container_name: nextjs-app    
		working_dir: /app    
		volumes:      
			- ./:/app    
		ports:      
			- "3000:3000"    
		environment:      
			- NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co      
			- NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key    
		tty: true
  • コンテナに入る(僕はvs codeのattach to running container)
  • Next.jsプロジェクト作成
npx create-next-app@latest
  • supabaseクライアントインストール
npm install @supabase/supabase-js
  • プロジェクトルートでnpm install する

  • supabase google認証

    • supabaseプロジェクト作成

    image(16).png

    • Authentication/Sign In・Providersからgoogleを選択

    image(18).png

    • Callback urlを保存(後でgoogle cloud consoleで使う)

      image(19)(1).png

    • google cloud consoleでクライアントID, クライアントシークレットを作成

      • プロジェクト作成

      image(16)(1).png

      image(17).png

      • APIとサービス/認証情報

      image(19).png

      • OAuthクライアントIDを作成

      image(20).png

      • プロジェクトで最初に作成するとき、同意画面の構成を求められる

      image(21).png

      • 開始をクリック

      image(22).png

      • 順に入力
      • 任意のアプリ名と連絡先のメアド

      image(20)(1).png

      • 今回は外部を選択

      image(23).png

      • 連絡先のメアド

      image(21)(1).png

      • 作成完了

      • 改めてOAuthクライアントIDの作成

      image(24).png

      • アプリケーションの種類はウェブ
      • 承認済みのJS生成元は開発用にlocalhost
        • (追加で本番用のドメインを登録してもよい)
      • 承認済みのリダイレクトURIは先ほどのsupabase google認証のCallback URLを入力

      image(22)(1).png

      • 作成完了
      • クライアントIDとクライアントシークレットを保存しておく

      image(23)(1).png

      • supabaseでgoogle認証を追加する
        • 先ほど取得したクライアントIDとクライアントシークレットを入力

      image(24)(1).png

  • .env.localやdockerfile, docker-composeに書いておく

NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhb...

GOOGLE_AUTH_CLIENT_ID=780764369850-xxx.apps.googleusercontent.com
GOOGLE_AUTH_CLIENT_SECRET=GOCS..
'use client'

import { supabase } from '@/utils/supabase/supabaseClient'

export default function Home() {
  const signInWithGoogle = async () => {
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        //redirectTo: 'http://localhost:3000/auth/callback',
      },
    })
  }

  return (
    <main>
      <h1>ログインページ</h1>
      <button onClick={signInWithGoogle}>Googleでログイン</button>
    </main>
  )
}

  • 実行してみる
    • cssを割り当てていない, 「Googleでログイン」部分をクリック

image(25).png

  • 見慣れたログイン画面

image(25)(1).png

  • supabaseコンソールでユーザー登録を確認

image(26).png

  • セッション取得
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/utils/supabase/supabaseClient'
import type { Session } from '@supabase/supabase-js'

export default function CallbackPage() {
  const router = useRouter()
  const [session, setSession] = useState<Session | null>(null)
  const [loading, setLoading]   = useState<boolean>(true)

  useEffect(() => {
    // 1. 直近のセッションを取得
    const fetchSession = async () => {
      const { data, error } = await supabase.auth.getSession()

      if (error) console.error('セッション取得失敗:', error)
      setSession(data.session ?? null)
      setLoading(false)
    }

    fetchSession()

    // 2. ログイン/ログアウトをリアルタイムで監視
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, newSession) => {
      setSession(newSession)
    })

    // 3. クリーンアップ
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  /* --------- 画面描画 --------- */
  if (loading) {
    return <p>ログイン処理中...</p>
  }

  if (session) {
    return (
      <div>
        <p>ログイン成功!ようこそ {session.user.email}</p>
        <button onClick={() => router.push('/')}>トップへ戻る</button>
      </div>
    )
  }

  return (
    <div>
      <p>認証に失敗しました。再度お試しください。</p>
      <button onClick={() => router.push('/')}>トップへ戻る</button>
    </div>
  )
}

image(27).png

  • crud
    • 簡単なTodoライクアプリ
    • 認証状態を保持し、自動でRLSが適応される
  • RLS
    • row level security
    • ログインユーザーごとにアクセス可能なデータ行を細かく制御できる仕組み
    • DBだけでなくStorage にも適用され、自分の画像だけをアップロード・閲覧できる
    • 簡単な SQL ポリシーを書くことで、安全なアクセス制御が実現できる
'use client'

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

type SampleRow = {
  id: string
  created_at: string
  text: string
  user_id: string
}

export default function SampleCrudPage() {
  const [samples, setSamples] = useState<SampleRow[]>([])
  const [inputText, setInputText] = useState('')
  const [userId, setUserId] = useState<string | null>(null)

  /* ──────── セッション取得 ──────── */
  useEffect(() => {
    const getSession = async () => {
      const { data } = await supabase.auth.getSession()
      setUserId(data.session?.user.id ?? null)
    }
    getSession()
  }, [])

  /* ──────── 初回 READ ──────── */
  useEffect(() => {
    if (userId) fetchSamples()
  }, [userId])

  /* 取得 (自分のデータのみ) */
  const fetchSamples = async () => {
    const { data, error } = await supabase
      .from('SampleTable')
      .select('*')
      .eq('user_id', userId)
      .order('created_at', { ascending: false })

    if (error) {
      console.error('データ取得エラー:', error)
    } else {
      setSamples(data)
    }
  }

  /* 追加 (user_id 必須) */
  const addSample = async () => {
    if (!userId) return

    const { error } = await supabase
      .from('SampleTable')
      .insert({ text: inputText, user_id: userId })

    if (error) {
      alert('追加失敗: ' + error.message)
    } else {
      setInputText('')
      fetchSamples()
    }
  }

  /* 削除 (RLS が自分のデータのみ許可) */
  const deleteSample = async (id: string) => {
    const { error } = await supabase
      .from('SampleTable')
      .delete()
      .eq('id', id)

    if (error) {
      alert('削除失敗: ' + error.message)
    } else {
      fetchSamples()
    }
  }

  return (
    <div style={{ padding: '2rem' }}>
      <h1>SampleTable CRUD</h1>

      <input
        value={inputText}
        onChange={e => setInputText(e.target.value)}
        placeholder="テキストを入力"
      />
      <button onClick={addSample} disabled={!userId}>
        追加
      </button>

      <ul>
        {samples.map(item => (
          <li key={item.id}>
            {item.text}{' '}
            <button onClick={() => deleteSample(item.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}
  • table, RLS
/* -----------------------------------------------------------
   Todo ライクな「SampleTable」 ― スキーマ & RLS ポリシー
   -----------------------------------------------------------
   - 各ユーザーが自分のタスクだけを CRUD 可能
   - Supabase がデフォルトで用意する auth.users(id) と連携
   ----------------------------------------------------------- */

/* 1) テーブル定義  ----------------------------------------- */
create table public."SampleTable" (
  id         uuid primary key default uuid_generate_v4(),
  created_at timestamp        default now(),
  text       text             not null,
  user_id    uuid             not null references auth.users(id)
);

/* 2) RLS を有効化  ----------------------------------------- */
alter table public."SampleTable" enable row level security;

/* 3) ポリシー: 自分の行だけ参照・操作できる --------------- */
/* ── SELECT(読み取り) ── */
create policy "sample_select_own"
on public."SampleTable"
for select
using ( auth.uid() = user_id );

/* ── INSERT(追加) ── */
create policy "sample_insert_own"
on public."SampleTable"
for insert
with check ( auth.uid() = user_id );

/* ── UPDATE(更新) ── */
create policy "sample_update_own"
on public."SampleTable"
for update
using ( auth.uid() = user_id )
with check ( auth.uid() = user_id );

/* ── DELETE(削除) ── */
create policy "sample_delete_own"
on public."SampleTable"
for delete
using ( auth.uid() = user_id );
  • SQL editorで実行

image(28).png

  • 動作する

image(29).png

  • DBにレコードが登録されていることを確認

image(30).png

  • ファイルサーバー

    • こちらも自動でRLSが適応される
    • 事前にバケットを作成

    image(31).png

    • new bucketから新規バケットを作成
      • public bucketはONにしてください(後から変更できます)

    image(32).png

image(33).png

  • 今回はget started quickly

image(34).png

  • 今回は2行目の「uidでフォルダーを作ってそこにアクセスできるようにする」を選択

image(35).png

  • allow operationのCRUD全てにチェック

image(36).png

  • ポリシーが追加される

image(37).png

  • アップロードとパブリックURLの取得
'use client'

import { ChangeEvent, useState } from 'react'
import Image from 'next/image'
import { supabase } from '@/utils/supabase/client'

export default function SingleImageUploader() {
  const [path, setPath] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)

  /* ── アップロード ── */
  const onFile = async (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    /* ① ログイン必須 ―― 未ログインなら弾く */
    const {
      data: { session },
    } = await supabase.auth.getSession()
    if (!session) {
      return setError('ログインしてからアップロードしてください')
    }

    /* ② ファイルパス = {uid}/{timestamp}.{ext} */
    const uid = session.user.id
    const ext = file.name.split('.').pop()
    const key = `${uid}/${Date.now()}.${ext}`

    /* ③ upload ―― 初回 INSERT, 2回目以降は upsert ⇒ UPDATE */
    const { error } = await supabase.storage.from('images').upload(key, file, {
      upsert: true,
      cacheControl: '3600',
    })

    if (error) return setError(error.message)
    setPath(key)
  }

  /* ── URL 変換 ── */
  const url =
    path && supabase.storage.from('images').getPublicUrl(path).data.publicUrl

  /* ── UI ── */
  return (
    <main className="p-6 space-y-4">
      <input type="file" accept="image/*" onChange={onFile} />

      {error && <p className="text-red-600">{error}</p>}

      {url && (
        <div className="relative w-60 h-60">
          <Image
            src={url}
            alt="uploaded"
            fill
            sizes="240px"
            className="object-cover rounded"
          />
        </div>
      )}
    </main>
  )
}

image(38).png

image(39).png

  • 画像アップロードとパブリックURLでの画像表示ができた

まとめ

  • 以上です。お疲れ様でした
  • supabaseのようなBaaSを使うことでかなり開発期間を短縮できます
    • MCPを使いAI駆動で利用することもできるようです
  • 個人開発では熱意が冷めないうちに形にすることが大切だと思います
  • 一緒に頑張りましょう
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?