supabaseの概要と各機能を軽くまとめ
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ワークフローを処理し、データベーススキーマから直接型を生成することができる
- Supabase をローカルで実行します。supabase start
- データベースの移行を管理します。supabase migration
- 本番環境にリリースするための CI/CD:supabase db push
- Supabase プロジェクトを管理します。supabase projects
- データベース スキーマから型を直接生成します。supabase gen types
Database
データベースはPostgresを使用している
Table View
スプレッドシートみたいなviewが使える
SQLエディタもあるので、結構使いやすそうではある
CSV,スプレッドシートからのインポートも可能
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() を実行する際に、それに付随するフィルタが提供されない場合に失敗することが保証
拡張機能
ダッシュボードからぽちぽちするだけで追加できる
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