空白画面は信頼を殺す。31個もあった。
SaaSアプリで31の空白画面を見つけました。原因はマルチテナンシーがデータアクセスだけでなくURLコンテキストの問題でもあることを忘れていたからです。Claude Codeの助けで一晩で全部修正した方法がこちらです。
11月1日、午前8時47分。メールが来ました。
「Chandlerさん — 『表示』をクリックしたら空白ページになりました。インタビューデータは全部消えましたか?」
胃が沈みました。空白ページはバグではありません。信頼を殺すものです。
開発コンソールを開きました。ルートを確認しました。エージェンシーユーザーがクライアント「Acme Corp」を表示中にボタンをクリックすると...URLが/clients/acme-corp/agents/agent-name/results/123からただの/agent-name/results/123に変わっていました。
クライアントコンテキストが失われた。React Routerがルートを見つけられなかった。空白画面。
「OK、バグが1つだな」と思いました。「修正して先に進もう。」
Claude Codeにコードベースの類似パターンの分析を依頼してから、家族と土曜日を過ごしました。ランチ。買い物。子供たち。いつも通り。
午後遅くに戻ってチェックしました。Claude Codeがパターンを見つけていました — そしてきれいなものではありませんでした。
午後7時42分までに14のバグを修正。午後7時48分までにさらに8つ発見。午後7時56分にはカウントは合計31に。
すべて同じ根本原因。すべて同じ修正。すべて、マルチテナンシーを構築する時に1つのことを忘れたからです:ナビゲーションはデータだけでなくコンテキストの問題でもある。
---
実際どれほど深刻だったか?
31のバグが実際に何を意味していたか正直に言います:
ユーザーにとって:
- エージェンシーユーザーが空白画面に遭遇(バグではなく壊れて見える)
- セッション途中でワークフローの進行が失われる
- 「このプラットフォームはクライアントに使えるほど安定しているのか?」
- 空白画面1つ = トライアルキャンセルに1歩近づく
私にとって:
- 1日5件以上のバグレポート(すべてナビゲーション関連)
- 各バグの個別デバッグに2-3時間
- 新機能がリリースできない(火消しに忙殺)
- 純粋な恐怖:「他にも何か壊れていて気づいていないのでは?」
実存的な疑問: ナビゲーションすら正しく動かせないなら、なぜ誰かがSTRAŦUMにクライアントのマーケティング戦略を任せるべきなのか?
空白画面は何よりも速く信頼を殺します。
---
すべての始まりとなったバグ
何が起きたか正確にお見せします。
ユーザーフロー(動くべきだったもの):
1. /clients/acme-corp/agents/analysisに移動
2. 「セッション開始」をクリック
3. 分析を完了
4. 「結果を見る」をクリック
5. /clients/acme-corp/agents/analysis/results/123で結果を見る
実際に起きたこと:
1. ✅ /clients/acme-corp/agents/analysis(OK)
2. ✅ セッション開始(動作中)
3. ✅ 分析完了(データ保存済み)
4. ❌ 「結果を見る」をクリック → 空白ページ
5. ❌ URLが/analysis/results/123に変わった(クライアントコンテキスト喪失)
React Routerが/analysis/results/123でルートを探しました。エージェンシーユーザーには存在しません。何もレンダリングしませんでした。
ユーザーが見るのは:真っ白な画面。エラーメッセージなし。ローディングスピナーなし。ただ...何もない。
---
掘り下げて(実際にはClaude Codeが)発見したもの
壊れたコードはこんな感じでした:
```typescript
// AgentPage.tsx(サニタイズ済み - 壊れたパターン)
import { useParams, useNavigate } from 'react-router-dom';
export function AgentPage() {
const { clientSlug } = useParams<{ clientSlug: string }>();
const navigate = useNavigate();
const handleViewResults = (sessionId: string) => {
// 問題:ハードコードされたルート、クライアントコンテキストなし
navigate(`/agent-name/results/${sessionId}`);
};
return (
// ... コンポーネント
);
}
```
問題が見えますか?
URLからclientSlugを抽出しました。データ取得に使いました。しかしナビゲーション時には完全に忘れていました。
エージェンシールートはこう:/clients/acme-corp/agents/agent-name
SMEルートはこう:/agent-name
SMEパターンをハードコードしていました。エージェンシーユーザー = 壊れる。
---
気づき:31個もあった
午後2時15分。最初のバグを修正。コミット。良い気分。
午後3時42分。別のエージェントで別のバグ発見。同じパターン。修正。
午後4時18分。さらに別のエージェント。同じ問題。
午後5時30分。画面を見つめたまま、確実に10分間動きませんでした。
すべてのエージェントページに同じバグがありました。 ナビゲーションするすべてのネストされたコンポーネント。「移動...」ボタンを持つすべての共有UI要素。
1つずつ修正して2日間費やすこともできます。またはパターンを見つけてすべてを体系的に修正するか。
体系的を選びました。
---
修正:コンテキスト対応ナビゲーション
すべてのコンポーネントでuseParams()を使う代わりに、Claude CodeにContextプロバイダーの作成を依頼しました:
```typescript
// contexts/ClientContext.tsx
import { createContext, useContext } from 'react';
import { useParams } from 'react-router-dom';
interface ClientContextValue {
clientSlug: string | null;
}
const ClientContext = createContext<ClientContextValue | null>(null);
export function ClientContextProvider({ children }: { children: React.ReactNode }) {
// レイアウトレベルで一度だけclientSlugを抽出
const { clientSlug } = useParams<{ clientSlug: string }>();
return (
<ClientContext.Provider value={{ clientSlug: clientSlug || null }}>
{children}
</ClientContext.Provider>
);
}
export function useClientContext() {
const context = useContext(ClientContext);
if (!context) {
throw new Error('useClientContext must be used within ClientContextProvider');
}
return context;
}
```
そしてエージェンシールートをラップしました:
```typescript
// App.tsx
<Route path="/clients/:clientSlug/*" element={
<ClientContextProvider>
<ClientLayout />
</ClientContextProvider>
}>
<Route path="agents/analysis" element={<AnalysisAgent />} />
<Route path="agents/strategy" element={<StrategyAgent />} />
{/* ... すべてのクライアントスコープルート */}
</Route>
```
これで内部のすべてのコンポーネントがparamsではなくcontextを通じてclientSlugにアクセスできます。
ルーティングヘルパー(同じif/elseを20回書くのは飽きるので):
```typescript
// hooks/useContextRoute.ts
import { useClientContext } from '@/contexts/ClientContext';
export function useContextRoute() {
const { clientSlug } = useClientContext();
const buildRoute = (route: string) => {
if (clientSlug) {
// エージェンシールート:/clients/acme-corp/agents/...
const cleanRoute = route.startsWith('/') ? route.slice(1) : route;
return `/clients/${clientSlug}/${cleanRoute}`;
}
// SMEルート:/agent-name/...
return route;
};
return { buildRoute, clientSlug };
}
```
使用方法(ずっとクリーン):
```typescript
// AgentPage.tsx(サニタイズ済み - 修正されたパターン)
import { useClientContext } from '@/contexts/ClientContext';
import { useNavigate } from 'react-router-dom';
import { useContextRoute } from '@/hooks/useContextRoute';
export function AgentPage() {
const { clientSlug } = useClientContext();
const { buildRoute } = useContextRoute();
const navigate = useNavigate();
const handleViewResults = (sessionId: string) => {
// SMEとエージェンシー両方のユーザーで動作
navigate(buildRoute(`agents/analysis/results/${sessionId}`));
};
return (
// ... コンポーネント
);
}
```
1つのヘルパー関数。コンテキスト対応。両方のユーザータイプで動作。
---
体系的修正:1日、31ファイル
パターンが分かれば、機械的になりました。
2025年11月1日 - スプリント
午後(2 PM - 7 PM) - 発見と分類:
- すべてのエージェントページにまたがるパターンを発見
- Contextプロバイダーとルーティングヘルパーを構築
- 最初のエージェントでアプローチをテスト
夜(7:00 PM - 8:00 PM) - 体系的修正:
7:42 PM - 第1波(14バグ):
```
fix(multi-tenant): fix 14 navigation bugs and refactor all agents to useClientContext()
Frontend Changes (8 files):
- Refactored 6 agents to use useClientContext() hook
- Fixed 14 navigation bugs that lost client context across multiple agents
Backend Changes (5 files):
- Added client_id parameter to all save operations
- Updated base classes to extract client_id properly
Files changed: 14
Insertions: +732
Deletions: -164
```
7:48 PM - 第2波(さらに8バグ):
```
fix(multi-tenant): fix 8 navigation bugs in strategy page
- Fixed 8 navigation buttons that lost client context
- Total bugs fixed: 22 (14 + 8)
Files changed: 1
Insertions: +71
Deletions: -23
```
7:56 PM - 最終波(さらに9バグ):
```
fix(multi-tenant): fix 9 navigation bugs in interview and tool pages
- Fixed remaining navigation issues in nested components
- Total bugs fixed: 31 bugs across entire application
Files changed: 2
Insertions: +46
Deletions: -10
```
すべてのエージェントフローを通しました。SMEユーザー。エージェンシーユーザー。すべてのボタンをクリック。空白ページなし。
総被害: 17ファイル変更、849行追加、197行削除。
投資時間: Claude Codeとの非連続的な作業で約6時間(2 PMの発見 → 8 PMの最終コミット)。
節約した時間: 恐らく30時間以上の個別バグ修正とユーザーサポート。
---
学んだこと(苦労して)
1. マルチテナントナビゲーションはデータ分離より難しい
データをorg_idでフィルタリングできます。それは簡単な部分。
でもナビゲーション?同じ機能に対して2つの有効なURL構造があります:
```
SME: /agent-name/session/123
Agency: /clients/acme-corp/agents/agent-name/session/123
```
すべてのnavigate()コールがどちらのパターンを使うか知る必要があります。1回間違えると、ユーザーは空白ページを見ます。
2. useParams()は嘘をつく
```typescript
const { clientSlug } = useParams();
```
これは動きます...ルートが変わるまで。するとclientSlugがundefinedになり、次のナビゲーションが壊れます。
React Contextは嘘をつきません。常に利用可能で、常に一貫しています。
3. 勝利宣言前にすべてのバグを数える
14のバグだと思っていました。さらに8つ見つかりました。さらに9つ。
教訓: バグがあると思うファイルだけでなく、コードベース全体をgrepしてください。
4. 同じバグを2回見つけたら、パターンを作る
個別修正: 31バグ = 恐らく1週間
体系的修正: パターンを見つける → ヘルパーを作成 → すべてのインスタンスを修正 = 6時間
午後5時30分に画面を見つめた10分間が数日を節約しました。
5. ナビゲーションバグは実存的脅威
状態管理、データフェッチ、API最適化に執着しがちです。
でもナビゲーションが壊れている = 空白画面 = 「このプラットフォームは壊れている。」
ユーザーはRLSポリシーやマルチテナントアーキテクチャを気にしません。「結果を見る」をクリックして結果が表示されることを気にします。「技術的に動く」と「実際に完成している」の間の同じギャップは、AIでネイティブiOSアプリを構築した時にも現れました。Claude Codeは速くスキャフォールドを生成しましたが、ユーザーの信頼を生むポリッシュは人間だけが提供できる40%です。
---
パターン(マルチテナントアプリ向け)
階層的URLを持つマルチテナントSaaSを構築しているなら:
✅ ステップ1: レイアウトレベルでContextプロバイダーを作成
```typescript
<ClientContextProvider>
{/* すべてのテナントスコープルート */}
</ClientContextProvider>
```
✅ ステップ2: ルーティングヘルパーを構築
```typescript
const { buildRoute } = useContextRoute();
navigate(buildRoute('agents/analysis/session/123'));
```
✅ ステップ3: ルートをハードコードしない
```typescript
// ❌ 悪い
navigate('/analysis/session/123');
// ✅ 良い
navigate(buildRoute('agents/analysis/session/123'));
```
✅ ステップ4: すべてのナビゲーションコールをgrep
```bash
# すべてのnavigate()コールを見つける
grep -r "navigate(" src/ > navigation_audit.txt
# ハードコードされたルートを見つける
grep -r "navigate('/" src/ | grep -v "buildRoute"
```
✅ ステップ5: すべてのユーザータイプでテスト
SMEフロー。エージェンシーフロー。すべてのボタン。すべてのリンク。空白ページなし。
---
結果
11月1日以前:
- 31のナビゲーションバグが潜伏
- 1日5件以上のバグレポート
- エージェンシーユーザーがプラットフォームの安定性を疑問視
- 私:新機能のリリースが怖い
11月1日以降:
- ナビゲーションバグ0
- ナビゲーション関連のサポートチケット0
- 将来の開発のためのパターンが確立
- 私:新しいエージェントルートのリリースに自信
開発速度:
- 新ルートの追加:5分(以前は30分 + 「これ壊れない?」)
- バグレポート:0(以前は1日5件以上)
- ユーザーの信頼:回復(「プラットフォームが安定していると感じる」)
この6時間のバグ修正は、サポート負担の軽減とユーザーの信頼回復で10倍のリターンがありました。
---
なぜこれを共有するのか
マルチテナントナビゲーションはセクシーではありません。「31のバグを6時間で修正」は「新機能をリリース」のようには祝われません。
でもhttps://stratum.chandlernguyen.com/のようなマルチテナントSaaSを構築しているなら — エージェンシー向けのAIマーケティングプラットフォーム — これにぶつかるでしょう。31バグではないかもしれません。5かもしれません。でもぶつかります。
その時、覚えておいてください:
1. ナビゲーション状態にはContext > Params
2. 個別修正より体系的修正
3. コードベース全体をgrep(思ったより多く見つかる)
4. 勝利宣言前にすべてのユーザータイプでテスト
そしてコンテキストがどこに行ったか分からない空白画面を見つめている自分がいたら、私も同じ状況だったことを知ってください。 :)
実際のユーザーにリリースした最も痛いバグは何でしたか — 誰かに言われて初めて気づいた種類の?ぜひ聞かせてください。
よろしくお願いします、Chandler
STRAŦUMアーキテクチャシリーズ: このナビゲーション危機は、より大きなマルチテナンシーの旅の一部でした。2日目のマルチテナンシー構築から始まり、67日目にスキーマ全体を再構築しなければならなくなり、データベースが正しいが296倍遅いことが判明して完結しました。
---
まだコーディング中、まだ学習中、まだ31個単位でバグを発見中。
https://stratum.chandlernguyen.com/request-invitationでアルファアクセスをリクエスト
---
P.S. - 最初のバグを報告したユーザー?インタビューデータは失われていませんでした。データベースに保存されていました。壊れたルートのせいで見えなかっただけです。午後7時42分に修正した時、作業はすべてそこにありました。あの小さな安堵感が6時間を価値あるものにしました。
---





