Supabase 初心者向け解説、Supabase CLI インストール(Scoop)

更新日:2025/04/09

Supabaseってなに?

超ざっくり言うと「Firebaseのオープンソース版みたいなもの」です。
サーバーサイドの面倒な処理をノーコード/ローコードで簡単にできるサービスです。

Supabaseは「BaaS(Backend as a Service)」に分類され、バックエンドのサービスを用意してくれます!

主な特徴:

  • PostgreSQLベースのリアルタイムなデータベース
  • ユーザー認証機能(ログインとか)
  • ストレージ(画像とかファイルを保存できる)
  • API自動生成(DB作るとREST APIが勝手にできる)
  • ローカル開発にも対応(→ Supabase CLI)

PostgreSQLは?

PostgreSQL(ポスグレ)は、世界中で使われている
無料で高機能なデータベースソフトのことです。

ソフト名特徴
MySQL軽くて速い。WordPressや多くのWebサーバーで使われる。
PostgreSQL高機能で拡張性が高い。Supabaseが使っている。
SQLite軽量。ファイル1つだけで動く。スマホアプリやテスト用に◎
MariaDBMySQLと互換性あり。オープンソース志向の人に人気。
Oracle Database商用。大企業・官公庁向けで超高機能。
SQL ServerMicrosoft製。Windows環境でよく使われる。

アカウント作成

メールアドレスで登録、GitHub認証できるようです。

アカウント作成後、ダッシュボードにアクセスできます

ダッシュボードについて

+ New Project で新しくプロジェクトを作成

organization を設定してない場合は作成する必要があります

organizationは会社や個人利用といった組織の設定ですね。

New Projectはプロジェクト名、データベースパスワード、リージョンを選択して作成できます!

プロジェクト作成するとProject API Keysも/settings/apiで確認できるようです

Get started by building out your database

プロジェクトページのウェルカムメッセージの下にデータベースの構築から始めましょうとありますので、まず用意されているSQLをSQLエディタで流してみることで構築を進めてみるとよさそうです、、

下記のDOCSを参考にSQLを実行してみます
https://supabase.com/docs/guides/getting-started/quickstarts/nextjs

-- Create the table
create table instruments (
  id bigint primary key generated always as identity,
  name text not null
);
-- Insert some sample data into the table
insert into instruments (name)
values
  ('violin'),
  ('viola'),
  ('cello');

alter table instruments enable row level security;

「Table Editor」で作成が確認できました!

Supabase CLI + Scoopについて

Supabase CLIって?

Supabaseをローカルで開発できるようにするコマンドツールです。
例:コマンドでデータベース立ち上げたり、テーブル作ったり、マイグレーションしたりできます。

Scoopって?

Windowsのパッケージ管理ツールです。
macOSでいうHomebrewみたいな感じ。コマンドで簡単にツールをインストールできます。

またScoopは管理者権限不要で個人フォルダにインストールで

C:\Users\<ユーザー名>\app\scoop\にインストールする場合

  • 下記は管理者権限で開く必要はありません
cd ~/Downloads
  • インストーラーをダウンロード
Invoke-WebRequest -UseBasicParsing get.scoop.sh -OutFile installer.ps1
  • インストーラー先を指定して実行
./installer.ps1 -ScoopDir "C:\Users\<ユーザー名>\app\scoop"

>>
Initializing...
Downloading...
Creating shim...
Adding ~\app\scoop\shims to your path.
Scoop was installed successfully!
Type 'scoop help' for instructions.
  • インストール成功の確認
scoop config
  • Supabaseのバケット(レポジトリ)を登録
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
>>
Checking repo... OK
The supabase bucket was added successfully.
  • Supabase CLI をインストール
scoop install supabase
>>


-----

'supabase' (2.20.12) was installed successfully!
  • インストール確認
supabase --version
>>
2.20.12
C:\
└─ Users\
   └─ <ユーザー名>\
      └─ app\
         └─ scoop\                       ← Scoop本体のルート
            ├─ apps\                    ← 実際にインストールされたアプリの本体が入る
            │  └─ supabase\
            │     └─ current\           ← Supabase CLI の実体(実行ファイルなど)
            │        ├─ supabase.exe   ← CLI本体(ここが実行される)
            │        └─ その他ファイル
            ├─ shims\                   ← CLI実行用のショートカット群(Pathに通ってる)
            │  └─ supabase.exe         ← 実行用の中継ファイル(shim)
            ├─ cache\                  ← 一時ファイル(ZIPなどが入る)
            ├─ buckets\                ← インストール元(GitHubレポジトリ)情報
            └─ persist\                ← 永続的な設定や保存データ(ツールによる)

制作物のおすすめ(Supabase練習向け)

以下はHTML/CSS/JS/PHP/Node.jsスキルを活かせる実践的な練習ネタです:

① ログイン付き掲示板(PHP or Node.js)

  • Supabaseの認証機能DB(投稿内容)
  • ログインしてる人だけが書き込みできる

② TODOアプリ(HTML/CSS/JS + Supabase)

  • ログイン不要でもOK
  • SupabaseのDBを使ってCRUD(登録・編集・削除)
  • JSから直接REST API呼ぶだけでも作れる!

③ ファイルアップローダー

  • Supabaseのストレージ機能を使って画像などをアップロード

④ お問い合わせフォーム + 管理画面(PHP)

  • お問い合わせ内容をSupabaseに保存
  • 管理者だけが内容を一覧・返信できるページを作る

⑤ 勉強メモ共有サービス(Node.js + Supabase)

  • 自分の学習メモを投稿・保存・公開
  • タグ・検索機能もつけるとステップアップ

WordPressから記事取得(Next.js & Supabase)

supabaseで下記の様なテーブルを作成(Supabase CLIかダッシュボードで確認)

supabase login
Hello from Supabase! Press Enter to open browser and login automatically.

Here is your login link in case browser did not open https://supabase.com/dashboard/cli/login?session_id=
Enter your verification code: xxxxxxxx
Token cli created successfully.

You are now logged in. Happy coding!
supabase link --project-ref <project-ref>

// 

CREATE TABLE IF NOT EXISTS "public"."posts" (
    "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
    "title" "text",
    "content" "text",
    "source_url" "text",
    "published_at" timestamp without time zone
);

Supabase に一意制約(UNIQUE制約)を追加する

ALTER TABLE posts
ADD CONSTRAINT unique_source_and_date
UNIQUE (source_url, published_at);

Next.js プロジェクト作成

npx create-next-app@latest wp-supa-search --typescript

Supabase SDK インストール

npm install @supabase/supabase-js

Supabase SDK(@supabase/supabase-js)ってなに?

Supabase の REST / Realtime / Auth 機能を、JavaScript / TypeScript から簡単に扱えるようにしたクライアントライブラリ

.env.local(環境変数ファイル)を作成

プロジェクトのダッシュボード「API Settings」から確認

NEXT_PUBLIC_SUPABASE_URL=https://あなたのプロジェクト.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=ここにanonキー

src\lib\supabase.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)

src\lib\supabase.tsを作成

// Next.js の App Router で API Route を返すためのレスポンスユーティリティ
import { NextResponse } from 'next/server'
// Supabase クライアントの読み込み
import { supabase } from '@/lib/supabase'

// 取得対象となる WordPress サイトのURLリスト(複数対応)
const WP_SITES = [
  'https://yoursite.com/',
  'https://yoursite.com/',
  'https://yoursite.com/',
]

// WordPress REST API で一度に取得できる最大件数(100件)
const PER_PAGE = 100;

export async function GET() {
  const created: string[] = [];
  const updated: string[] = [];
  const unchanged: string[] = [];

  for (const site of WP_SITES) {
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const url = `${site}/wp-json/wp/v2/posts?_embed&per_page=${PER_PAGE}&page=${page}`;

      try {
        console.log(`🔍 Fetching from: ${url}`);

        const res = await fetch(url, {
          // ヘッダーに User-Agent を追加してVercelからWordPressにアクセスできるようにする(Bot判定で User-Agent が引っかかってる?)
          headers: {
            "User-Agent":
              "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
          },
        });

        if (!res.ok) {
          console.error(`❌ Failed to fetch ${url} → status: ${res.status}`);
          break;
        }

        const posts = await res.json();
        console.log(`✅ ${site} page ${page} - ${posts.length} posts fetched`);

        if (posts.length === 0) break;

        for (const post of posts) {
          const { title, content, date, link } = post;
          const titleText = title?.rendered || "(no title)";
          const contentText = content?.rendered || "";
          const publishedAt = new Date(date).toISOString();

          const { data: existing } = await supabase
            .from("posts")
            .select("title, content")
            .eq("source_url", link)
            .eq("published_at", publishedAt)
            .maybeSingle();

          if (!existing) {
            const { error } = await supabase.from("posts").insert([
              {
                title: titleText,
                content: contentText,
                source_url: link,
                published_at: publishedAt,
              },
            ]);
            if (!error) {
              created.push(link);
              console.log(`🆕 Created: ${link}`);
            }
          } else if (
            existing.title !== titleText ||
            existing.content !== contentText
          ) {
            const { error } = await supabase
              .from("posts")
              .update({
                title: titleText,
                content: contentText,
              })
              .eq("source_url", link)
              .eq("published_at", publishedAt);

            if (!error) {
              updated.push(link);
              console.log(`🔄 Updated: ${link}`);
            }
          } else {
            unchanged.push(link);
          }
        }

        page += 1;
        hasMore = posts.length === PER_PAGE;
      } catch (err) {
        console.error(`💥 Exception while fetching ${site}:`, err);
        hasMore = false;
      }
    }
  }

  console.log(
    `✨ Fetch complete - Created: ${created.length}, Updated: ${updated.length}, Unchanged: ${unchanged.length}`
  );

  return NextResponse.json({
    created: created.length,
    updated: updated.length,
    unchanged: unchanged.length,
    details: {
      created,
      updated,
      unchanged,
    },
  });
}

src\app\page.tsxを作成

"use client";
import { useEffect, useState } from "react";
import { supabase } from "@/lib/supabase";

type Post = {
  id: string;
  title: string;
  content: string;
  source_url: string;
  published_at: string;
};

const PAGE_SIZE = 10;

export default function Home() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState("");

  useEffect(() => {
    const fetchPosts = async () => {
      const from = (page - 1) * PAGE_SIZE;
      const to = from + PAGE_SIZE - 1;

      let query = supabase
        .from("posts")
        .select("*", { count: "exact" })
        .order("published_at", { ascending: false })
        .range(from, to);

      if (search.trim() !== "") {
        query = query.or(
          `title.ilike.%${search.trim()}%,content.ilike.%${search.trim()}%`
        );
      }

      const { data, error, count } = await query;

      if (!error && data) {
        setPosts(data);
        setTotal(count || 0);
      } else {
        console.error(error);
      }
    };

    fetchPosts();
  }, [page, search]);

  const totalPages = Math.ceil(total / PAGE_SIZE);

  // WordPress の記事を取得中かどうか
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<null | {
    created: string[];
    updated: string[];
    unchanged: string[];
  }>(null);

  const fetchPosts = async () => {
    setLoading(true);
    setResult(null);
    try {
      const res = await fetch("/api/fetch-posts");
      const json = await res.json();
      setResult(json.details);
    } catch (err) {
      console.error("取得エラー:", err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <main
      style={{
        padding: "2rem",
        fontFamily: "sans-serif",
        maxWidth: 800,
        margin: "auto",
      }}
    >
      <h1>クロスサイト検索ポータル</h1>

      <input
        type="text"
        placeholder="タイトルや本文を検索"
        value={search}
        onChange={(e) => {
          setSearch(e.target.value);
          setPage(1);
        }}
        style={{
          padding: "0.75rem",
          width: "100%",
          marginBottom: "1.5rem",
          border: "1px solid #ccc",
          borderRadius: "4px",
          fontSize: "1rem",
        }}
      />

      <button
        onClick={fetchPosts}
        disabled={loading}
        style={{
          padding: "0.6rem 1.2rem",
          backgroundColor: "#0070f3",
          color: "#fff",
          border: "none",
          borderRadius: "4px",
          cursor: "pointer",
          marginBottom: "1.5rem",
        }}
      >
        {loading ? "取得中..." : "記事を再取得"}
      </button>
<p>Vercel上で WordPress API にアクセスできない	403エラー(おそらくCloudflare・WAF・Wordfenceなどの制限)</p>
<p>ローカルではWordPress API の内容	正常に取得できる✅</p>
      {result && (
        <div style={{ marginBottom: "2rem" }}>
          <h3>取得結果</h3>

          <p>
            <strong>新規:</strong> {result.created.length}件
          </p>
          <ul>
            {result.created.map((url) => (
              <li key={url}>
                <a href={url} target="_blank">
                  {url}
                </a>
              </li>
            ))}
          </ul>

          <p>
            <strong>更新:</strong> {result.updated.length}件
          </p>
          <ul>
            {result.updated.map((url) => (
              <li key={url}>
                <a href={url} target="_blank">
                  {url}
                </a>
              </li>
            ))}
          </ul>

          <p>
            <strong>変更なし:</strong> {result.unchanged.length}件
          </p>
          <ul>
            {result.unchanged.map((url) => (
              <li key={url}>
                <a href={url} target="_blank">
                  {url}
                </a>
              </li>
            ))}
          </ul>
        </div>
      )}

      {posts.length === 0 && <p>記事が見つかりませんでした。</p>}

      {posts.map((post) => (
        <div
          key={post.id}
          style={{
            marginBottom: "1.5rem",
            padding: "1rem",
            border: "1px solid #ddd",
            borderRadius: "6px",
            backgroundColor: "#fafafa",
          }}
        >
          <h2 style={{ margin: 0 }}>
            <a
              href={post.source_url}
              target="_blank"
              style={{ textDecoration: "none", color: "#0070f3" }}
            >
              {post.title}
            </a>
          </h2>
          <p style={{ fontSize: "0.85rem", color: "#666" }}>
            {new Date(post.published_at).toLocaleDateString()}
          </p>
        </div>
      ))}

      <div
        style={{
          marginTop: "2rem",
          display: "flex",
          flexWrap: "wrap",
          columnGap: "calc(10% / 9)",
          rowGap: "2px",
        }}
      >
        {Array.from({ length: totalPages }, (_, i) => (
          <button
            key={i}
            onClick={() => setPage(i + 1)}
            style={{
              padding: "0.5rem 1rem",
              backgroundColor: page === i + 1 ? "#333" : "#eee",
              color: page === i + 1 ? "#fff" : "#000",
              border: "none",
              borderRadius: "4px",
              cursor: "pointer",
              minWidth: "9%",
            }}
          >
            {i + 1}
          </button>
        ))}
      </div>
    </main>
  );
}

人気記事ランキング
話題のキーワードから探す