Open13

Supabase

0xkohe0xkohe

Multi Tenant

https://github.com/supabase-community/supabase-custom-claims?tab=readme-ov-file

https://www.reddit.com/r/Supabase/comments/165kbqs/is_supabase_capable_of_multi_tenancy/

Decision for Tenant's Authrization

---------------------------------------------------
-- テナントメンバーシップチェック関数
---------------------------------------------------
CREATE OR REPLACE FUNCTION check_tenant_membership(tenant_id_param INTEGER, user_id_param UUID)
RETURNS BOOLEAN AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1 
    FROM tenant_members
    WHERE tenant_id = tenant_id_param
      AND user_id = user_id_param
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TABLE tenant_members (
  tenant_id INTEGER NOT NULL REFERENCES tenants(id),
  user_id   uuid NOT NULL REFERENCES users(id),
  role      staff_role NOT NULL DEFAULT 'staff',
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (user_id, tenant_id)
);


ex:

CREATE POLICY "Tenant users can create visits."
  ON visits FOR INSERT
  TO authenticated
  WITH CHECK (
    check_tenant_membership(visits.tenant_id, (select auth.uid()))
  );

CREATE POLICY "Tenant users can update visits."
  ON visits FOR UPDATE
  TO authenticated
  USING (
    check_tenant_membership(visits.tenant_id, (select auth.uid()))
  )
  WITH CHECK (
    check_tenant_membership(visits.tenant_id, (select auth.uid()))
  );

0xkohe0xkohe

RLS

https://push.co.jp/articles/supabase-row-level-security-rls

https://github.com/orgs/supabase/discussions/811

ViewにもRLSを継承

Viewを作成し、
security_invokerを使う。

https://stackoverflow.com/questions/33858030/why-isnt-row-level-security-enabled-for-postgres-views

https://www.postgresql.org/docs/current/sql-createview.html

Viewの活用

usersテーブルからpublicになって良い情報だけを取得する。


CREATE VIEW view_tenant_users AS
SELECT 
    tu.tenant_id,
    u.id AS user_id,
    u.name,
    u.created_at,
    u.updated_at,
    tu.memo
FROM tenant_users tu
JOIN users u ON tu.user_id = u.id
WHERE u.id = auth.uid();

https://supabase.com/blog/postgresql-views

0xkohe0xkohe

About UserTable

冗長管理にはなる。
https://supabase.com/docs/guides/auth/managing-user-data

https://supabase.com/docs/guides/auth/users

Insertだけでは更新対応できないので、こんな感じで

create function public.handle_update_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
  update public.users
  set email = new.email
  where id = new.id;
  return new;
end;
$$;

create trigger on_auth_user_updated
  after update on auth.users
  for each row execute procedure public.handle_update_user();

0xkohe0xkohe

Utilities

DO $$
DECLARE
    r record;
BEGIN
    FOR r IN
        SELECT t.typname AS enum_name
        FROM pg_type t
        JOIN pg_namespace n ON t.typnamespace = n.oid
        WHERE t.typtype = 'e'
          AND n.nspname = 'public'
    LOOP
        EXECUTE 'DROP TYPE public.' || quote_ident(r.enum_name) || ' CASCADE';
    END LOOP;
END$$;


DO $$ 
DECLARE 
    r RECORD;
BEGIN 
    FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP 
        EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
    END LOOP; 
END $$;


0xkohe0xkohe

Quesitoins

  • Foreign Keyを貼る際に自身で作ったpublic.usersのidを参照として貼るべきなのかauth.usersのidを参照で貼るべきなのか。(public.usersのidがauth.usersのidを参照しているから、同じっちゃ同じではあるが…)
    • 2025/03/23 同じではない。usersテーブルの方を参照としてpublicの方は作り上げたようが良い
    • 特に、統計分析や退会したあとも。残す必要があるデータが存在しているケース。(統計データなど)
  • TenantID情報をcustom claimに入れるのか、DBに入れちゃって、policyで設定しちゃうのかどっちが良いんだろう。
-- Create the tasks table
CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    account_id uuid NOT NULL REFERENCES basejump.accounts(id),
    title TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- enable RLS on tasks table
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

-- allow team members to view tasks
CREATE POLICY "Only members can view tasks" on tasks
    FOR SELECT
    TO authenticated
    USING (
        account_id IN (SELECT basejump.get_accounts_for_current_user())
    );


  • RLSにって閲覧できないレコードにする更新 例: uuidを別の値に変える。をした際のcons
0xkohe0xkohe

Edge Functions

Cors problem

https://zenn.dev/k_kind/articles/supabase-edge-functions-cors

https://supabase.com/docs/guides/functions/cors

Transaction

https://supabase.com/docs/guides/functions/connect-to-postgres
supabase client libでは貼れない。
edge functionにて、通常のclientで処理すればOK

Database Webhook

https://supabase.com/docs/guides/database/webhooks

Best Practices

https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv

Otehrs

Edge FunctionsはおそらくCloudflareで動いている

   "response": [
      {
        "headers": [
          {
            "content_length": "56",
            "content_type": "application/json",
            "date": "Sun, 09 Mar 2025 06:27:30 GMT",
            "server": "cloudflare",
            "vary": "Accept-Encoding",
            "x_sb_compute_multiplier": null,
            "x_sb_edge_region": "ap-northeast-1",
            "x_sb_resource_multiplier": null,
            "x_served_by": "supabase-edge-runtime"
          }
0xkohe0xkohe

Query

Relationで取得する際に、Relation先の情報でfilterする際に書き方
https://stackoverflow.com/questions/72942304/supabase-filter-rows-based-on-if-any-of-their-one-to-many-relations-has-propert

      let query = supabase
        .from('visits')
        .select(`
          *,
          view_tenant_users!inner(name)
        `)
        .eq('tenant_id', tenantId);

      if (filters.name) {
        query = query.filter('view_tenant_users.name', 'ilike', `%${filters.name}%`);
      } 

Supabase join skipping junction table

無理そう、Viewを作るのが良さそう。
https://github.com/orgs/supabase/discussions/2990#discussioncomment-1249797

Max rows

Paginationのcountのためには

const { data, count } = supabase
  .from('countries')
  .select('*', { count: 'exact' })

を使えばこれを回避できる。

Quesion

ViewのSelectは影響を受ける??

Aggregate Functions

https://supabase.com/blog/postgrest-aggregate-functions

ALTER ROLE authenticator SET pgrst.db_aggregates_enabled = 'true';
NOTIFY pgrst, 'reload config';
0xkohe0xkohe

Storage

https://supabase.com/docs/guides/storage/security/access-control

Tenant制御

ex:

((bucket_id = 'shop'::text) AND EXISTS (
SELECT 1
FROM tenant_members
WHERE tenant_id = ((storage.foldername(name))[1])::integer
AND user_id = auth.uid()
))

失敗記録、StorageのPolicyからfunctionは呼び出せない

functionを使う場合、SQL Editorから実行する必要あり。
https://github.com/supabase/supabase/issues/12357

ex: テナント用

((bucket_id = 'shop'::text) AND 
check_tenant_membership(
(
(storage.foldername(name))[1])::integer,
 ( SELECT auth.uid() AS uid))
)

0xkohe0xkohe

local Env

https://supabase.com/docs/guides/local-development/overview
https://github.com/supabase/cli/issues/2456#issuecomment-2227472376
on Ubuntu

ローカル開発環境が壊れたと思ったとき。

rm -rf supabsae/migrations
rm -rf supabsae/schemas

# 下記のようなものをすべて実行
supabase migration repair --status reverted 20250409123154
supabase migration repair --status reverted 20250410082044
supabase migration repair --status reverted 20250419115236
supabase migration repair --status reverted 20250419115605
supabase migration repair --status reverted 20250419142729
supabase migration repair --status reverted 20250424005616

supabase db pull
supabase db pull --schema auth,storage

0xkohe0xkohe
CREATE OR REPLACE FUNCTION public.check_tenant_role(
  tenant_id_param  INTEGER,
  user_id_param    UUID,
  required_roles   public.staff_role[]
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''               -- ← ★ここで固定
AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1
    FROM public.tenant_members      -- ← ★スキーマを明示
    WHERE tenant_id = tenant_id_param
      AND user_id  = user_id_param
      AND role     = ANY(required_roles)
  );
END;
$$;


SELECT check_tenant_role(1, '00000000-0000-0000-0000-000000000000', ARRAY['admin', 'manager']::public.staff_role[]);

0xkohe0xkohe

migrationファイルは冪等に作っておいて

supabase migration repair 20250621103402 --status reverted --linked

みたいな感じで、履歴を消して再実行という運用がやりやすそう