0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

本番の Firestore を素手で触るのが怖いので、「仕組みで守る」GUI クライアントを作った

0
Posted at

本番の Firestore を素手で触るのが怖いので、「仕組みで守る」GUI クライアントを作った

本番の Firestore、素手で触っていますか

Firestore を使った運用で、こんな経験はないでしょうか。

  • Firebase コンソールでドキュメントを消した直後に「あっ」と声が出た(undo はありません)
  • 開発環境のつもりで本番プロジェクトを開いていた(タブの見た目は同じです)
  • price が string の行と number の行が混ざっていることに、集計が壊れてから気づいた
  • 「昨日誰かがこのドキュメント触った?」に誰も答えられない

私はぜんぶあります。特に1つ目は、深夜のデータメンテナンス中にやりました。バックアップを取っていない
コレクションだったので、アプリのログから値を推測して手で復旧する羽目になりました。

「気をつける」では二度と防げないと思ったので、うっかりを人間の注意力ではなく仕組みで止める
Firestore GUI クライアントを作りました。この記事ではその設計、特に「全書き込みを安全パイプラインに
強制的に通す」アーキテクチャと、開発中に踏んだ落とし穴を共有します。

作ったもの: Firescope(macOS / Windows、日本語ネイティブ)
https://fire-scope.com

型注釈つきグリッド

コンセプト: 「本番の書き込みを、仕組みで守る」

機能一覧(表形式表示・CSV入出力・分割ビュー・リアルタイム監視など)はサイトに譲るとして、
この記事では核になっている3つの設計だけ書きます。

  1. すべての書き込みが安全パイプラインを通る(バイパス経路が存在しない)
  2. UI ではなくメインプロセスで強制する(renderer は信用しない)
  3. 確認と実行をワンタイム 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 コンソールとの機能比較はこちらのページにまとめました。

「この安全設計はここが甘いのでは?」という指摘こそ一番ほしいフィードバックです。
コメントでぜひ教えてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?