3ヶ月でFlutterアプリをリリースした話📱 〜個人開発で学んだこと〜
Flutterを利用して個人開発アプリをリリースさせてもらいました。
目次
📣 報告
記念すべきFirst Commitは8/11!💪
そこから、約2ヶ月を経て・・・
Androidは、10/23にアプリリリース🎉
iOSは、11/03にアプリリリース🎉
🎮 アプリ概要
アプリの内容的には、ポケポケ(Pokémon Trading Card Game Pocket)のカード検索補助アプリを作りました!
ポケポケのアプリの中にあるカード検索がとても使いづらく、それなら自分で作った方が早くね?ってなって、思い立ったが吉日。いつのまにか行動に移してました!笑
特に使いづらい点
- ワザのタイプ種類検索が無い
- 逃げるエネルギー検索が無い
- イラストレーター検索が無い
などなど・・・
あげたらキリがないほどたくさんありました笑
実装した機能 ✨
そこで、以下のような機能を実装しました。
🔍 高度なカード検索機能
- カード名、イラストレーター、ワザのタイプ、逃げるエネルギーなど多様な検索条件
- リアルタイム検索とフィルタリング
- 検索結果のキャッシング
🎴 デッキ管理機能
- デッキの作成・編集・削除
- デッキのQRコード生成・共有
- クラウド同期(プレミアム機能)
- デッキ枠拡張機能(チケット制)
📴 オフライン対応
- カード画像のダウンロード機能
- オフラインでの閲覧
💳 課金システム
- デッキ枠拡張チケット(買い切り)
- クラウド保存プラン(月額課金)
- Apple/Googleのレシート検証による不正防止
デッキデータをクラウド同期し、複数デバイスでシームレスに利用可能。
| プラン | 価格 | 更新周期 | 特典 |
|---|---|---|---|
| 月額プラン | ¥300/月 | 毎月自動更新 | デッキ無制限・自動同期 |
| 年額プラン | ¥3,000/年 | 毎年自動更新 | 月額比17%お得 |
🔐 認証・同期
- Google Sign-In / Apple Sign-In
- Supabaseによるバックエンド
- 自動保存機能
📱 画面構成
カード検索画面
アプリのメイン画面。ポケモンカードを様々な条件で検索できます。
- リアルタイム検索: カード名やイラストレーター名で即座に絞り込み
- 詳細フィルタ: タイプ、レアリティ、パック別での検索に対応
- グリッド表示: カード画像を見やすく一覧表示
- タップで詳細: カードをタップすると詳細情報を確認可能
カード詳細検索画面-1
公式アプリにはない高度な検索条件を提供。
- 逃げるエネルギー検索: 逃げるコストでカードを絞り込み
カード詳細検索画面-2
- ワザのタイプ検索: 特定タイプのワザを持つカードを検索
- ワザコスト検索: 必要エネルギーで絞り込み
カード詳細検索画面-3
さらに詳細な条件でカードを絞り込めます。
- イラストレーター検索: お気に入りのイラストレーターの作品を探せる
- 複数条件の組み合わせ: AND/OR検索で柔軟な絞り込みが可能
デッキ一覧画面
作成したデッキを一覧で管理できます。
- デッキ作成: 新しいデッキを簡単に作成
- デッキ編集: 既存デッキの修正・削除
- デッキ枠表示: 現在のデッキ数と上限を確認
- クラウド同期: プレミアム機能で複数デバイス間で同期
- QRコード共有: デッキをQRコードで簡単に共有
デッキ編集画面
直感的なUIでデッキを構築できます。
- カードの追加・削除: タップ操作で枚数を調整
- 枚数表示: 各カードの枚数を視覚的に確認
- デッキ枚数カウント: 合計枚数をリアルタイム表示
- カード検索連携: 検索画面から直接カードを追加可能
- 並べ替え機能: カードを長押しでドラッグ&ドロップして並び替え
設定画面
アプリの各種設定とアカウント管理を行えます。
- テーマ切り替え: ライト/ダークモード対応
- アカウント管理: Google/Apple Sign-Inでログイン
- オフライン設定: カード画像の一括ダウンロード
- プライバシー: 利用規約・プライバシーポリシーの確認
- 課金管理: 購入したアイテムやサブスクリプションの確認
デッキ数上限解放チケット
買い切り型の課金アイテムでデッキ枠を拡張できます。
- 1チケット = デッキ枠+15: 必要な分だけ購入可能
- 永続的な拡張: 一度購入すれば永久に有効
- 複数購入対応: 何度でも購入して枠を増やせる
- In App Purchase: Apple/Googleの正規課金システム
- レシート検証: サーバーサイドで不正購入を防止
クラウド保存
月額/年額サブスクリプションでデッキを無制限に保存できます。
- デッキ無制限: 999個まで保存可能
- 自動同期: 編集内容が自動的にクラウドに保存
- 複数デバイス対応: iPhone/iPad/Android間で同期
- 月額/年額プラン: ¥300/月または¥3,000/年(17%お得)
- 自動更新: ストア経由で自動的に更新
🛠️ 技術スタック
フロントエンド
- Flutter (Dart 3.0+)
- State Management: Provider
- UI: Material Design 3 + Google Fonts (Kiwi Maru)
バックエンド・サービス
- Supabase: 認証・データベース・Edge Functions
- Netlify: 静的アセット配信(カードJSON、利用規約、プライバシーポリシー)
-
Firebase:
- Analytics: ユーザー行動分析
- Crashlytics: クラッシュレポート
- Cloud Messaging: プッシュ通知
- Remote Config: 強制アップデート制御
課金・レシート検証
- In-App Purchase: Flutter公式プラグイン
-
Supabase Edge Functions:
- Apple App Store API連携
- Google Play Developer API連携
- サーバーサイドでのレシート検証
その他
- Xcode Cloud: iOS CI/CD
-
GitHub Actions:
- Flutter CI/CD(現在は無効化中)
- Supabaseデータベースの日次自動バックアップ(Google Cloud Storage)
- DeployGateへの自動デプロイ
🏗️ アーキテクチャ
レイヤー構成 🏗️
lib/
├── main.dart # アプリエントリーポイント
├── providers/ # State Management (Provider)
│ ├── auth_provider.dart # 認証状態管理
│ ├── deck_provider.dart # デッキ状態管理
│ ├── pokepoke_card_provider.dart # カードデータ管理
│ ├── purchase_provider.dart # 課金状態管理
│ ├── subscription_provider.dart # サブスクリプション管理
│ ├── offline_provider.dart # オフライン状態管理
│ ├── maintenance_provider.dart # メンテナンス状態管理
│ └── theme_provider.dart # テーマ管理
│
├── services/ # ビジネスロジック(29サービス)
│ ├── supabase_service.dart # Supabase初期化
│ ├── supabase_auth_service.dart # 認証処理
│ ├── supabase_deck_service.dart # デッキCRUD
│ ├── purchase_service.dart # 課金処理
│ ├── in_app_purchase_service.dart # IAP処理
│ ├── firebase_analytics_service.dart # Firebase Analytics
│ ├── crashlytics_service.dart # クラッシュレポート
│ ├── push_notification_service.dart # プッシュ通知
│ ├── remote_config_service.dart # Remote Config
│ ├── review_service.dart # In App Review
│ ├── storage_service.dart # ローカルストレージ
│ └── ... (その他サービス)
│
├── screens/ # UI画面
│ ├── card_search/ # カード検索画面
│ ├── deck_list/ # デッキ一覧画面
│ ├── deck_detail/ # デッキ詳細画面
│ └── settings/ # 設定画面
│
├── models/ # データモデル
│ ├── pokepoke_card.dart # カードモデル
│ └── deck.dart # デッキモデル
│
└── utils/ # ユーティリティ
├── debug_logger.dart # ログ出力
└── format_utils.dart # フォーマット処理
主要な設計パターン
1. Provider パターン 🔄
Flutterの推奨State ManagementライブラリであるProviderを採用。
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()..initialize()),
ChangeNotifierProvider(create: (_) => DeckProvider()),
ChangeNotifierProxyProvider<AuthProvider, PurchaseProvider>(
create: (_) => PurchaseProvider(PurchaseService())..initializeProducts(),
update: (_, auth, purchase) {
purchase!.setUserId(auth.userId);
return purchase;
},
),
],
child: MyApp(),
)
2. Repository パターン 🗂️
Service層でデータソースを抽象化し、Provider層はビジネスロジックに集中。
// Provider層
class DeckProvider extends ChangeNotifier {
final SupabaseDeckService _deckService = SupabaseDeckService();
Future<void> createDeck(Deck deck) async {
await _deckService.createDeck(deck);
notifyListeners();
}
}
// Service層
class SupabaseDeckService {
Future<void> createDeck(Deck deck) async {
await supabase.from('decks').insert(deck.toJson());
}
}
3. Dependency Injection 💉
ProviderProxyパターンで依存関係を管理。
ChangeNotifierProxyProvider3<AuthProvider, SubscriptionProvider, PurchaseProvider, DeckProvider>(
create: (_) => DeckProvider(),
update: (_, auth, subscription, purchase, deck) => deck!
..setAuthProvider(auth),
)
🚀 Claude Code CLIで開発が爆速
個人開発で最も時間がかかるのは、「調べる時間と実装する時間」です。
その問題を解決してくれたのがClaude Code CLIでした。
Claude Code CLIとは? 🤖
AnthropicがリリースしたCLIベースのAIコーディングアシスタント。
VSCodeなどのエディタではなく、ターミナル上で動作し、プロジェクト全体を理解しながらコーディングをサポートしてくれます。
実際の活用例
1. Supabase Edge Functionsのデプロイ 🚀
Apple App Store APIとの連携が必要だったレシート検証機能。
通常なら以下のステップが必要です:
- Apple Developer Portalで認証キーを生成
- JWT形式のトークン生成ロジックを実装
- App Store Server APIの仕様を調査
- Deno (Supabase Edge Functions) で実装
- デプロイとテスト
これをClaude Code CLIに依頼したところ:
$ claude "Apple App Store APIと連携するレシート検証Edge Functionを実装して"
わずか5分で、以下を自動生成してくれました:
- JWT生成ロジック
- App Store Server API呼び出し
- エラーハンドリング
- 環境変数管理
- デプロイコマンド
2. Xcode Cloud CI/CD設定 ☁️
iOSのCI/CD環境構築も、通常は以下の作業が必要:
-
ci_post_clone.shスクリプト作成 - App Store Connect Secretsの設定
- Schemeの判定ロジック
- GoogleService-Info.plistの動的生成
これもClaude Code CLIが全自動で実装:
$ claude "Xcode CloudでSecrets管理を実装して"
結果、ビルド成功までの時間を数日から数時間に短縮できました。
なぜClaude Code CLIが爆速なのか? 🚄
-
プロジェクト全体を理解している
- ファイル構造、依存関係、既存のコードスタイルを把握
- 一貫性のあるコードを生成
-
最新の情報にアクセスできる
- WebSearch機能で最新のAPIドキュメントを参照
- 公式ドキュメントに基づいた実装
-
マルチファイル編集が得意
- 複数ファイルを同時に編集
- 関連するファイルも自動で修正
-
Git操作も自動化
- コミットメッセージ自動生成
- 適切な絵文字プレフィックス付き
- GitHub CLI連携でIssue管理も完全自動化
-
GitHub Issue管理の自動化
-
ghコマンドでIssueの作成・更新・クローズを自動化 - 修正点・改善点・新機能はすべてIssueで管理
- Claude Code CLIがIssue番号を自動認識してコミットに紐付け
-
GitHub Issue活用による開発管理 📋
個人開発でも、タスク管理は重要です。このプロジェクトではすべての機能・修正・改善をGitHub Issueで管理し、Claude Code CLIと組み合わせることで効率化しました。
Issue管理の流れ
# 1. Claude Code CLIにIssueを作成してもらう
$ claude "Google Sign-In 400エラーの調査と修正をIssue化して"
# Claude Code CLIが自動実行:
$ gh issue create --title "🐛 Google Sign-In 400エラーの修正" \
--body "## 問題\n- Google Sign-In時に400エラーが発生\n..." \
--label "bug"
# 2. Issueに基づいて実装
$ claude ".claude/issue.md の内容を実装して"
# 3. 実装完了後、自動でIssueにコメント・クローズ
$ gh issue comment 45 --body "✅ 修正完了..."
$ gh issue close 45
実際のIssue活用例
このプロジェクトでは119個のIssueを作成し、すべて管理しました:
| Issue種別 | 件数 | 例 |
|---|---|---|
| 🐛 バグ修正 | 38件 | #45: Google Sign-In 400エラー |
| ✨ 新機能 | 52件 | #92: In App Review実装 |
| 📝 ドキュメント | 15件 | #98: Netlifyでカードデータ配信 |
| 🔧 改善 | 14件 | #119: データベース自動バックアップ |
Claude Code CLIとの連携メリット
従来の手動管理:
# 手動でIssue作成(ブラウザで操作)
# → コード修正
# → 手動でコミット
# → 手動でIssueクローズ(ブラウザで操作)
所要時間: 約1時間/Issue
Claude Code CLI自動化:
$ claude ".claude/issue.md の内容を実装して"
# → 自動でコード修正
# → 自動でコミット(Issue番号付き)
# → 自動でIssueコメント・クローズ
所要時間: 約1分/Issue
119個のIssueで節約できた時間:
- (60分 - 1分) × 119件 = **約117時間(約5日分)**の節約!
Issueベース開発の効果
-
進捗の可視化
- 完了したタスクが一目瞭然
- モチベーション維持につながる
-
後から振り返れる
- なぜその実装をしたのか記録が残る
- バグ修正の経緯も明確
-
優先度管理
- LabelでPriority分類
- 重要なバグから順に対応
-
Claude Code CLIとの相性抜群
-
.claude/issue.mdに内容を書くだけ - あとはClaude Code CLIが全自動で処理
-
個人開発でも、GitHub IssueとClaude Code CLIを組み合わせることで、チーム開発並みの管理体制を実現できました。
実際のコミットログ 📝
Claude Code CLIが生成したコミットメッセージの一例:
✨ In App Reviewを実装し、問い合わせフォームを削除
## 変更内容
### In App Review実装
- `in_app_review` パッケージを追加
- `ReviewService` クラスを新規作成
- アプリ起動回数をカウント(SharedPreferencesで保存)
- 30回起動時に自動的にレビューダイアログを表示
- レビューリクエスト済みフラグで重複表示を防止
- main.dartの初期化処理にレビューチェックを追加
🤖 Generated with [Claude Code](https://claude.ai/code)
詳細で分かりやすく、絵文字も適切。まるで人間が書いたようなクオリティです。
💡 工夫した点
1. 課金システムの実装 💰
サーバーサイドレシート検証
不正課金を防ぐため、Supabase Edge Functionsでサーバーサイド検証を実装しました。
なぜサーバーサイド検証が必要?
クライアント側だけの検証では、不正なレシートデータを送信されると防げません。
App Store/Google Play APIを直接叩くことで、正規の購入かどうかを確実に検証できます。
アーキテクチャ:
┌─────────────┐
│ Flutter │
│ App │
└──────┬──────┘
│
│ 1. 購入リクエスト
↓
┌──────────────────────────┐
│ StoreKit / Play Billing │
│ (各プラットフォームSDK) │
└──────┬───────────────────┘
│
│ 2. 購入完了 + Receipt/Token
↓
┌─────────────┐
│ Flutter │
│ App │
└──────┬──────┘
│
│ 3. サーバー検証リクエスト
↓
┌──────────────────────────┐
│ Supabase Edge Functions │
│ (verify-purchase) │
└──────┬───────────────────┘
│
│ 4. Store APIで検証
↓
┌──────────────────────────┐
│ Apple / Google Store API │
└──────┬───────────────────┘
│
│ 5. 検証結果
↓
┌──────────────────────────┐
│ Supabase Edge Functions │
└──────┬───────────────────┘
│
│ 6. DB保存(user_purchases)
↓
┌─────────────┐
│ Supabase │
│ Database │
└─────────────┘
Edge Function実装例:
// supabase/functions/verify-purchase/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { receipt, platform } = await req.json()
// Apple App Store API検証
if (platform === 'ios') {
const jwtToken = await generateAppleJWT()
const response = await fetch(
`https://api.storekit.itunes.apple.com/inApps/v1/transactions/${transactionId}`,
{ headers: { Authorization: `Bearer ${jwtToken}` } }
)
// 検証処理...
}
// Google Play API検証
if (platform === 'android') {
// 検証処理...
}
})
チケット制デッキ枠拡張
買い切り型の「デッキ枠拡張チケット」を実装。
1チケット = デッキ枠+15という設計で、ユーザーが必要な分だけ購入できるようにしました。
class PurchaseProvider extends ChangeNotifier {
int get maxDeckCount {
if (hasCloudStorage) return 999; // 無制限
return 15 + (deckExpansionTicketCount * 15); // チケット制
}
}
2. オフライン対応 📴
通信環境が悪い場所でもカードを閲覧できるよう、画像ダウンロード機能を実装。
class OfflineProvider extends ChangeNotifier {
Future<void> downloadAllImages() async {
for (final card in cards) {
final imageUrl = card.imageUrl;
final file = await _downloadImage(imageUrl);
await _storageService.saveImage(card.id, file);
}
}
}
3. 自動保存機能 💾
クラウド保存プラン契約者向けに、デッキ編集時の自動保存を実装。
class DeckProvider extends ChangeNotifier {
Timer? _autoSaveTimer;
void _scheduleAutoSave() {
_autoSaveTimer?.cancel();
_autoSaveTimer = Timer(Duration(seconds: 3), () {
if (_authProvider.isAutoSaveEnabled) {
_saveDeckToCloud();
}
});
}
}
4. Apple Sign-In対応 🍎
App Store審査のGuideline 4.8に対応するため、Apple Sign-Inを実装しました。
Guideline 4.8とは?
アプリ内でGoogle Sign-Inなどのサードパーティ認証を提供する場合、Apple Sign-Inも必ず提供しなければなりません。
これはAppleのプライバシー保護ポリシーの一環で、ユーザーに選択肢を与えることが目的です。
実装のポイント:
プラットフォーム別の表示制御
Android Production環境では、Apple Sign-Inボタンを非表示にして、Google Playの審査をスムーズに通過。
// settings_screen.dart
// AndroidのProduction環境ではApple Sign-Inを非表示
if (!(Platform.isAndroid && F.isProduction)) ...[
const AppleSignInButton(),
const SizedBox(height: 12),
],
const GoogleSignInButton(),
SupabaseとApple Sign-Inの統合
Supabaseは標準でApple Sign-Inをサポートしているため、追加の実装は最小限で済みました。
// apple_sign_in_button.dart
Future<void> _handleAppleSignIn() async {
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
await Supabase.instance.client.auth.signInWithIdToken(
provider: OAuthProvider.apple,
idToken: credential.identityToken!,
nonce: credential.state,
);
}
Info.plistのURL Scheme設定
Apple Sign-Inのコールバック用にURL Schemeを追加。
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.pokepoke.database</string>
</array>
</dict>
</array>
この対応により、iOS App Storeの審査を無事通過できました!
5. iPad対応でレスポンシブUI 📱
iPhone向けに開発していたUIを、iPadでも快適に使えるようレスポンシブ対応しました。
レスポンシブ対応の重要性
Flutterは単一のコードベースでiPhone/iPad両対応できますが、画面サイズが大きく異なるため、
デバイスに応じたレイアウト調整が必須です。MediaQueryを活用することで、
同じコードで異なる画面サイズに最適なUIを提供できます。
課題:
- iPhoneのグリッドレイアウト(4列)だと、iPadの大画面で余白が多すぎる
- デッキ共有画面のQRコードが小さすぎて見づらい
解決策:
デバイスサイズに応じたグリッド列数調整
// deck_share_screen.dart
Widget build(BuildContext context) {
final constraints = MediaQuery.of(context).size;
final isTablet = constraints.maxWidth >= 600; // タブレット判定
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isTablet ? 5 : 4, // iPad: 5列、iPhone: 4列
crossAxisSpacing: isTablet ? 12 : 8,
mainAxisSpacing: isTablet ? 12 : 8,
childAspectRatio: 0.7,
),
itemBuilder: (context, index) => CardTile(card: cards[index]),
);
}
デッキ共有画面の最適化
iPadでは画面を左右に分割し、QRコードと共有オプションを並べて表示。
// deck_share_screen.dart
@override
Widget build(BuildContext context) {
final isTablet = MediaQuery.of(context).size.width > 600;
if (isTablet) {
// タブレット: 横並びレイアウト
return Row(
children: [
Expanded(child: _buildQRCodeSection()),
Expanded(child: _buildShareOptionsSection()),
],
);
} else {
// スマホ: 縦並びレイアウト
return Column(
children: [
_buildQRCodeSection(),
_buildShareOptionsSection(),
],
);
}
}
結果:
- iPadでカード一覧が5列で見やすく表示
- iPhoneでは4列で表示
- デッキ共有画面でQRコードが大きく表示され、スキャンしやすくなった
- 画面サイズに応じて自動的にレイアウトが最適化される
6. MakefileでGitHub Actions利用料を節約 💰
個人開発では**GitHub Actionsの無料枠(月2,000分)**を節約することが重要です。
そこで、Makefileを活用してローカルでCI/CDを完結させる仕組みを構築しました。
GitHub Actions無料枠の制約
個人開発では月2,000分(約33時間)が無料枠。Flutter/iOSのビルドは1回10〜15分かかるため、
頻繁にビルドすると月の途中で枠を使い切ってしまうリスクがあります。
Makefileで実現したこと:
ローカルCI/CDパイプライン
# Makefile
.PHONY: ci
ci: setup test build-android-production deploy-android
@echo "✅ CI/CDパイプライン完了"
.PHONY: test
test:
@echo "🧪 テスト実行中..."
flutter test
flutter analyze
dart format --set-exit-if-changed .
.PHONY: build-android-production
build-android-production:
@echo "🔨 Android本番ビルド..."
flutter build apk --release --flavor production \
--dart-define-from-file=dart_defines_prod.json
環境別ビルドコマンド
# 開発環境
make build-android-dev
make build-ios-dev
make deploy-android-dev # DeployGateに自動デプロイ
# ステージング環境
make build-android-staging
make build-ios-staging
# 本番環境
make build-android-production
make build-ios-production
make deploy-all # 全プラットフォームデプロイ
メリット:
- GitHub Actionsを使わずローカルで完結
- ビルド時間の短縮(ローカルマシンのスペックによる)
- 環境変数の管理が簡単(dart_defines_*.json自動生成)
- コマンド一つで複雑なビルド手順を実行
実際の運用:
- 開発中: ローカルMakefileでビルド・デプロイ(無料)
- 本番リリース: Xcode Cloud(iOS)のみ利用
- GitHub Actions: データベースバックアップのみ使用
この工夫により、GitHub Actionsの利用を月200分以下に抑えることに成功しました。
7. Netlifyで静的アセットを無料配信 📦
カードデータ(JSON)や利用規約などの静的ファイルを、Netlifyを使って無料でホスティングしています。
なぜSupabase Storageではなく?
Supabase Storageは便利ですが、無料枠(1GB)を超えると課金が発生します。
カードデータJSONは頻繁に更新され、容量も増えていくため、Netlifyの無料枠(100GB/月)を活用しました。
実装構成:
-
別リポジトリで管理:
pokepoke_database_assets - Netlifyで自動デプロイ: GitHubにpushすると自動的にデプロイ
-
配信するアセット:
- カードデータJSON(全カード情報)
- プライバシーポリシー(HTML)
- 利用規約(HTML)
アプリ側の実装:
// lib/constants/urls.dart
class AppUrls {
static const String pokepokeCardsDataUrl =
'https://***.netlify.app/api/v1/pokepoke_cards.json';
static const String privacyPolicyV1 =
'https://***.netlify.app/api/v1/privacy_policy.html';
static const String termsOfServiceV1 =
'https://***.netlify.app/api/v1/terms_of_service.html';
}
メリット:
- 完全無料: Netlifyの無料枠で十分(月100GB転送量)
- CDN配信: 世界中で高速アクセス
- 自動デプロイ: GitHubにpushするだけで反映
- バージョン管理: JSONの変更履歴がGitで管理できる
- Supabase容量を節約: データベースは本当に必要なデータだけに
運用:
- 新カード追加時: JSONを更新してpush → 自動デプロイ
- アプリ側: JSONを定期的にfetchして最新データを取得
- キャッシング: ローカルに保存して高速表示
この構成により、サーバーコストをゼロに保ちながら、高速なデータ配信を実現しています。
8. データベース自動バックアップ戦略 💾
個人開発でもデータの喪失リスク対策は重要です。ユーザーのデッキデータやサブスクリプション情報を守るため、GitHub Actionsを使った自動バックアップシステムを構築しました。
なぜ自動バックアップが必要?
Supabaseは信頼性の高いサービスですが、万が一のサーバー障害や人為的ミスによるデータ削除に備え、独自のバックアップ戦略が必要です。手動バックアップは忘れがちなため、完全自動化が理想的です。
バックアップの構成:
GitHub Actionsで毎日自動実行
# .github/workflows/database_backup.yml
name: Daily Database Backup
on:
schedule:
- cron: '0 17 * * *' # 17:00 UTC = 02:00 JST(深夜に自動実行)
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Backup Supabase Database
run: |
# Supabase Transaction Poolerに接続
pg_dump $DATABASE_URL > backup.sql
- name: Upload to Google Cloud Storage
run: |
# バックアップファイルをGCSにアップロード
gsutil cp backup.sql gs://pokepoke-backup-gcs/database-backups/$(date +%Y%m%d_%H%M%S)/
マルチクラウド戦略
バックアップは2箇所に保存し、冗長性を確保:
| 保存先 | 保存期間 | 容量制限 | コスト |
|---|---|---|---|
| Google Cloud Storage | 3ヶ月(自動削除) | 無制限 | $0.02/GB/月 |
| GitHub Artifacts | 30日(自動削除) | 500MB | 無料 |
バックアップ構造:
gs://pokepoke-backup-gcs/
└── database-backups/
├── 20250915_020000/
│ ├── data-backup.sql # データのみ
│ ├── schema-backup.sql # スキーマのみ
│ └── manifest.json # メタデータ
├── 20250916_020000/
└── 20250917_020000/
自動クリーンアップ:
# 3ヶ月以上前のバックアップを自動削除
gsutil -m rm -r gs://pokepoke-backup-gcs/database-backups/$(date -d '3 months ago' +%Y%m%d)*/
コスト分析:
| 項目 | 使用量 | コスト/月 |
|---|---|---|
| GitHub Actions | 毎日5分 × 30日 = 150分 | $0(無料枠内) |
| GCS ストレージ | 約1GB × 90日分 | $0.02 |
| GCS ネットワーク | ほぼ0(アップロードのみ) | $0 |
| 合計 | - | 約$0.02/月 |
復元手順:
# 最新バックアップから復元
gsutil cp gs://pokepoke-backup-gcs/database-backups/latest/data-backup.sql .
psql $DATABASE_URL < data-backup.sql
メリット:
- 完全自動化: 人手不要で毎日バックアップ実行
- 低コスト: 月額0.02ドル(約3円)で3ヶ月分保存
- マルチクラウド: GCS障害時もGitHub Artifactsから復元可能
- バージョン管理: 過去90日分のデータ履歴を保持
- 安心感: ユーザーデータ保護の責任を果たせる
この仕組みにより、万が一のデータ喪失リスクをほぼゼロにできました。個人開発でも、ユーザーの信頼を得るためにはデータ保護が不可欠です。
9. Flavor管理 🍦
開発・ステージング・本番環境をflutter_flavorizrで管理。
# flavorizr.yaml
flavors:
dev:
app:
name: "PokePokeDB Dev"
android:
applicationId: "com.pokepoke.database.dev"
ios:
bundleId: "com.pokepoke.database.dev"
production:
app:
name: "PokePoke Database"
android:
applicationId: "com.pokepoke.database"
ios:
bundleId: "com.pokepoke.database"
😓 苦労した点
1. Google Sign-In 400エラー 😓
初期実装時、Google Sign-Inで400エラーが頻発。
原因:
- Supabase側で
nonceチェックが有効だったが、Flutterから正しく送信できていなかった
解決策:
- Supabaseの設定で「Skip nonce checks」を有効化
- デバッグログを追加して問題を特定
// 当初のデバッグコード(最終的には削除)
AppLogger.logInfo('🔐 ID Token Claims Debug:');
AppLogger.logInfo(' nonce: ${idTokenClaims['nonce']}');
AppLogger.logInfo(' iss: ${idTokenClaims['iss']}');
2. In App Purchase Response Code 6 エラー 💳
課金機能実装時、Response Code 6エラーが頻発。
原因:
- Google Play Consoleでの商品登録が不完全
- アプリ署名(SHA-1)の設定ミス
- Bundle IDの不一致
解決策:
// 詳細ログを追加してエラー原因を特定
DebugLogger.log('=== 課金エラー診断 ===');
DebugLogger.log('Bundle ID: ${F.bundleId}');
DebugLogger.log('商品ID: $productId');
DebugLogger.log('接続状態: ${await InAppPurchase.instance.isAvailable()}');
最終的には、Google Play Consoleの設定を修正して解決しました。
3. TestFlightの課金テストで消耗した3日間 🍎💸
iOS版の課金機能テストで、最も時間を消耗したのがこの問題でした。
現象:
デッキ枠拡張チケット(買い切り型)を何度購入しても、枚数が増えない。
初回購入時のみカウントが+1されるが、2回目以降は反応なし。
当初の仮説:
- レシート検証ロジックのバグ?
- Supabaseへの保存処理が失敗している?
- 購入完了コールバックが正しく動作していない?
デバッグに費やした時間:
約3日間、以下のような調査を繰り返しました:
// 追加したデバッグログ(抜粋)
DebugLogger.log('🎫 購入開始: $productId');
DebugLogger.log('📝 レシート: ${purchase.verificationData.serverVerificationData}');
DebugLogger.log('✅ 購入完了: transactionId=${purchase.purchaseID}');
DebugLogger.log('💾 Supabase保存前のチケット数: $currentTicketCount');
DebugLogger.log('💾 Supabase保存後のチケット数: $newTicketCount');
// App Store Server APIのレスポンスも詳細ログ
DebugLogger.log('🍎 Apple API Response:');
DebugLogger.log(' transactionId: ${response.transactionId}');
DebugLogger.log(' originalTransactionId: ${response.originalTransactionId}');
DebugLogger.log(' productId: ${response.productId}');
すべてのログが正常に見えるのに、なぜかチケット数が増えない...
真の原因:
調査を重ねた結果、衝撃の事実が判明。
TestFlight Sandbox環境の仕様
TestFlightでは、同じ商品IDの買い切りアイテムを購入すると、常に同一のoriginalTransactionIdが返されるという仕様がありました。
つまり:
- 1回目の購入:
originalTransactionId = "ABC123" - 2回目の購入:
originalTransactionId = "ABC123"← 同じ! - 3回目の購入:
originalTransactionId = "ABC123"← また同じ!
重複チェックでoriginalTransactionIdをユニークキーとして使っていたため、2回目以降の購入が「重複」として扱われ、チケット数が増えなかったのです。
| 環境 | トランザクションID | データベースへのINSERT | チケット数 |
|---|---|---|---|
| TestFlight | 常に同じID | 1回目のみ | 1個のまま ❌ |
| 本番環境 | 毎回新しいID | 毎回実行 | 毎回+1される ✅ |
// 問題のあったコード
final existingPurchase = await supabase
.from('purchases')
.select()
.eq('original_transaction_id', originalTransactionId)
.maybeSingle();
if (existingPurchase != null) {
// 重複として扱われてしまう!
return;
}
解決策:
TestFlight環境ではtransactionId(毎回異なる)を使用し、本番環境ではoriginalTransactionIdを使用するよう分岐。
final uniqueKey = F.isProduction
? originalTransactionId // 本番: originalTransactionId
: transactionId; // TestFlight: transactionId
final existingPurchase = await supabase
.from('purchases')
.select()
.eq('transaction_id', uniqueKey)
.maybeSingle();
学んだこと:
- TestFlightとApp Store本番環境では、課金の動作が異なる
- 公式ドキュメントには「Sandbox環境では同じoriginalTransactionIdが返される」と記載されていた(見落としていた...)
- 本番環境でしかテストできない仕様もあるため、早めにリリースしてテストすることが重要
この問題に3日間も費やしましたが、結果的にTestFlight Sandboxの課金仕様について詳しくなったので、今後の開発に活かせそうです...😅
参考: Testing in-app purchases - Apple Developer
4. Xcode Cloud pod install エラー ☁️
Xcode Cloudでビルド時に以下のエラー:
/Users/local/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework must exist.
原因:
- Flutter EngineのiOS用キャッシュがダウンロードされていなかった
解決策:
ci_post_clone.shに以下を追加:
# Flutter precache実行(iOS用キャッシュのダウンロード)
flutter precache --ios
これで無事ビルド成功!🎉
5. App Store Connect審査で3回リジェクト 🍎😭
iOS版のリリースで最も精神的に消耗したのが、App Store Connectの審査でした。
結果的に3回リジェクトされ、審査チームと何度もやり取りすることになりました。
1回目のリジェクト: Guideline 4.8 - Sign in with Apple
リジェクト理由:
Your app uses a third-party login service (Google Sign-In), but does not offer Sign in with Apple.
Guideline 4.8 - Design - Sign in with Apple
Apps that exclusively use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user's primary account with the app must also offer Sign in with Apple as an equivalent option.
対応:
Apple Sign-Inを急遽実装。SupabaseがApple Sign-Inをサポートしていたため、1日で実装完了。
// Apple Sign-Inボタンを追加
const AppleSignInButton(),
const SizedBox(height: 12),
const GoogleSignInButton(),
2回目のリジェクト: Guideline 2.1 - Performance - App Completeness
リジェクト理由:
We were unable to complete the review of your app because one or more of your in-app purchase products have not been submitted for review.
Next Steps:
To resolve this issue, please be sure to take the following steps:
- Ensure that you have created your in-app purchase products in App Store Connect.
- Ensure that all in-app purchase products are submitted with the app.
課金アイテム(デッキ枠拡張チケット、クラウド保存プラン)を作成していましたが、「審査と一緒に提出」にチェックを入れ忘れていたことが原因でした。
対応:
App Store Connectで課金アイテムの「審査と一緒に提出」にチェックを入れて再提出。
3回目のリジェクト: Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage
リジェクト理由:
Your app collects user data (email address, name, and purchase history), but does not have the required privacy information in App Store Connect.
Next Steps:
To resolve this issue, please update your app's privacy information in App Store Connect to include:
- Data types collected
- Purpose of data collection
- Whether data is linked to the user
これが最も面倒でした。App Store Connectの「App Privacy」セクションで、以下を詳細に記載する必要がありました:
収集するデータ:
- メールアドレス(認証目的)
- 名前(ユーザー表示名)
- 購入履歴(課金管理)
- デッキデータ(クラウド同期)
データの用途:
- アプリ機能のため
- 分析のため
- プロダクト改善のため
データのリンク:
- ユーザーIDとリンクされている
すべて英語で入力する必要があり、Claude Code翻訳を駆使しながら1時間ほどかけて記載しました。
4回目の申請: ついに承認!🎉
3回のリジェクトを経て、4回目の申請でようやく承認されました。
審査期間の推移:
- 1回目: 2日で審査完了 → リジェクト
- 2回目: 1日で審査完了 → リジェクト
- 3回目: 3日で審査完了 → リジェクト
- 4回目: 2日で審査完了 → 承認!
合計審査期間: 約8日間
学んだこと:
- Apple Sign-Inは必須(Google Sign-Inを使う場合)
- 課金アイテムは「審査と一緒に提出」を忘れずに
- App Privacyセクションは詳細に記載する
- リジェクト理由は丁寧に読んで、指摘された箇所を正確に修正する
- 審査チームとのやり取りは英語で丁寧に(自動翻訳でOK)
App Store審査は厳格ですが、指摘された内容を正確に修正すれば必ず通ります。
リジェクトされても諦めず、粘り強く対応することが大切です!💪
🎯 まとめ
学んだこと 📚
-
個人開発はスピードが命
- Claude Code CLIのようなツールを活用することで開発速度が劇的に向上
- 調べる時間と実装の時間を減らし、設計に集中できる
-
課金システムはサーバーサイド検証が必須
- クライアント側だけの検証では不正を防げない
- Supabase Edge Functionsで手軽にサーバーレス実装
-
CI/CDは早めに構築すべき
- 手動ビルド・デプロイは時間の無駄
- Xcode Cloudで自動化して開発に集中
-
ログは多すぎるくらいでちょうどいい
- 本番環境でのバグ特定に不可欠
- Firebase Crashlyticsと組み合わせて効果的
今後の展望 🚀
- 他人のデッキ閲覧機能(フォロー・いいね)
- プッシュ通知で新カード情報配信
- ユーザー間のデッキランキング
最後に
個人開発は大変ですが、自分のアイデアを形にできる喜びはとても糧になりました。
そして、Claude Code CLIのような強力なツールがあれば、一人でもリリースまで漕ぎ着けます!
ぜひ皆さんも、思い立ったが吉日で個人開発にチャレンジしてみてください!💪
ちなみに、この記事もClaude Code CLIに書かせました!笑
リンク集
- App Store
- Google Play Store
- Claude Code CLI
- Supabase
- Flutter









