🐈

supabaseの概要と各機能を軽くまとめ

2023/03/12に公開

supabase

Firebaseの代替となるオープンソースで、プロダクトを構築するために必要なすべてのバックエンド機能を提供している
提供している機能

  • Database
  • Auth
  • Edge Functions
  • Realtime
  • Storage

Javascript Client Library

@supabase/supabase-jsというクライアントがある
Supabase-jsを使用すると、Postgresデータベースとのやり取り、データベースの変更の監視、Deno Edge関数の呼び出し、ログインおよびユーザ管理機能の構築、大きなファイルの管理などを行うことがでる

nuxtの場合は@nuxtjs/supabaseを使う。この中にクライアントが含まれているっぽい。

supabase cli

Supabase CLI は、ローカルでプロジェクトを開発し、Supabase プラットフォームにデプロイするためのツールを提供
CLIを使用してSupabaseプロジェクトを管理し、データベースマイグレーションやCI/CDワークフローを処理し、データベーススキーマから直接型を生成することができる

Database

データベースはPostgresを使用している

Table View

スプレッドシートみたいなviewが使える
SQLエディタもあるので、結構使いやすそうではある
CSV,スプレッドシートからのインポートも可能
image

Database Functions

Postgresには、SQL関数のサポートが組み込まれている。これらの関数はデータベース内に存在し、APIで使用することができます。

データベース関数を作成するためのいくつかのオプションがある。
Dashboardを使用するか、SQLを使用して直接作成することができる。

文字列 "hello world" を返す基本的なデータベース関数

create or replace function hello_world() -- 1
returns text -- 2
language sql -- 3
as $$  -- 4
  select 'hello world';  -- 5
$$; --6

関数が作成された後、その関数を「実行」する方法がいくつかある。SQLを使ってデータベース内で直接実行するか、クライアントライブラリの一つを使って実行する

sql

select hello_world();

js

const { data, error } = await supabase.rpc('hello_world')

テーブルからデータセットを返すことも可能
関数を作る

create or replace function get_planets()
returns setof planets
language sql
as $$
  select * from planets;
$$;

実行
この関数はテーブルセットを返すので、フィルタやセレクタを適用することもできる

const { data, error } = supabase.rpc('get_planets').eq('id', 1)

パラメータの受け渡し
新しいデータを挿入して新しいIDを返す

create or replace function add_planet(name text)
returns bigint
language plpgsql
as $$
declare
  new_row bigint;
begin
  insert into planets(name)
  values (add_planet.name)
  returning id into new_row;

  return new_row;
end;
$$;
const { data, error } = await supabase.rpc('add_planet', { name: 'Jakku' })

Database Webhooks

テーブルイベントが発生するたびに、データベースから他のシステムにリアルタイムでデータを送信することができる
3つのテーブルイベントにフックすることができます。INSERT、UPDATE、DELETE

全文検索

PostgresはFull Text Searchクエリを処理するための組み込み関数を持ってる
これは、Postgresの中の検索エンジン のようなもの

基本的な全文クエリ

1つのカラムを検索する
descriptionに「big」という言葉が含まれるすべての書籍を検索する。

const { data, error } = await supabase.from('books').select().textSearch('description', `'big'`)

descriptionに「little」「big」の単語が含まれるすべての書籍を検索するには、「|」記号を使用

const { data, error } = await supabase
  .from('books')
  .select()
  .textSearch('description', `'little' | 'big'`)

データベースのテスト

Supabase CLI を使用してデータベースをテストできる

サーバーレスAPI

Supabaseは、データベーススキーマから直接3種類のAPIを自動生成する

  • REST: レストフルインターフェースを介してデータベースと対話
  • Realtime: データベースの変更をリスニング
  • GraphQL: ベータ版

データベースを更新すると、APIを通じてすぐに変更にアクセスできるようになる
データベースの変更に伴って更新されるドキュメントをダッシュボードに生成する
APIはPostgreSQLのRow Level Securityで動作するように設定されており、key-authを有効にしたAPIゲートウェイの後ろにプロビジョニングされている
基本的な読み込みのベンチマークでは、Firebaseよりも300%以上高速
数千の同時リクエストに対応でき、Serverlessワークロードに適している

REST API
PostgRESTを使用したRESTfulなAPIを提供
Postgresの上にある非常に薄いAPIレイヤ
CRUD APIに必要なものはすべて提供される

  • 基本的なCRUD操作
  • 深くネストされた結合により、一度のフェッチで複数のテーブルからデータをフェッチすることが可能
  • Postgresビューとの連携
  • Postgres 関数との連携
  • 行レベルセキュリティ、ロール、およびグランツを含むPostgresセキュリティモデルで動作

GraphQL API
beta版で破壊的変更が今後入る可能性がある
SupabaseのGraphQLは、GraphQLのためのオープンソースのPostgreSQL拡張であるpg_graphqlを通して動作

Realtime API
これを使えば、データベースの変更をウェブソケットで聞くことができる
Realtimeは、PostgreSQLの組み込みの論理レプリケーションを利用します。PostgreSQLのパブリケーションを管理するだけで、リアルタイムAPIを管理することができる

APIルートは、Postgresのテーブル、ビュー、またはファンクションを作成する際に自動的に作成される
試しにtodosテーブルを作成

create table todos (
  id bigint generated by default as identity primary key,
  task text check (char_length(task) > 3)
);

Supabaseプロジェクトは、固有のAPI URLを持っている。APIはAPIゲートウェイで保護されており、リクエストごとにAPIキーが必要
ダッシュボードにドキュメントを生成し、データベースを変更するたびに更新される

// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// Make a request
const { data: todos, error } = await supabase.from('todos').select('*')

Realtime API
デフォルトでは無効

alter publication supabase_realtime add table todos;
// Initialize the JS client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// Create a function to handle inserts
const handleInserts = (payload) => {
  console.log('Change received!', payload)
}

// Listen to inserts
const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe()

セキュリティ
APIは、Postgres Row Level Security (RLS)と共に動作するように設計されている。Supabase Authを使用すると、ログインしたユーザに基づいてデータを制限することができる。データへのアクセスを制御するには、Policiesを使用。Postgresでテーブルを作成する場合、行レベルセキュリティはデフォルトで無効。

alter table todos enable row level security;

service_role key は見えるところにおいてはいけない
使用例はバックエンドでデータ解析ジョブを実行することとか

GitHubと連携していて、もしservice_role keyがpushされたら自動でスキャンされてAPIキーがsupabaseに転送されて自動で無効化される

間違えて削除とか更新しないようにされている
API からのすべてのクエリに対して Postgres の拡張モジュール safeupdate がデフォルトで有効
delete() や update() を実行する際に、それに付随するフィルタが提供されない場合に失敗することが保証

拡張機能

ダッシュボードからぽちぽちするだけで追加できる
https://supabase.com/docs/videos/toggle-extensions.mp4

Auth

Supabase Authは、他のSupabase製品と深く統合して動作するように設計されている。Postgresはすべての活動の中心であり、Authシステムもこの原則に従っている。可能な限りPostgresの組み込みの認証機能を利用している

認証:この人物の入場を許可する必要があるか?もしそうなら、彼らは誰なのか?
認可:この人が入ったら、何をすることが許されるのか?

認証

認証の種類

  • Email & password.
  • Magic links (one-click logins).
  • Social providers.
  • Phone logins.

認可

Row Level Security
細かい認証ルールが必要な場合、PostgreSQLのRow Level Security (RLS)が一番いい

PoliciesはPostgreSQLのルールエンジンです。非常に強力で柔軟性があり、独自のビジネスニーズに適合する複雑なSQLルールを書くことができる
ポリシーを作成することにより、それぞれの行に対してどのようなルールでアクセスできるのかを制御する

データベースがルールエンジンになる。クエリーを繰り返しフィルタリングするのではなく

const loggedInUserId = 'd0714948'
let { data, error } = await supabase
  .from('users')
  .select('user_id, name')
  .eq('user_id', loggedInUserId)

データベースのテーブルに auth.uid() = user_id というルールを定義するだけで、ミドルウェアからフィルタを削除しても、そのルールを通過した行をリクエストで返す

let { data, error } = await supabase.from('users').select('user_id, name')

ユーザー管理
Supabaseは、ユーザーを認証・管理するための複数のエンドポイントを提供する

ユーザーがサインアップすると、Supabaseはそのユーザーに一意のIDを割り当てる
このIDはデータベースの任意の場所で参照することができ、user_idフィールドを使用して、auth.usersテーブルのidを参照するprofilesテーブルを作成することができる

Edge Functions

Edge FunctionsはサーバーサイドのTypeScript関数で、ユーザーの近くにあるエッジでグローバルに分散されます。Webhookをリッスンしたり、SupabaseプロジェクトとStripeなどのサードパーティを連携させたりするために使用する
CLIが必要
Denoでつくられてる

関数を作成

supabase functions new hello-world

デプロイ

supabase functions deploy hello-world

このコマンドは ./functions/hello-world/index.ts にある Edge Function をバンドルし、Supabase プラットフォームにデプロイするコマンド

実行する

import { createClient } from '@supabase/supabase-js'

// Create a single supabase client for interacting with your database
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key')

const { data, error } = await supabase.functions.invoke('hello-world', {
  body: { name: 'Functions' },
})

Database Functions vs Edge Functions
データベース内で実行され、RESTやGraphQL APIを使用してリモートで呼び出すことができるデータベース関数を使用することをお勧め

低レイテンシーを必要とするユースケースには、グローバルに分散され、TypeScriptで記述可能なEdge Functionsがお勧め

小さな関数をたくさん開発するよりも、大きな関数をいくつか開発することを推奨している
Functionsを開発する際によくあるパターンとして、2つ以上のFunctionの間でコードを共有する必要がある場合がある。
アンダースコア(_)をプレフィックスとするフォルダに共有コードを格納することができる

└── supabase
    ├── functions
    │   ├── import_map.json # A top-level import map to use across functions.
    │   ├── _shared
    │   │   ├── supabaseAdmin.ts # Supabase client with SERVICE_ROLE key.
    │   │   └── supabaseClient.ts # Supabase client with ANON key.
    │   │   └── cors.ts # Reusable CORS headers.
    │   ├── function-one # Use hyphens to name functions.
    │   │   └── index.ts
    │   └── function-two
    │       └── index.ts
    ├── migrations
    └── config.toml

RealTime

Supabaseは、グローバルに分散したRealtimeサーバーのクラスタを提供し、以下の機能を実現

  • Broadcast: クライアントからクライアントへメッセージを低レイテンシーで送信
  • Presence: クライアント間の共有状態を追跡し、同期させる
  • Postgres Changes: Postgresデータベースの変更を聞き取り、許可されたクライアントに送信

Broadcast

ブロードキャストは、クライアントが一意の識別子を持つチャネルにメッセージを発行する、publish-subscribe patternに従っている

あるユーザが id room-1 のチャネルにメッセージを送信すると、他のクライアントは、id room-1 のチャンネルを購読することによって、リアルタイムでメッセージを受信することを選択できる
これらのクライアントがオンラインであり、購読していれば、メッセージを受け取ることができる

クライアントがメッセージを送信し、複数のクライアントがメッセージを受信できる

一般的なユースケースとしては、オンラインゲームにおいてユーザーのカーソル位置を他のクライアントと共有すること

Presence

メモリ内のコンフリクトフリー レプリケート データ タイプ (CRDT) を利用して、最終的に一貫した方法で共有状態を追跡および同期する
新しいクライアントがチャネルを購読すると、他のすべてのクライアントがそれぞれの状態を送信するのを待つ代わりに、チャネルの最新の状態を単一のメッセージで即座に受信する

クライアントは自由に出入りでき、全員が同じチャンネルに登録している限り、お互いに同じプレゼンス状態を持つことができる

Presence の優れた点は、クライアントが突然切断された場合(たとえばオフラインになった場合)、そのクライアントの状態が共有状態から自動的に削除されること

Postgres Changes

Postgres Changesは、Row Level Security (RLS)ポリシーに基づき、データベースの変更をリスニングし、許可されたクライアントにブロードキャストさせることができる

Storage

大容量ファイルの保存と配信を簡単に行える

ファイル、フォルダ、バケットで構成されている
ファイルは画像、GIF、動画とか
フォルダはファイルを整理する方法でプロジェクトに適した構造に保存できる
バケットはファイルとフォルダの別個のコンテナ。s3と同じ感じだと思われる。それぞれに異なるセキュリティルールを作成できる

ファイル名、フォルダ名、バケット名は、AWSオブジェクトキーの命名規則に従い、他の文字の使用は避ける

バケットの作成、ファイルのアップロード、ダウンロードなどをダッシュボード、SQL、JSでできる

// Use the JS library to create a bucket.
const { data, error } = await supabase.storage.createBucket('avatars')

const avatarFile = event.target.files[0]
const { data, error } = await supabase.storage
  .from('avatars')
  .upload('public/avatar1.png', avatarFile)

開発フロー

ローカル

nuxtだったり、nextだったりのプロジェクトは作成済み
ダッシュボードからプロジェクトを作成しておく

セットアップ

supabase init

ログインしてプロジェクトをリンクする

supabase login
supabase link --project-ref $PROJECT_ID

ローカルの変更を Git にコミットし、ローカルの開発セットアップを実行

git add .
git commit -m "init supabase"
supabase start

これでローカルの準備は完了なので開発していく
スキーマの変更はダッシュボードから行なって行く感じだと思われる

スキーマの差分作成

employeeテーブルを作成したとする

supabase db diff -f new_employee

マイグレーションファイルが作成される

適用する

supabase db reset

移行のデプロイ

GitHub Actionsでデプロイする

ステージング用と本番用のsupabseプロジェクトを作成する
GitHub Actionsも有効にしておく

CLIをCIとかで動かすには下記が必要

  • SUPABASE_ACCESS_TOKEN: 人的なアクセストークン
  • SUPABASE_DB_PASSWORD: プロジェクト固有のデータベースのパスワード

下記のci用のファイルを作成

name: CI

on:
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3

      - uses: supabase/setup-cli@v1

      - name: Start Supabase local development setup
        run: supabase start

      - name: Verify generated types are up-to-date
        run: |
          supabase gen types typescript --local > types.ts
          if [ "$(git diff --ignore-space-at-eol types.ts | wc -l)" -gt "0" ]; then
            echo "Detected uncommitted changes after build. See status below:"
            git diff
            exit 1
          fi
name: Deploy Migrations to Staging

on:
  push:
    branches:
      - develop
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-22.04

    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
      STAGING_PROJECT_ID: abcdefghijklmnopqrst

    steps:
      - uses: actions/checkout@v3

      - uses: supabase/setup-cli@v1

      - run: |
          supabase link --project-ref $STAGING_PROJECT_ID
          supabase db push

リリースジョブは、supabase/migrations ディレクトリにマージされたすべての新しい移行スクリプトを、リンクされた Supabase プロジェクトに適用します。ジョブがどのプロジェクトにリンクするかは、PROJECT_ID 環境変数で制御できる

TypeScript

CLIでschemaから型生成できる

supabase gen types typescript --linked >| schema.ts

Nuxtの場合使用する時に下記のようにすると型がつく

 import { Database } from "schema";
 const supabase = useSupabaseClient<Database>()

  const { data: todos, error } = await supabase.from('todos').select('*')
  todos?.map((t) => t.task)

適当に作ったDatabaseFunctionsだと型生成されるけど、返り値の型がunknownとかになっちゃう

Discussion