■ 1. サービス終了の困難さ
- 作るより消すほうが大変:
- Webサービスは作成時より終了時のほうが面倒
- サーバーを止めるだけでは終わらず、多数の撤去作業が残る
- 終了時に必要な作業:
- 新規登録停止、課金停止、書き込み停止
- ユーザー通知、個人データ削除、Webhook停止
- APIキー削除、DNS削除、リポジトリ整理
- 複数SaaS統合の代償:
- 各SaaSは導入が容易な一方、解約時はUIの異なる管理画面を巡回する必要がある
- 処分対象の技術スタック:
- インフラはCloudflare Workers、Supabase、GitHub
- APIはGemini API、監視はSentry、分析はGoogle Analytics
- 収益はPolarとGoogle AdSense、加えて独自ドメイン
■ 2. 終了方針の決定
- まず「どう死ぬか」を決める:
- 終了方針の決定が最初の必須作業
- 方針がなければ削除の判断ごとに迷い、本番環境で試行錯誤する羽目になる
- 事前に決めるべき項目:
- 終了日、新規登録停止日、既存ユーザーに許す操作の範囲
- 購入済みコンテンツの扱い、データ削除期限、決済履歴の保持有無
- サポート終了時期、ソースコードの保管方針、本番データのバックアップ有無
■ 3. 段階的な撤退戦
- 一撃必殺ではなく撤退戦:
- 一度にすべてを止めると、終了処理自体に必要な機能まで止まる
- 3段階の停止設計:
- 第1段階は新しい利用の停止
- 第2段階は終了日まで読み取り専用を維持
- 第3段階は完全停止とインフラ撤去
- 店舗の比喩:
- 新規客の入場禁止、来客をすべて帰す、在庫処分という順序に相当する
■ 4. 第1段階: 責任を増やさない
- 新規利用の停止:
- 新規登録、購入、回答保存、ペアリング、データ更新を止める
- 終了日後に新規ユーザーからサポート要請を受ける事態を避ける
- ボタンを消して満足してはいけない:
- UIはセキュリティ境界ではなく、APIを直接叩けばバイパスされる
- 玄関に本日休業と貼ったあと、裏口を全開にしている状態に等しい
- 停止すべきレイヤー:
- 画面・フォーム、サーバーAction、APIエンドポイント
- 認証サービス、DB権限(RLS)、決済設定
■ 5. 読み取り専用モードと認証停止
- 読み取り専用モードの実装:
- 終了日までは既存ユーザーによるデータ閲覧を許可する
- 環境変数 SERVICE_READ_ONLY で制御し、書き込みは403で拒否する
- 読み取り処理と書き込み処理が分離できているかの確認にもなる
- Authもちゃんと止める:
- フロント側のサインアップ画面を消しても、認証APIが生きたままでは不十分
- 認証APIを直接叩けばフロントエンドの制限をバイパスできる
- 認証側で行う対策:
- Supabase側で登録を無効化する
- RLSと権限設定により一般ユーザーの直接書き込みを拒否する
- 権限剥奪のやりすぎに注意:
- 終了するからと全権限を剥奪すると、終了処理自体が実行できなくなる
■ 6. ユーザーへの終了通知
- 通知に含める情報:
- 終了日、新規登録・購入の停止、終了日まで使える機能
- アクセス不可となる時点、問い合わせ先、データの扱い
- 通知内容の記録:
- 人間の記憶はログではなく、終了作業中はとりわけ信用できない
■ 7. 完全停止用PRの事前準備
- 当日にコードを書かない:
- 終了日当日に本番環境で緊急にコードを書くリスクを避け、PRを事前に用意する
- 停止時のレスポンス設計:
- Web画面は終了案内ページ、APIはJSON形式のエラーレスポンスを返す
- ステータスコードは410 Goneとする
- キャッシュ制御は no-store、SEO設定は noindex, nofollow とする
■ 8. スケジュール実行の不確実性
- GitHub Actionsのcronは時刻を保証しない:
- 予約実行が予定時刻から大幅に遅延した
- cronに23:59と書いても、cronは約束を覚えていない
- 代替手段:
- 事前実行で余裕を持たせる、外部スケジューラーを使う、当日手動で実行する
- アプリケーション自体で時刻を判定する、CDN・DNS側で設定する
■ 9. デプロイ成功とサービス停止は別
- CI成功でもサービスが止まらない:
- 停止処理をReact Routerのmiddlewareに実装した
- 設定が v8_middleware: false であり、機能自体が無効化されていた
- コードは存在し、型チェック・テスト・ビルド・デプロイはすべて成功したが実行されなかった
- 確認すべき段階:
- PRのマージ、ビルド成功、デプロイ成功
- 新バージョンへの経路確保、停止処理の実行、外部から見た期待レスポンス
- 緑のチェックマークの限界:
- 緑色のチェックマークは心を落ち着かせるが、本番環境を止めはしない
■ 10. curlによる最終確認
- 最後に信じられるのはcurl:
- 管理画面の状態表示ではなく、HTTPレスポンスを直接確認する
- 外からcurlして死んでいたら死んでいる、観測可能な事実のみを信頼する
- 確認方法:
- curl -I でヘッダを確認し、curl -i で詳細を確認する
- 期待する結果:
- 410 Gone、SERVICE_CLOSEDエラー、no-storeのキャッシュ制御、noindexのロボット指示
- 接続不能の確認対象:
- ルートドメインとwwwサブドメインが解決不可であること
- Worker直接URLとAPI・Webhook URLが接続不可であること
■ 11. インフラ撤去の順序
- 撤去は順序が重要:
- 前後関係を誤るとサービス終了そのものに支障が出る
- 決済Webhookを最初に止める:
- アプリケーション削除後も決済サービスがPOSTを再送し続ける
- 商品を販売停止または非公開にし、Checkoutを止め、Webhookを削除する
- 決済履歴は会計と問い合わせ対応に必要で、ユーザーデータと同じ保存方針では扱わない
- Workerとドメインの削除:
- Cloudflare Worker、Custom Domain、Worker Route、DNSレコードを削除する
- Worker用のVariablesとSecrets、GitHubの自動ビルド連携も削除する
- ドメインの自動更新停止は即時消滅を意味せず、契約期限まで存続する
- 即時停止にはDNSとRouteの削除が必要
- ユーザーデータの削除:
- Authユーザー、回答データ、利用履歴、購入権限、Storage内ファイルを削除する
- 本番ユーザーデータのバックアップは取らない方針を採り、一応ダンプしておく誘惑に抵抗する
- 削除日時、削除前後の件数、Authユーザー0件、Storage空、バックアップ未実施の判断を記録する
- 最後にSupabaseプロジェクト自体を削除し、確認ダイアログでプロジェクト名を入力する
- APIキーを一つずつ失効させる:
- Gemini APIキー、Sentryプロジェクト、Google Analyticsプロパティ、AdSenseサイト設定を処理する
- Cloudflare Secrets、Supabase Personal Access Token、GitHub ActionsのSecretsとVariablesを処理する
- プロジェクト削除は認証情報の自動失効を意味せず、明示的な失効が必須
- ほぼ無害だろうという判断が危険を招く
- GitHubリポジトリを墓石にする:
- 本番の秘密情報を無効化し、Actions SecretsとVariablesを削除する
- 不要なWebhookとDeploy keyを削除し、予約実行Workflowを無効化する
- ローカルファイルを整理し、リポジトリを非公開にしたうえでArchiveする
- DB構造とマイグレーションは参照・監査用にGitへ残し、本番ユーザーデータは残さない
■ 12. 敗戦処理チェックリスト
- チェックリストの6区分:
- 方針、事前停止、最終停止、データ削除、外部サービス、最終整理の順に並ぶ
- 方針の項目:
- 最終終了日、新規登録・購入停止日、既存ユーザーに残す機能の明示
- 購入済みコンテンツの扱い、データ削除期限、バックアップ方針、問い合わせ先
- 事前停止の項目:
- 新規登録と新規購入の停止、書き込みUIと書き込みAPIの停止
- 認証サービス側の登録停止、DB権限とRLSの確認、対象ユーザーへの終了通知
- 最終停止の項目:
- 停止用変更の本番反映、WebとAPIのレスポンス確認
- 決済Webhook停止、Worker・サーバー削除、DNSとCustom Domainの削除
- 外部ネットワークからの接続不能確認
- データ削除の項目:
- Authユーザー削除、DB個人データ削除、Storage削除
- 削除結果の記録、本番バックアップ方針の確認、DBプロジェクト削除
- 外部サービスの項目:
- 決済商品とWebhookの停止、AI用APIキーの失効、エラー監視プロジェクトの削除
- アクセス解析の削除、広告設定の削除、CI/CD Secretsの削除、Personal Access Tokenの失効
- 最終整理の項目:
- ドメイン自動更新停止、ローカル秘密情報の削除
- リポジトリの非公開化とArchive、問い合わせ先の維持、完了日時の記録
- チェックリストの位置づけ:
- サービス終了は機能開発と同等の正式な工程管理を要する
■ 13. まとめ
- サービス終了の本質:
- 重要なのはコードを止めることではなく、責任を順番に片付けること
- 痛感した3点:
- 新規利用の停止はUI・API・認証・DBのすべてのレイヤーで行う
- 完了の判断はマージやデプロイの成功ではなく本番のHTTPレスポンスで行う
- ユーザーデータ、決済履歴、ソースコードはそれぞれ別の方針で扱う
- 個人開発観の転換:
- 失敗したら閉じればいいという理解は誤り、サービス終了にも実装が必要
- 個人開発はデプロイまでではなくArchiveまで
- 最終的な教訓:
- 緑のチェックマークで満足せず、curlで検証する
- 終了日に元気に動作していたサービスが、この逆説的な教訓を残した