ハルスペース・ラボ
ボタンより先に境界を作った — プラットフォーム権限・グループメンバーシップ・人による承認
機能を増やす前にプラットフォーム管理とグループ参加を分離し、ガバナンスの履歴とAIの下書き・人による適用の境界を築く中で、Haru Spaceが経験した二つの失敗を記録しました。

Summary
ひと目でわかる要約
- 最高管理者のプラットフォーム管理ビューと実際のグループメンバーシップを分離し、管理権限がチャット参加やグループ内で行動する権限へ広がらないようにしました。
- グループの議案と投票を上書きしない履歴として残し、AIは制約された空間の下書きだけを提案し、人がプレビューして適用するようにしました。
- 最高管理者の議案作成条件とスキーマバージョンを見落とした失敗から、権限の意味とデータ変更の手順をコード・画面・運用で一致させる必要があると学びました。
この記事は、2026年7月15–21日のGit記録と当時の設計・運用文書をもとに、Haru Spaceが機能より先に権限の境界を立て直した一週間を振り返って書いた記録だ。
サービスを作っていると、権限はいつもボタンの姿で先に見えてくる。
誰が作れるのか。誰が修正できるのか。承認と拒否のボタンは誰に見せるべきか。画面だけを見れば、役割名に応じてボタンを隠したり表示したりすれば終わるように思える。私も初めはそう考えていた。
しかし、グループが増え、一人が複数のグループを行き来し、ペットがAIの役割を担い始めると、問いは変わった。
そのボタンを見られる人と、そのデータの主体になれる人は、本当に同じなのか。
7月の第3週、Haru Spaceには多くの機能が加わった。グループガバナンス、複数グループへの参加、最初の空間のオンボーディング、3言語UIと共通フィードバックの仕組みが続いた。同時に、機能よりも長く残る境界を立て直した時期でもあった。プラットフォームを管理する権限とグループに参加する資格、人とAIペットのアイデンティティ、下書きを作ることと実際に適用することを分離した。
その過程は滑らかではなかった。最高管理者が必要な議案を作れず、新しいカラムまで用意したのにスキーマバージョンを上げなかったため、本番ではグループ承認だけが失敗することもあった。どちらも小さな条件文や一つの数字で直せたが、原因はもっと深かった。権限と変更の意味を、システム全体で同じ言葉によって説明できていなかった。
最高管理者なら、すべてのグループのメンバーなのか
初期には「最高管理者」という役割名が、あまりにも多くの意味を抱えていた。プラットフォーム全体を点検するアカウントなので、すべてのグループを開くことができ、現在選択中のグループも持つことができた。そのためコードでは、「現在のグループがある」という事実を「そのグループのメンバーである」という意味に受け取りやすかった。
この二つは異なる。
最高管理者がプラットフォーム運用のためにグループの設定や状態を確認することは、管理ビューだ。チャットルームに入り、メッセージや添付ファイルを読み、接続人数や既読状態に含まれ、通知を受け取ることは、グループ参加だ。前者はプラットフォームの責任に必要な範囲であり、後者はグループの構成員として持つ関係である。
私たちは、最高管理者がすべてのグループを確認できるという理由だけで、自動的にメンバーシップを持たないよう境界を引き直した。参加申請が承認されたか、明示的に割り当てられた有効なメンバーシップがあるグループでだけ、チャット活動の主体になれるようにした。メンバーシップがなければ、メッセージ、添付ファイル、既読、リアルタイム接続、ルームへの招待、公開ルームへの自動参加をサーバーで防いだ。グループ選択画面でも、すべてを単純に「参加中」と見せず、管理ビューと参加中を区別した。
この区別が重要なのは、画面に見えるボタンよりも、データに残る痕跡のためだ。管理点検をしたアカウントがチャット参加者として数えられ、未読メッセージの受信者に入り、グループ通知を受け取った瞬間、プラットフォームの管理権限は静かにグループ内で行動する権限へ広がる。あとから画面上のボタンを一つ隠しても、すでに混ざった関係を元に戻すのは難しい。
そこで、判断基準を次のように変えた。
- プラットフォームの役割は、プラットフォームのポリシーとセキュリティ、グループの作成・停止を管理する。
- グループメンバーシップは、特定のグループ内での参加と役割を表す。
- 現在画面に開いているグループは閲覧の文脈にすぎないことがあり、それ自体はメンバーシップの証拠ではない。
- チャット、通知、参加人数の集計では、有効で承認済みのメンバーシップをサーバーがもう一度確認する。
管理者が便利にすべてを見られることよりも、グループ自身が誰を実際の構成員としているか説明できることのほうが重要だった。
アカウント承認とグループ参加も同じことではなかった
それまでは、登録承認にアカウントとグループ参加がひとまとまりになっていた。ユーザーが登録すると、どのグループに属するかまで一度に決まる流れは、グループが一つのときは単純だった。複数グループと新規グループの申請が生まれると、この構造はすぐに限界へ達した。
私たちは、最初のユーザーを除く新しいアカウントを有効にしつつ、グループメンバーシップは自動作成しない方向を選んだ。ログインセッションは存在しても、承認済みのメンバーシップがなければ、グループのオンボーディングだけを利用できる。既存グループへの参加と新規グループの作成は別々の申請になり、それぞれ誰が確認したかと、承認・拒否した時刻を残すようにした。
ここでも、画面だけを隠すのでは不十分だった。メンバーシップのない一般アカウントが、ホームやAPIのアドレスを直接呼び出した場合、サーバーがオンボーディング必要状態として拒否しなければならない。グループ管理者は自分のグループへの参加申請だけを確認し、新規グループ作成の申請はプラットフォーム責任者だけが扱うよう、問い合わせ結果そのものを分けた。
脱退も、行を削除するのではなく状態として残した。本人が退会した場合と管理者が退会させた場合を区別し、再参加の際に以前の役割を復元するか、一般メンバーとして始めるかを別々に決めた。人の関係は、「存在する」か「削除された」かの二つだけでは説明しにくいからだ。
この作業を通じて、アカウントは身元を、メンバーシップは関係を、役割はその関係の中での権限を意味するべきだという基準ができた。一つの役割値ですべてを解決する手軽さよりも、後からなぜアクセスが許可されたのかを説明できる構造を選んだ。
人のメンバーとAIペットを同じ枠に入れなかった
AIペットがチャットルームに入り始めると、別の誘惑が生まれた。ペットもメッセージを残すので、人のユーザーと同じメンバーテーブルに入れれば実装しやすそうに見えた。しかしペットは登録して同意した人ではなく、独立したグループ構成員でもない。
ペットは、グループが取得または割り当てを受け、設定するAIツールに近い。表示名、役割、使用するモデル、公開の有無はグループごとに変えられ、どのチャットルームに配置するかも管理者が決める。ユーザーは、現在のグループが許可し、現在のルームに配置したペットにだけ質問できなければならない。
そこで、人のグループメンバーシップと、ペットのグループ別設定・ルーム配置を分離した。AIの回答を保存するときも、人のアカウントに見せかけず、専用のボットアイデンティティを使うようにした。サーバーは、質問した人のルームアクセス権、ペットのグループ公開設定とルーム配置をもう一度確認した。
この境界は、単にテーブルを整理する作業ではなかった。ペットが回答したからといって、そのペットにグループの全資料を読む権限が生じるわけではない。最高管理者がペットを設定できるからといって、そのペットの会話がすべてのグループを横断してもよいわけではない。人とペットを同じ「メンバー」という言葉で呼ぶことはできても、同じ権利と責任を持つ主体として保存してはならなかった。
投票結果だけでなく、意思決定の過程も残した
誰がグループ管理者の権限を持つかを、プラットフォーム運用者が任意に上書きできる値だけにはしたくなかった。現在のグループで議案を作り、承認済みメンバーが承認または拒否を一度だけ選び、過半数によって結果を確定する、小さなガバナンスの流れを作った。
重要なのは、最終的なapprovedやrejectedという値だけを残すことではなかった。
議案を作成した時点で投票対象者を固定し、一人一票をデータベースでも強制した。議案の作成、承認票、拒否票、最終決定を、順番に追加される履歴として保存した。提出済みの票を修正または削除するAPIは作らなかった。権限の付与・喪失に関する議案が可決された場合は、最終状態、役割の変更、適用時刻を一つのトランザクションで記録した。決定の直前に対象メンバーシップが無効になっていた場合は、安全側へ倒して拒否で終了した。
この方式は不便だ。誤って投じた票を運用者が簡単に直すことはできず、データの行数も増える。その代わり、「今は誰が管理者なのか」だけでなく、「誰が、どの構成員の範囲で、どのような決定を経て管理者になったのか」を説明できる。
グループ主権は、壮大な宣言よりも、このように復元可能な説明に近かった。プラットフォームが意思決定の結果を保存しても、グループの決定を見えないところで書き換えないという約束だった。
最初の失敗:最高管理者の議案作成が妨げられた
境界を細分化すると、すぐに反対方向のバグが現れた。
一般メンバーがグループ管理者権限に関する議案を提案するには、現在のグループで有効かつ承認済みのメンバーシップが必要だった。この検査は正しかった。問題は、プラットフォームの範囲で権限の付与・喪失に関する議案を登録する必要がある最高管理者にも、同じメンバーシップ検査を重ねて適用していたことだった。最高管理者は一部のグループを管理ビューで開けても、実際のメンバーシップを持たない場合があるため、必要な議案の登録が拒否された。
解決策は、最高管理者をすべてのグループの構成員へ戻すことではなかった。議案登録に必要なプラットフォーム管理権限にだけ明示的な例外を置き、投票とチャット参加には引き続き実際のメンバーシップを求めた。
このバグは、権限検査を増やせば自動的に安全になるわけではないことを示した。異なる行為に同じメンバーシップ条件をコピーすると必要以上に遮断したり、反対に一つの例外が広がりすぎたりする。
それ以降は、「管理者だから許可する」や「メンバーではないから拒否する」という文だけで条件を作らないようにした。まず、どのリソースに対して、どの行為を、どの範囲で実行するのかを書いてから検査した。この例外は議案の作成にだけ適用され、グループへの参加資格を生み出さないという限界も設計文書に残した。
AIは空間を直接適用せず、制約された下書きを提案した
同じ週、新しいグループの最初の空間をAIペットのココと一緒に整えるオンボーディングも作った。長いプロダクトオンボーディング全体を一度に実装するのではなく、グループ承認後に空の空間で離脱しないようにする、小さく完結した一連のフローから始めた。
ユーザーは空間の目的、最初に見せるメニュー、雰囲気を選ぶ。AIの呼び出しは、ユーザーがAIと一緒に整えるを明示的に押したときだけ行った。結果は自由なReactコードやCSSではなく、既存のレンダラーが理解できる制約されたマニフェストだった。許可済みのブロックと内部パスだけを残し、外部URL、HTML、JavaScript、CSS、実行コードは除外した。
最も重要な段階は、生成の後にあった。
AIの結果を、そのままグループ空間へ適用しなかった。同じレンダラーでプレビューを見せ、グループ管理者がこの空間で始めるを押したときだけ適用した。保存時には、検証済みの下書き、現在の適用内容、下書きのリビジョン、適用バージョン、履歴を一つのトランザクションで一緒に更新した。想定していたリビジョンが変わっていれば、古い画面が最新の設定を上書きできないようにした。
モデルへの入力も制限した。グループチャットの原文や文書を自動送信せず、ユーザーが選んだ目的、メニュー、雰囲気と短いメモだけを渡した。モデルが接続されていない場合や呼び出しに失敗した場合は、流れを止めず、同じ許可構造を持つ決定的なローカルガイドを使った。監査のためという理由で原文全体を再保存せず、入力ハッシュ、文字数、モデルとプロンプトのバージョン、トークン数、結果、処理時間を記録した。
この時点で、AIがプロジェクトを作ったりコードをデプロイしたりすることはなかった。役割のすべては、制約されたグループ空間の下書きを提案することだった。しかし範囲が小さかったからこそ、その後も再利用する原則を実際の流れで確認できた。
AIは提案できる。人は実際の表示結果を確認して適用する。下書きと適用済みの内容は同じものではない。
二つ目の失敗:カラムはあったが、本番にはなかった
オンボーディングには、グループ承認後にココが最初の案内を一度だけ表示するためのメンバーシップ状態が必要だった。コードには新しいカラムを追加し、既存の行には初期値が適用され、新しい承認や再参加のときだけ案内が有効になるように経路も書いた。
ところが、本番で新規グループの承認が失敗した。拒否は正常に処理された。違いは、承認トランザクションだけが新しいメンバーシップカラムを使っていたことだった。
原因はスキーマ変更の内容ではなく、スキーマのバージョンだった。新しいカラムを作る冪等なマイグレーションを書いたのに、コアバージョンは以前の値のままだった。本番データベースではすでにそのバージョンが適用済みだったため、初期化処理が変更を飛ばした。アプリケーションは存在すると考えていたカラムを読み書きしようとし、承認の経路だけが失敗した。
すでにカラムが存在していても安全なマイグレーションが再び実行されるよう、バージョンを上げた。当時の記録には、認証境界の確認と新規グループ承認の再試行が続いている。修正自体は短かったが、教訓は長く残った。
- アプリケーションコードとデータベース変更は、同じコミットに含まれていても同じ時点に適用されるとは限らない。
- 新しいフィールドを使わない拒否経路の成功は、承認経路が正常に動くことを証明しない。
- 冪等なDDLを書くことだけでは足りず、本番にその変更を実行させるバージョンゲートも一緒に変えなければならない。
- デプロイ検証では、画面が一つ開くかどうかよりも、変更したデータ経路を実際に通過しなければならない。
権限エラーとスキーマエラーは、外から見るとどちらも「承認できない」と見えることがある。そのため、ユーザーには次の行動を選べるメッセージを示し、運用者にはどの境界で拒否されたかを観察できる記録を残すことが、ともに必要だった。
権限は、理解できてこそ守ることができた
この時期には、韓国語・英語・日本語のUIと共通トーストも整えた。初めは、翻訳と通知は権限設計より軽い仕上げ作業のように見えた。実際には、権限をユーザーが理解できるようにする最後の境界だった。
承認、拒否、保存、削除の結果が画面ごとに異なる場所と文章で現れると、ユーザーは自分の選択が反映されたのか確信しにくい。特にポップアップの背後に通知が隠れたり、一つの言語で対象や結果が抜けたりすると、誤った操作を繰り返す可能性がある。そこで、短く知らせればよい変更結果は共通のshowToast()の流れにまとめ、ユーザーがすぐ直す必要のあるフィールドエラーと再試行が必要な障害は該当する画面に残した。
静的なUI文言は3言語の辞書へ同時に登録する一方、ユーザー名、グループ名、文書、チャットの原文をUI辞書が勝手に翻訳しないようにした。言語設定APIも言語の値だけを変え、表示名や権限には触れないよう範囲を狭めた。
権限ポリシーがサーバー上でだけ正確でも不十分だった。ユーザーが、どのグループを管理ビューで開いているのか、実際に参加中なのか、自分の承認が保存されたのかを理解できてこそ、同じ誤りを減らせる。多言語と共通フィードバックは装飾ではなく、説明可能な権限の一部だった。
Codex開発協力者は、まだ設計図の中にしかいなかった
7月19日には、AIペットが実際の開発作業を提案する流れを検討した。当時、完成した実行システムが存在したと書けば事実ではない。
その週に決めたのは、実装よりも境界だった。ペットは作業を提案できるが、実行権限を直接持たない。開発作業はHaru Spaceの承認フローと隔離された実行環境を通り、人が実行前と結果の反映前にそれぞれ確認する。コードが反映されたという理由だけで、本番デプロイの権限まで続くこともない。
具体的な実行機とデプロイ接続は、その後の課題として残った。この段階で得た基準はシンプルだった。AIによる提案、コード変更、反映、本番デプロイはそれぞれ異なる権限であり、結果の影響が大きくなるたびに、人による承認地点がもう一度現れなければならない。
この一週間の後に残った基準
機能を増やす前に境界を分けることは、開発を遅らせるだけではなかった。どこで問題が起きたのか探す範囲が狭まり、次の機能でも再利用できる言葉が生まれた。その週の決定から得た実践的な基準は、次のとおりだ。
1. 役割名より先に、リソースと行動を書く
adminという名前だけで許可しない。プラットフォーム設定の閲覧、グループ議案の作成、投票、チャット参加、デプロイは、それぞれ異なるリソースと行動である。
2. アカウント、メンバーシップ、役割を分離する
ログインできるという事実は、特定のグループに所属しているという意味ではない。現在のグループを画面に開いたという事実も、行動する権限を生み出さない。
3. 画面で隠す仕組みの後ろに、サーバー検査を置く
ボタンの表示は使いやすさのための案内だ。本当の境界は、セッション、有効なメンバーシップ、グループとリソースの所属をサーバーがもう一度確認したときに生まれる。
4. AIの結果を、下書き・プレビュー・適用に分ける
モデルの出力を許可リストで正規化し、人が実際の表示結果を確認してから、明示的に適用する。公開や実行など結果の影響が大きい行為は、別の承認としてもう一度分ける。
5. 決定を上書きせず、履歴を追加する
誰が承認したかだけでなく、その時点で誰が対象であり、どの順序で決まったかを残す。取り消しも過去を削除せず、新しい出来事として説明できなければならない。
6. スキーマ変更は、バージョンと経路まで検証する
カラムとマイグレーションコードの確認だけで終わらない。本番のバージョンが実際に上がるか、新しいフィールドを使う承認・保存経路を通過するか確認する。
7. 権限の結果をユーザーの言葉で説明する
承認、拒否、エラーの文言は、3言語で同じ意味を持たなければならない。短い結果は共通フィードバックで伝え、修正や再試行が必要な問題は、その文脈の中に残す。
ボタンより先に作ったもの
この週が終わったとき、Haru Spaceには多くのボタンが増えていた。グループ参加と作成の申請、承認と拒否、議案と投票、AIによる空間の生成と適用が画面に並んだ。
しかし、実際に作ったものはボタンの数ではなかった。
プラットフォームが閲覧できることと、グループの構成員ができることを分けた。人の関係とAIペットの配置を分けた。AIが提案した下書きと、人が適用したバージョンを分けた。現在の状態と、そこに至った意思決定の履歴を分けた。
二つの失敗も、この境界をさらに鮮明にした。最高管理者に必要な例外は、グループメンバーシップ全体ではなく、特定の議案を作成する行為だけに必要だった。新しいカラムはコードに存在するだけでは本番に存在せず、バージョンと実際の承認経路が一緒に動かなければならなかった。
振り返れば、グループ主権とは、プラットフォームが何もしないという意味ではなかった。プラットフォームが何を管理し、何をグループの代わりに決めないのか、グループ内の関係がどこで始まり、どこで終わるのかを説明できるという意味に近かった。
優れた権限設計とは、一人の強い管理者を作ることではなく、それぞれの決定が正しい場所でだけ力を持つようにすることだった。
次の記録では、この境界の上で、話すAIペットを実際の開発協力者につなげようとしたときに何が失敗したのか、作業の提案、隔離された実行、人による承認、コミットとデプロイの間をどのように一つずつつないでいったのかを振り返りたい。
反応を残すと、重複防止のためランダムな識別子がこのブラウザーに保存される場合があります。

