対象: 前の記事でRealtimeの三目並べを動かして、「これを仕事の画面にするなら、どうなるの?」が気になった方
所要時間: 40〜60分 / 使うもの: ブラウザ(タブを複数)、ターミナル、Supabaseのプロジェクト(Freeプランで可)
確認日: 2026年10月(画面や料金は変わることがあります)
前の記事(手順編): https://qiita.com/tomozilla/items/6656954cf2bbd52e7a57
ビジネス編(やさしい解説・note): https://note.com/tomozilla/n/n79d740dc996d
サンプルコード一式(GitHub): https://github.com/tomozilla/realtime-tictactoe-demo
記事の中のコードは日本語コメントです。リポジトリの本体は英語コメントで、日本語コメント版(この記事と同じもの)はja/フォルダーにあります。
前の記事の三目並べでは、途中から入った人の盤面が空っぽになって、「ああ、Broadcastは過去を覚えていないんだ」と学びました。そのときは、「状態を教えて」と放送する工夫で乗り切りました。
でも、仕事の画面を想像すると、もっとしっかり考えたくなりました。飲食店の厨房の注文ボード、サポート窓口のチケット管理、倉庫の出荷ボード。複数の人が同時に開いて、誰かが動かしたら、全員の画面がすぐ変わる。しかも、「いま誰が見ているのか」「誰がそのカードを触っているのか」が分かると、二重作業が減りそうです。
そこで、ライブ注文ボードをつくってみました。
0. 先に伝えておきたいこと(正直ベース)
- デモ用の最小構成です。 ログインなしで、誰でも注文を読める・追加できる・更新できる設定にしています(後ろの「セキュリティ」を読んでください)。
- 前の記事の三目並べとは、考え方を変えました。「いまの状態」はデータベースに置いて、Realtimeは「変わったよ」の通知と「誰がいるか」だけを運びます。
- 費用:Freeプランは、Realtimeのメッセージ数が月200万、同時接続が200まで(ドキュメントの料金表より)。このボードは、3タブで試しただけなので、十分でした。数え方の細かい規則(1つの変更を、見ている人数ぶん数えるか、など)は、私は確かめていません。 もし「1つの変更が、見ている人の数だけメッセージとして数えられる」なら、10人が見ているボードで1日500回の変更があると、月に約15万メッセージです(これは、その仮定のもとでの計算です)。料金ページで確認してください。
- 未検証:認証つきの非公開チャンネル、多人数(私は3タブまで)、スマホのブラウザ、別のブラウザ(Macのchromeだけで確認しました)、2人が同時に同じカードを動かしたときの扱い。
1. つくったもの:ライブ注文ボード
3つの列(受付 / 調理中 / 完了)に、注文のカードが並びます。ボタンで、次の列に動かします。
画面の右上に、いま見ている人(A、B)が出ます。Bobがカードを選ぶと、Aliceの画面のそのカードに、**「Bobが見ています」**という札が付きました。
Bobが「調理中へ →」を押すと、Aliceの画面で、自動でカードが動きます。
「Bobさんが更新・5秒前」と、右下の「いま起きたこと」に、誰が何をしたかが出ます。
反映までの時間
BobのクリックからAliceの画面が変わるまでを、同じパソコンの2つのタブで、5回測りました。369〜745ミリ秒でした(データベースへの書き込みの時間を含みます。別の端末・別のネットワークでは未測定です)。
2. 前の記事との違い:「状態」はどこに置く?
| 前の記事(三目並べ) | 今回(注文ボード) | |
|---|---|---|
| 「いまの状態」を持つのは | 各タブの画面(持っているだけ) | データベース |
| Realtimeの役割 | 手を伝える(Broadcast) | 変更を知らせる(Postgres Changes)+ 誰がいるか(Presence) |
| 途中から入った人 | 盤面が空 → 「状態を教えて」で解決 | 入室時にデータベースを読むだけ |
途中から参加しても、全部見える
Carolが、あとから参加しました。
入った瞬間に、6件の注文が、いまの状態で並びました。 三目並べのときの「空っぽ」は、起きません。入室したときに、データベースから全部読んでいるからです。
3. つくり方
データベース
create table if not exists public.orders (
id bigint primary key generated always as identity,
customer text not null,
item text not null,
status text not null default '受付' check (status in ('受付', '調理中', '完了')),
updated_by text,
updated_at timestamptz not null default now()
);
alter table public.orders enable row level security;
-- ★ デモ用:ログインなし(anon)で、読む・追加する・更新するのを許可
create policy "demo: anyone can read orders" on public.orders for select to anon, authenticated using (true);
create policy "demo: anyone can add orders" on public.orders for insert to anon, authenticated with check (true);
create policy "demo: anyone can update orders" on public.orders for update to anon, authenticated using (true) with check (true);
-- 変更をRealtimeで配信する対象にする
alter publication supabase_realtime add table public.orders;
最後の1行が、Realtimeで「変更の通知」を受け取るための設定です。
画面
supabase-js で、1つのチャンネルに、変更の通知と**Presence(誰がいるか)**の両方をつなぎます。
channel = supabase.channel('orders-board', { config: { presence: { key: myKey } } })
.on('postgres_changes', { event: '*', schema: 'public', table: 'orders' }, (p) => {
orders.set(p.new.id, p.new); render() // 変更が届いたら、手元の状態を更新
})
.on('presence', { event: 'sync' }, () => {
people = Object.values(channel.presenceState()).flat() // いまいる人の名簿
render()
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ name: me, focus: null }) // 自分を名簿に載せる
await loadAll() // 入室時に、データベースから全件を読む
}
})
「誰がどのカードを見ているか」は、Presenceに、focus: 注文のID を載せるだけです。カードを選んだら channel.track({ name: me, focus: id }) を呼び、他の人の画面では、focus が一致するカードに「◯◯が見ています」を出します。
動かすには、前の記事と同じ要領です。
cd app
cp config.example.js config.js # URL と publishable キーを入れる
python3 -m http.server 8765 --bind 127.0.0.1
# http://127.0.0.1:8765/index.html?name=Alice / ...?name=Bob のように、タブごとに名前を変える
4. つまずいたこと
① 裏にしていたタブが、いつの間にか切れていた(一番大きなつまずき)
3つのタブを開いて試していたとき、気づきました。Carolの画面には、注文が5件、「いま見ている人」はAとCの2人。ところが、Bobの画面は、注文が4件のままでした。Aliceが追加した注文が、届いていません。
Bobの画面の左上を見ると、「切れました」。そして、そのまま戻りませんでした。画面には、何も警告が出ていませんでした。
つまり、接続が切れたことに、使っている本人が気づけない状態でした。しかも、画面はふだんどおりに見えるので、古い注文を見て仕事をしてしまうかもしれません。
原因は、私は特定できていません。別のタブを前面にして操作している間に、裏のタブが切れたようです(ブラウザは裏のタブのタイマーを間引くことがあるので、それが関係しているのでは、という私の推測です。確かめてはいません)。ノートパソコンのスリープや、Wi-Fiの切り替えでも、似たことが起きそうです。
直し方は、3つです。
- 切れたら、画面を灰色にして、「古い情報かもしれません」と出す
- 自動でつなぎ直す(3秒後。裏のタブから戻ってきたときは、すぐ)
- つながり直したら、データベースから全件を読み直す(切れている間の変更を取りこぼさないため)
.subscribe(async (status) => {
if (mine !== channel) return // 古いチャンネルの通知は無視
if (status === 'SUBSCRIBED') { setConn(true); await mine.track(...); await loadAll() }
else if (['CLOSED', 'CHANNEL_ERROR', 'TIMED_OUT'].includes(status)) { setConn(false); scheduleReconnect() }
})
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && !connected) scheduleReconnect(200) })
切断を試すために、?debug=1 を付けたときだけ、window.__rt.disconnect() でわざと切れるようにしました。Bobの画面を切って、その間にAliceが注文を追加すると、こうなりました。
数秒後、Bobの画面は自動でつながり直して、切れている間にAliceが追加した注文も含めて、6件に追いつきました。「いま見ている人」にも、Bobが戻りました。
② 「つなぎ直している間」に、灰色が消えていた
最初の直し方では、つなぎ直しを始めた瞬間に、灰色が消えて「接続中…」になっていました。中身はまだ古いままなのに、普通の見た目です。「一度つながったあとで切れたなら、つながり直すまでは、古い情報として扱う」に直しました。
③ Presenceのキーに、ランダムな文字を足した
Presenceは「キー」ごとに状態を持ちます。キーを名前だけにすると、同じ人が2つのタブを開いたときに、片方の状態が上書きされそうだったので、最初から名前にランダムな文字を足しました(名前だけのキーで実際に困るかどうかは、試していません)。
④ 反映の確認は、実際に別のタブで見る
ボタンを押した本人の画面は、データベースの返事ですぐ変わります。他の人の画面が変わるかどうかは、別のタブで見ないと分かりません。今回は、Alice・Bob・Carolの3タブで確かめました。
5. セキュリティ(デモ用の構成です)
- 誰でも、全注文を読めて、追加できて、更新できます。 「更新できる」には、どの列でも(金額や状態など)含まれます。本物のアプリでは、ログインした人だけにして、更新できる列や行を絞ってください(私は試していません)。
- 変更の通知が、ポリシーの対象になるかどうか(読む権限がない行の変更は、届かないはずですが)は、私は確かめていません。
- 「誰が更新したか」(
updated_by)は、画面が送る名前です。なりすませます。 監査用の記録には使えません。
6. 実際の運用画面にするなら
どれも、私は試していません。
- ログインと権限:店長だけが「完了」にできる、など
- 同時に同じカードを動かしたとき:今は、あとから届いたほうが勝ちです。更新時刻で、古い画面からの更新を弾く方法が使えそうです
- 音や通知:新しい注文が入ったら、音を鳴らす
- 履歴:いつ誰が動かしたかを、別の表に残す
- 多人数・長時間:画面を開きっぱなしにして、つなぎ直しがうまく働き続けるか
まとめ
- 仕事の画面にするときは、「いまの状態」はデータベース、「変わったよ」と「だれがいる」はRealtime、と役割を分けると安心だった
- 入室時にデータベースを読むので、途中から入っても、全部見える
- Presenceに「どのカードを見ているか」を載せると、二重作業を防ぐ札を出せる
- 反映までは、私の測定で約0.4〜0.7秒(同じパソコン、5回)
- 接続は、いつの間にか切れる。 切れたことに気づける画面と、つなぎ直して読み直すしくみが要る
- 誰でも更新できる設定は、デモ限定。本番では、ログインと更新の範囲の制限を
コードは、上のGitHubにあります。次は、画像変換を使った「お知らせCMS」です。
スクリーンショットは、記事用に作った無料プランのデモ環境で撮影したものです。名前(Alice・Bob・Carol)と注文は、すべて架空のものです。




