データベース

Supabase Realtimeの実装|3方式の使い分けとRLS併用・同時接続の上限

Supabase Realtimeの実装|3方式の使い分けとRLS併用・同時接続の上限

Supabase Realtime には Postgres Changes・Broadcast・Presence の3方式があり、どれも supabase.channel() から始まる同じ書き方に見えます。ところが通る経路も、認可の効き方も、上限にぶつかる条件も別物です。テーブル変更をそのまま流す実装で組んだあと、購読者が増えた段階で配信が詰まる、という順序で問題が出ます。この記事では、3方式それぞれの現行チャネル API による実装手順、Realtime Authorization と行レベルセキュリティを併用したときに認可がどこで効くか、プラン別の同時接続数とメッセージ流量の上限、そして方式を切り替える判断の境界までを、2026年8月時点の公式ドキュメントに沿って実装順に並べます。

まとめ:3方式の選び方と実装前に決めておく3点

先に結論を出します。恒久的なデータ更新を少数の購読者へ届けるだけなら Postgres Changes で足りますが、同じ変更を数千クライアントが同時に見る画面では Broadcast へ寄せてください。カーソル位置やタイピング表示のような毎秒10回を超える一時的なイベントは最初から Broadcast、在席表示は Presence が担当します。

実装前に決めておくのは3点です。第一に配信元をどこに置くか。クライアント発信で足りるのか、データベースのトリガーから realtime.broadcast_changes で流すのか。第二に認可の置き場所で、private channel を使うなら realtime.messages 側にポリシーが要ります。第三に同時接続数の見込みで、Free は同時接続200・メッセージ毎秒100が上限のため、規模の想定を先に置かないとプラン選択をやり直すことになります。

Postgres・Broadcast・Presenceの3方式が通る経路の違い

3方式は同じ WebSocket 接続に相乗りしますが、メッセージの出どころが違います。ここを取り違えたまま実装すると、上限の読み方まで狂います。

Postgres ChangesがWAL経由でDB変更を配信する経路

Postgres Changes は、PostgreSQL の論理レプリケーションを起点にします。テーブルへの書き込みが WAL に載り、Realtime サーバーがそれを読み取って、購読しているクライアントへ配信する流れです。アプリケーション側は何も送信しません。データベースが唯一の真実であり続けるため、書き込みの取りこぼしが起きにくい構造になります。

その代わり、公式ドキュメントは「スループットは書き込み量ではなく購読者数に比例して増える」と明記しています。変更の順序を保つために単一スレッドで処理される設計なので、1件の更新に対して購読者が1,000人いれば1,000回分の配信処理が走る計算です。Supabase 全体の構成や Firebase との設計思想の差はSupabaseとFirebaseの違いを整理した比較記事で扱っています。

Broadcastが持つクライアント発信とDBトリガー発信の2経路

Broadcast はトピック名を共有するクライアント同士がメッセージを送り合う仕組みで、データベースを経由しません。送信元は2つあります。ひとつはブラウザやアプリからの channel.send()、もうひとつはデータベース側の realtime.send 関数です。後者を AFTER トリガーから呼べば、テーブル変更をきっかけにした配信を Broadcast の経路で実現できます。同じトリガーから関数側の処理を走らせる経路は、Supabase Edge Functionsの呼び出し経路で扱っています。

公式ガイドがデータベース変更の通知手段として Broadcast from Database を推す理由は、購読者数に対する挙動にあります。Postgres Changes が購読者ごとに認可を評価するのに対し、Broadcast はトピック単位で配る設計なので、同じ変更を大量のクライアントへ届ける場面で処理量が伸びにくくなります。

Presenceが接続状態を同期する仕組みとtrack頻度の上限

Presence は「誰が今このチャネルにいるか」を各クライアントが持ち寄って同期する機能です。状態は競合を自動で解決するデータ構造で保持され、同時に発生した参加と離脱を破綻させずに畳み込みます。接続が切れた分は自動的に消えるため、離脱の通知をアプリ側で作り込む必要はありません。

注意点は更新頻度です。公式ドキュメントはマウス移動のたびに track() を呼ぶような使い方を明確に戒めており、チャネルが溢れて性能問題を招くと警告しています。高頻度の位置情報は Presence ではなく Broadcast に載せる、という切り分けが前提になります。

現行チャネルAPIによる実装手順と旧v1 APIからの主な変更点

ネット上のサンプルには、テーブルから直接購読する v1 時代の書き方が残っています。現行は channel() を起点にした統一 API で、購読対象はイベント名と設定オブジェクトで指定する形に変わりました。

postgres_changesの購読とpublication登録の実装手順

まずテーブルを配信対象の publication に加えます。ダッシュボードの Publications 画面から切り替えるか、SQL で追加してください。

alter publication supabase_realtime add table public.orders;

クライアント側は次のように書きます。event には INSERT・UPDATE・DELETE のいずれか、あるいはワイルドカードを渡します。

const channel = supabase
  .channel('db-orders')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'orders' },
    (payload) => console.log(payload)
  )
  .subscribe()

絞り込みには filter、返す列の限定には select を指定できます。配信量はそのままコストと遅延に効いてくるので、購読の時点で範囲を狭めておくほうが後の調整が楽になります。

replica identity fullを設定してold値を受け取る手順

既定では更新後の行だけが届き、変更前の値は空です。差分表示や監査の用途で旧値が要るなら、テーブル側の設定を変えます。

alter table public.orders replica identity full;

この設定は WAL に載る情報量を増やすため、更新が多いテーブルでは書き込み側の負荷と保持サイズが増えます。全テーブルに一律で入れるのではなく、旧値が要る画面のあるテーブルだけに絞ってください。なお1メッセージあたりの上限は1,024KBで、これを超えると新旧レコードは値サイズが64バイト以下の項目だけに切り詰められます。

Broadcastの送受信とbroadcast_changesによるDB連携

クライアント間で送る場合は、同じトピック名のチャネルを開いて send するだけです。データベース起点にするなら、トリガー関数から realtime.broadcast_changes を呼びます。

create or replace function public.orders_changed()
returns trigger
security definer set search_path = ''
as $$
begin
  perform realtime.broadcast_changes(
    'orders:' || new.id::text,
    tg_op, tg_op, tg_table_name, tg_table_schema, new, old
  );
  return null;
end;
$$ language plpgsql;

受信側は postgres_changes ではなく broadcast イベントとして受け取ります。イベント名にはトリガー操作がそのまま入るため、INSERT と UPDATE は別ハンドラへの分割が可能です。キューを挟んで非同期処理につなぐ構成にするなら、PostgreSQLをキュー化するpgmqの使い方と組み合わせる手もあります。

Presenceのtrackとsyncイベントで在席状態を扱う手順

チャネル設定で識別キーを決め、購読が成立してから track() を呼びます。状態の取得は presenceState() です。

const room = supabase.channel('room-42', {
  config: { presence: { key: userId } },
})
room
  .on('presence', { event: 'sync' }, () => {
    console.log(room.presenceState())
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') await room.track({ editing: false })
  })

sync のほかに join と leave が届きます。ただし実際の出入りがなくても、サーバーとクライアントの状態突き合わせで join と leave が発生することがあります。差分イベントを起点に副作用を書くのではなく、sync のたびに全体を描き直す実装のほうが破綻しません。

Realtime AuthorizationとRLSを併用するときの認可の効き方

3方式は認可の掛かる場所が違います。同じ「行レベルセキュリティを書けば守れる」という理解で全部を扱うと、片方だけ穴が残ります。

Postgres Changesが購読者ごとに認可を評価する挙動

Postgres Changes は、公式表現を借りれば購読者ごとにイベントを認可します。テーブルに設定した行レベルセキュリティが配信の可否を決めるため、他人の行が混ざって届くことはありません。ポリシーの書き方そのものはSupabaseのRLS設定手順とAuth連携ポリシー設計にまとめてあるので、そちらを先に済ませてください。public 以外のスキーマにテーブルを置く場合は、対象ロールへの読み取り権限の付与も別途必要です。

private channelとrealtime.messagesへのポリシー設計

Broadcast と Presence には、購読対象のテーブルという概念がありません。代わりに認可の対象になるのが realtime.messages というシステム側のテーブルです。private channel を使うと、このテーブルへのポリシーがチャネルの入室可否と受信可否を決めます。

create policy "authenticated can receive broadcasts"
on realtime.messages
for select
to authenticated
using ( true );

実務では条件を真のままにせず、トピック名を参照して所属テナントや部屋の会員かどうかを判定する式に置き換えます。ここを書かずに private を有効化すると、購読が成立せず無言で何も届かない状態になるため、動かないときの確認先として覚えておいてください。

setAuthによるトークン更新とDELETE時に生じる認可の穴

private channel の購読前には supabase.realtime.setAuth() を呼び、Realtime 側へ現在のアクセストークンを渡す必要があります。トークンには期限があるので、更新イベントを拾って再度渡す処理を入れておかないと、長時間開きっぱなしの画面で配信が止まります。

もうひとつ、公式ドキュメントが注意書きとして置いているのが DELETE の扱いです。削除済みレコードへのアクセス可否を PostgreSQL 側で検証できないため、DELETE 文にはポリシーが適用されません。削除イベントの payload に秘匿すべき値を載せない設計にするか、削除そのものを論理削除の UPDATE に置き換える判断が要ります。

プラン別の同時接続数・メッセージ流量の上限とコストの見積り方

Realtime は接続数と流量の両方に上限があります。設計の初期に数字を置いておくと、後からプランを組み替える手戻りを避けられます。

FreeとProプランで変わる同時接続数とメッセージ毎秒の上限

プラン 同時接続 メッセージ毎秒 チャネル参加毎秒
Free 200 100 100
Pro 500 500 500
Pro(上限解除時) 10,000 2,500 2,500
Team 10,000 2,500 2,500

Enterprise はこれ以上の枠を個別に設定できます。見落としやすいのは1接続あたりのチャネル数で、こちらは全プラン共通の100が既定です。画面ごとにチャネルを開く設計だと、タブを多数開くユーザーで先にこちらへ当たります。

Broadcastのペイロード上限と64バイト切り詰めが起きる境界

Broadcast の1メッセージあたりのペイロード上限は Free が256KB、上位プランでは3,000KB超まで広がります。Postgres Changes 側は前述のとおり1,024KBが境界で、これを超えると新旧レコードが64バイト以下の項目だけに縮む仕様です。大きな本文や画像メタデータをそのまま流す設計にすると、この切り詰めに気づかないまま一部の項目が欠けた状態で運用が始まります。識別子だけを配信し、本体は受信後に取りに行く形へ寄せてください。

実装前に決める3方式の採用条件とSupabase Realtimeを見送る場面

ここからは判断を言い切ります。3方式は優劣ではなく適用範囲の問題で、境界は購読者数と更新頻度で決まります。

Postgres Changesを選んでよい規模と3000購読者の境界

Postgres Changes を採用してよいのは、同じ変更を見る同時購読者が概ね3,000未満で、かつ更新が秒単位より粗い場合です。公式ドキュメントも、同一の変更に対して約3,000を超える同時購読者が見込まれるなら Broadcast へ切り替えるよう案内しています。注文ステータスの反映や通知の配信のように、データの取りこぼしが許されず遅延は数百ミリ秒まで許容できる用途が該当します。

逆に、管理画面で全社の更新を全員が眺めるような「1つの変更を全員が購読する」画面は、利用者が増えた瞬間に上限へ当たります。ここは規模が小さいうちから Broadcast from Database で組んでおくほうが、後の書き換えが要りません。

Broadcastへ寄せるべき高頻度イベントの判定条件と移行手順

1ユーザーあたり毎秒10イベントを超える一時的なデータは、無条件で Broadcast です。カーソル位置、入力中の表示、ゲームの状態、描画ストロークが代表例で、これらはデータベースに残す価値もありません。逆に永続化が要るデータでも、購読者数が前項の境界を超えるならトリガー経由の Broadcast に移します。

移行の手順は、テーブルを publication から外し、AFTER トリガーで realtime.broadcast_changes を呼ぶ関数を足し、クライアントの購読を postgres_changes から broadcast へ差し替える3段です。トピック名の設計だけは先に決めてください。行 ID 単位にするか、テナント単位にするかで、後の認可ポリシーの書きやすさが変わります。

Supabase Realtimeを見送って自前実装に倒す3つの条件

次の3つのいずれかに当てはまるなら、Supabase Realtime に寄せない判断をおすすめします。第一に、双方向通信の途中でサーバー側の重い処理を挟みたい場合。第二に、ブラウザ以外のプロトコルや独自のバイナリフレームを扱う場合。第三に、接続を張る主体が Supabase の認証情報を持たない外部システムである場合です。いずれも WebSocketの仕組みとNode.jsでの実装で示した自前サーバー、あるいは片方向で足りるなら Server-Sent Events のほうが素直に収まります。

逆に、これらに当てはまらず認証が Supabase Auth で完結しているなら、自前で WebSocket サーバーを立てて運用する手間に見合う利点はほとんどありません。判断に迷う構成や、既存システムとつなぎ込む前提でのリアルタイム機能の設計は、業務用・Webアプリ開発の受託でも相談を受けています。

よくある質問

Postgres ChangesとBroadcastはどちらを先に検討すべきですか?

購読者が少なく永続データを扱うなら Postgres Changes から始めて構いません。テーブルを publication に追加するだけで動くため、初期の実装量が小さく済みます。同じ変更を数千人が同時に見る画面や、毎秒10回を超える一時的なイベントが混ざるなら、最初から Broadcast を選んでください。

v1時代のサンプルコードはそのまま動きますか?

現行のクライアントでは推奨されません。購読は channel() を起点にし、イベント名 postgres_changes と設定オブジェクトを渡す形で書きます。ネット上の記事は v1 時代の記法が残っているものが多いため、コードを流用する前に channel を使っているかどうかを確認してください。

Broadcastでも行レベルセキュリティは効きますか?

テーブルのポリシーは効きません。Broadcast の認可対象は realtime.messages で、private channel を有効にしたうえでこのテーブルへポリシーを書く形になります。public のままにすると、トピック名を知っている相手は誰でも受信できる状態になります。

Realtimeの同時接続数はどこまで無料で使えますか?

2026年8月時点の公式クォータでは、Free プランが同時接続200・メッセージ毎秒100・チャネル参加毎秒100です。Pro はいずれも500で、支出上限を解除すると同時接続10,000・毎秒2,500まで伸びます。1接続あたりのチャネル数は全プラン100が既定値になります。

DELETEイベントで旧レコードの値が空になるのはなぜですか?

テーブルの replica identity が既定のままだと、WAL に主キー以外の値が載らないためです。全列を載せる設定に変えれば旧値が届きます。ただし削除イベントにはポリシーが適用されない仕様なので、秘匿値を載せる設計にはしないでください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  3. 2026.07.15 コラム LINEミニアプリとは?未認証と認証済みの違い・LIFFとの使い分け・審査と開発費用【2026年版】
  4. 2026.07.23 コラム クラウド販売管理システムとは?オンプレとの違い・料金相場と5年総額の比べ方【2026年】
  5. 2026.01.20 テックブログ PineDramaとは?TikTokのショートドラマアプリの日本配信・料金・安全性を解説

RELATED POSTS 関連記事

目次