本番の Firestore を素手で触るのが怖いので、「仕組みで守る」GUI クライアントを作った
本番の Firestore、素手で触っていますか
Firestore を使った運用で、こんな経験はないでしょうか。
- Firebase コンソールでドキュメントを消した直後に「あっ」と声が出た(undo はありません)
- 開発環境のつもりで本番プロジェクトを開いていた(タブの見た目は同じです)
-
priceが string の行と number の行が混ざっていることに、集計が壊れてから気づいた - 「昨日誰かがこのドキュメント触った?」に誰も答えられない
私はぜんぶあります。特に1つ目は、深夜のデータメンテナンス中にやりました。バックアップを取っていない
コレクションだったので、アプリのログから値を推測して手で復旧する羽目になりました。
「気をつける」では二度と防げないと思ったので、うっかりを人間の注意力ではなく仕組みで止める
Firestore GUI クライアントを作りました。この記事ではその設計、特に「全書き込みを安全パイプラインに
強制的に通す」アーキテクチャと、開発中に踏んだ落とし穴を共有します。
作ったもの: Firescope(macOS / Windows、日本語ネイティブ)
https://fire-scope.com
コンセプト: 「本番の書き込みを、仕組みで守る」
機能一覧(表形式表示・CSV入出力・分割ビュー・リアルタイム監視など)はサイトに譲るとして、
この記事では核になっている3つの設計だけ書きます。
- すべての書き込みが安全パイプラインを通る(バイパス経路が存在しない)
- UI ではなくメインプロセスで強制する(renderer は信用しない)
- 確認と実行をワンタイム nonce で 1:1 に対応させる(「さっきの確認」の使い回しを防ぐ)
スタック
- Electron + React + TypeScript + Tailwind(electron-vite)
- Firestore アクセスは
firebase-admin(サービスアカウント接続) - main ⇔ renderer は Zod スキーマを唯一の真実とする型付き IPC
設計1: 安全パイプライン — 書き込みの「唯一の道」を作る
まず大方針として、破壊的操作(作成・更新・削除・インポート・復元)の実行関数を1つに絞りました。
どの機能から来た書き込みでも、必ずこの runDestructive を通ります。
// main プロセス(簡略化)
export async function runDestructive(req: DestructiveRequest): Promise<Result> {
const connection = getConnection(req.connectionId);
// 1. 確認チケットの検証(後述の nonce)
consumeConfirmation(req.confirmationId, req);
// 2. 自動バックアップ: 対象ドキュメントをスナップショットとして保存
const snapshotId = await backupTargets(connection, req.targets);
// 3. 実行
const outcome = await execute(connection, req);
// 4. 操作ログに記録(成功でも失敗でも)
recordOperation({ ...summarize(req, outcome), snapshotId });
return outcome;
}
ポイントは「バックアップを取ってから実行する」の順序がコードの構造として崩せないことです。
「今回は急いでいるからバックアップなしで」という抜け道を、UI にもコードにも作りません。
削除してしまっても、直前のスナップショットがあるので差分プレビューを見ながらワンクリックで復元
できます。undo がない Firestore に、実質的な undo を後付けする発想です。
環境ラベル × 危険度マトリクス
接続には「本番 / ステージング / テスト / 開発」のラベルを付けます。確認 UI の強度は
環境 × 操作の危険度のマトリクスで決まります。
// 例: 本番 × コレクション削除 → プロジェクトIDのタイプ入力を要求
const MATRIX: Record<Environment, Record<Risk, Guard>> = {
production: { low: "confirm", medium: "typed", high: "typed" },
staging: { low: "none", medium: "confirm", high: "typed" },
development:{ low: "none", medium: "none", high: "confirm" },
// ...
};
本番でコレクションを消そうとすると、AWS のリソース削除のようにプロジェクトIDを手で入力
しないとボタンが押せません。「開発のつもりだった」事故はここで止まります。
設計2: renderer を信用しない — 強制はメインプロセスで
Electron アプリの「確認ダイアログ」を renderer(React 側)だけで実装すると、
バグや将来の改修で確認を飛ばす経路ができたときに、そのまま書き込みが通ってしまいます。
Firescope ではガードの判定と強制をすべて main プロセス側に置いています。renderer の役割は
「確認 UI を表示して、ユーザーの応答を返す」ことだけで、判定の主体ではありません。
さらに main ⇔ renderer の IPC は、チャネルごとに Zod スキーマで入出力を検証しています。
// shared/ipc/contract.ts — main と renderer が同じ定義を参照する
export const contract = {
"doc:update": {
input: z.object({
connectionId: z.string(),
path: z.string(),
confirmationId: z.string(), // ← 確認チケットなしでは呼べない型にする
patch: fsonPatchSchema,
}),
output: operationResultSchema,
},
// ...
} as const;
renderer からは型付きの invoke("doc:update", ...) しか呼べず、main 側は受信時に
スキーマ検証してから処理します。「型の真実」を shared に1箇所だけ置くことで、
main と renderer の認識ずれ(引数の形が違う・検証漏れ)をコンパイル時と実行時の両方で潰せます。
設計3: 確認の使い回しを防ぐ — ワンタイム nonce
「確認 → 実行」を素朴に実装すると、確認したときと実行するときで内容がすり替わる
バグ(TOCTOU)の余地が生まれます。対策として、確認が完了すると main が
操作内容にひもづいたワンタイムの confirmationId を発行し、実行時に消費します。
- confirmationId は操作リクエストのハッシュと 1:1(別の操作には使えない)
- 一度使ったら無効(同じ確認で2回実行できない)
- 有効期限つき(確認したまま放置 → 失効)
これで「確認ダイアログは出たが、実行されたのは別の内容だった」という最悪のパターンを
構造的に排除できます。
開発で踏んだ落とし穴(これから作る人へ)
同じものを作る人はいなくても、部分的に役立つはずの知見です。
firebase-admin v14 はエミュレーターでもダミー credential を拒否する
昔よくあった credential: 適当なオブジェクト が通らなくなっています。使い捨ての RSA 鍵を
その場で生成して cert() に渡すと、エミュレーター相手でも初期化できます。
また db.settings() は同じ app に2回呼ぶと throw するので、named app の再利用時は注意。
PowerShell が書く JSON には BOM が付く
外部ツールが設定 JSON を書き換えるケースで JSON.parse が突然死にます。ファイル読み込みは
BOM 除去を一枚かませたユーティリティに統一しました。
Electron の safeStorage は「保存先」ではなく「鍵」
サービスアカウント鍵の保管は safeStorage(macOS Keychain / Windows DPAPI 由来の鍵で暗号化)
を使い、復号は接続確立の瞬間だけ。データベースの内容も鍵も外部サーバーへは一切送らない
構成にしています(アプリは Firestore と直接通信)。デスクトップアプリの良さはここで、
Web ベースの管理ツールと違って第三者のサーバーにデータを預けずに済みます。
「誰が触ったか」を外部サーバーなしで共有する
チームで「誰が・いつ・何を触ったか」を見たい要望に対して、履歴を自社サーバーに集める設計は
プライバシー方針と衝突します。結論として、操作メタデータ(値は含めない)を顧客自身の
Firestore の専用コレクションに書く方式にしました。データ主権が顧客側に残り、
外部送信ゼロのまま「共有ログ」が成立します。ログは30日で自動削除します。
まとめ
- Firestore の事故は「注意」ではなく「構造」で防ぐ — 書き込みの唯一の道を作り、そこにバックアップとガードを埋め込む
- 強制は renderer ではなく main で。IPC の型は Zod で一元化する
- 確認と実行はワンタイム nonce で 1:1 に縛る
Firescope は https://fire-scope.com で配布しています(14日間は全機能無料、期限後も閲覧は無料の
ままです)。Firebase コンソールとの機能比較はこちらのページにまとめました。
「この安全設計はここが甘いのでは?」という指摘こそ一番ほしいフィードバックです。
コメントでぜひ教えてください。

