subnote
WIP(現在進行中)
Work In Progress
このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。
現在の状況
フェーズ: Phase 3(Android)RevenueCat 連携完了 — 次は署名キー→AAB→内部テスト(2026-07-23 14:49 JST)。iOS は配信地域未設定事故を修正し公開処理中
今どこまで進んだか:
- Android パリティ実装コミット済み(
d90c3cf、レビュー済み・独立ビルド検証済み・versionName 1.0.0)+ RC 公開キー設定済み(6a4e880、goog_qyoLEgWvXvnPUyQhkXVvMquffIy) - RevenueCat Play 連携一式完了: RC アプリ
appa1d98c25d3(me.subnote.android)/ GCP SArevenuecat-play@subnote-ab213.iam.gserviceaccount.com(JSON キーは RC アップロード済み・Pub/Sub 管理者ロール付与済み)/ Play Android Developer API + Reporting API 有効 / Play Console に SA 招待済み(3権限)/ RTDN 疎通確認済み(トピックprojects/subnote-ab213/topics/Play-Store-Notifications、Play 側にも登録、テスト通知が RC に到達)- RC の「Credentials need attention」は権限伝播待ち(最大36h、初回テスト購入で実検証)。放置で OK
- iOS「App Store から削除済み」事故を検出・修正: 原因は appAvailability(配信地域)未設定(API 提出フローの落とし穴、DEVLOG 14:49 の教訓参照)。全175地域で設定 → 「配信可能を処理中」(最大24時間)。今朝からストアに出ていなかったのはこれが原因で、反映ラグではなかった
次にやること:
- iOS ストア公開の確認(数時間後、
apps.apple.com/app/id6790385156)→ 実機 DL 動作確認 - Android: upload 署名キー作成 → release AAB ビルド → Play 内部テストへアップロード(キーストアは git 管理外、→ Play Billing 商品作成が解放される)
- Play Billing 商品3種作成 → RC 紐付け → 内部テスト購入 E2E(想定構成: 定期購入
premium(ベースプラン monthly/annual)+ 買い切りpremium_lifetime、価格は iOS と同じ 月¥150/年¥1,200/買い切り¥2,500。※商品構成は作成時に最終決定) - ストア掲載整備: 説明文(iOS への言及を入れない=Apple 2.3.10 の逆パターン)/ スクショ(iOS 素材流用、store-screenshots スキル)/ Data safety / コンテンツレーティング
- You: Play お受け取り方法(銀行口座)追加(収益 ¥100 到達までで OK)/ Google・Apple ログインの実機 E2E
継続メモ:
ASC API 提出フローの教訓: 提出前に
GET /v1/apps/{id}/appAvailabilityV2が 200 を返すか確認する(404 なら配信先ゼロで公開時に「削除済み」になる)ASC メタデータ編集: asc-subnote MCP は GET 専用。説明文などの PATCH や reviewSubmission 作成は
~/.config/asc/AuthKey_8M2SWG78S6.p8(issuer94a845e5…/ key8M2SWG78S6)で ES256 署名(dsaEncoding:'ieee-p1363')した JWT を作り api.appstoreconnect.apple.com を直接叩く。リジェクト具体理由は公開 API に出ず Apple 2通目メール / Resolution Center にあるFirebase/GA4: プロジェクト
subnote-ab213、iOS は GoogleService-Info.plist 同梱で有効(plist はコミット済み・公開前提のクライアントキーのみ)。Web の測定 ID は layout.tsx にフォールバック埋め込み(Build variableNEXT_PUBLIC_GA_MEASUREMENT_IDで上書き・空文字で無効化可)。Crashlytics の dSYM はアーカイブ時に自動アップロード(Xcode の「Upload Symbols Failed」警告は無害)Send Email Hook: 受け口
web/app/api/auth/send-email/route.ts+web/lib/auth-emails.ts。secret は Worker secretSEND_EMAIL_HOOK_SECRET。Web/アプリ判別は redirect_to の origin + pathname/auth/callback完全一致Web スクショ撮影レシピ: ローカル Supabase に demo@subnote.me(
5becac41…、本番デモアカ同等データ)シード済み。headless Chrome + CDP + password grant で自作 Cookie 注入(PKCE リンクは別ブラウザで開けない)。次回もこの手順でASC スクショ現況: iPhone 7枚(4枚目=Web同期訴求)/ iPad 4枚(3枚目=同期)。生成は
node docs/asc-screenshots/generator/generate.mjs(Codex は設定のみで生成を忘れることがある→サイズ比較で不変検証)ASC 提出フローの要点: 新 UI は提出物(Review Submission)方式。サブスクはグループと配下の商品を個別に「審査用に追加」。App プライバシー・価格・配信権は API/ブラウザ操作の合わせ技(App プライバシーは公開 API なし)
SiwA ネイティブの primary 構成(重要):
me.subnote.appは旧me.subnote.Subnoteに group。旧 App ID は絶対に削除しないRC の app_user_id は小文字 UUID に統一(Android 実装時も同じ規約)
Entitlement 識別子
premiumは web/lib/entitlements.ts の定数と一致必須 / 手動付与の lifetime は webhook 同期で消える(消えたら再付与 SQL)iOS アプリ内から Web 決済ページへ誘導しない(anti-steering)
ADMIN_EMAILS は
ichirokisanuki@gmail.comのみ(/admin ガードは常設)Apple client secret 2方式併存(iOS=Worker secret で都度署名 / Web=半年更新、期限 2027-01-09、dev-timer #32/#33)
旧継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール量監視 / Google OAuth クライアント法人化)は残存
過去のWIPアーカイブ
(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)
2026-07-23 13:14〜の状況のスナップショット(2026-07-23 14:49 にアーカイブ)
フェーズ: Phase 3(Android)始動 — Codex 実装完了・レビュー待ち + Play Console 土台完了。iOS 1.0.0 は公開済み(ストア反映確認は継続)
- Android パリティ依頼書作成(docs/codex-task-android-parity.md)→ Codex 実装完了・未コミット(レビュー待ち、RC キーはプレースホルダ)
- Play Console 土台: アプリ作成 / お支払いプロファイル RAMANU LLC / アカウントグループ + 関連申告なし / 15% 手数料登録
- Firebase Android 登録 + google-services.json 配置(未コミット)
- 次にやること(当時): 実装レビュー→コミット / RC Play アプリ + goog_ キー / AAB 内部テスト→Play Billing 商品 / 銀行口座 / iOS ストア反映確認
2026-07-23 10:57〜の状況のスナップショット(2026-07-23 13:14 にアーカイブ)
フェーズ: Phase 2(iOS)審査通過 → 公開ボタン押下・正式リリース(2026-07-23 10:57 JST)
今どこまで進んだか:
- iOS 1.0.0 審査通過: 2回のリジェクト(1回目=3.1.2 規約リンク欠落 / 2回目=2.3.10 説明文の Android 言及、いずれもメタデータ修正で対応)を潰した再提出分が承認された
- 公開ボタン押下(正式リリース): App Store Connect で手動リリースを実行。1.0.0 初回リリースは段階的リリース対象外=全ユーザー即時公開。ストア反映(検索・DL 可)まで数十分〜数時間のラグ想定
- Crashlytics 疎通確認済み: Firebase コンソールが「アプリが検出されクラッシュを待機しています」=SDK 疎通済み・実クラッシュ待ちの正常状態。bundle ID は Xcode / GoogleService-Info.plist / Firebase コンソールの3箇所すべて
me.subnote.appで一致、GOOGLE_APP_ID も iOS として登録済みを検証。dSYM は Release ビルドのみアップロード=本番配信ビルドのクラッシュはシンボル化される
次にやること(当時):
- ストア反映を確認 → 実機で本番配信ビルドを DL→起動して動作確認
- 公開後: カタログ画面スクショの差し替え
- 告知 / Small Business Program 申請
- GA4 定点観測の立ち上げ
- 数日運用して致命的な問題がなければ iOS アプリ内レビュー導線に着手
2026-07-22 09:49〜の状況のスナップショット(2026-07-23 10:57 にアーカイブ)
フェーズ: Phase 2(iOS)審査リジェクト2回対応済み・再提出完了(WAITING_FOR_REVIEW、2026-07-22 09:49 JST)
今どこまで進んだか(審査は1件ずつ潰す構造。1つ直すと次が出る):
- 1回目リジェクト(3.1.2・自動チェック、解消済み): 「自動更新サブスクに利用規約(EULA)の機能リンクがメタデータに無い」→ App説明文末尾に利用規約 https://subnote.me/terms + プライバシーポリシー https://subnote.me/privacy を追記(自前 /terms 採用、DECISIONS 2026-07-22)
- 2回目リジェクト(2.3.10・人間レビュアー、今回対応): 「説明文に他プラットフォーム(Android)への言及」→ 説明文「■ Web版とクラウド同期」末尾の
Android版も準備中です。を削除(Web版の言及は維持)。Android 出現数 1→0 を検証 - 修正方式: いずれもメタデータのみ・再ビルド不要(バイナリ=ビルド3 は通過)。asc-subnote MCP は GET 専用のため P8 ES256 署名で
PATCH /v1/appStoreVersionLocalizations/{id}を直接実行(200) - 再提出完了: 同一 reviewSubmission を再利用し5項目(アプリ 1.0.0 (3) + サブスクグループ + 月額/年額/買い切り)束ね直しで再提出 →
WAITING_FOR_REVIEW。公開方法 AFTER_APPROVAL のまま
次にやること:
- 審査結果待ち(再提出済み)。承認 → 自動公開 / 再リジェクト → 内容確認して対応。ステータスは asc-subnote MCP(
appStoreVersions.appStoreState/reviewSubmissions.state)で監視可能。理由本文は公開 API に出ないので Apple 2通目メール / Resolution Center を見る - 公開後: カタログ画面スクショの差し替え(新導線版の撮り直し。審査中はメタデータロックのため公開後 or 1.0.1 で)
- 告知(ASO・告知文面は promo-expert 壁打ち)/ Small Business Program 申請
- GA4 定点観測の立ち上げ(「カタログ収載済みサービスの手入力率」等。Subnote 用 ga4 MCP をプロジェクト .mcp.json に足すと Claude Code から見られる)
2026-07-20 03:57〜の状況のスナップショット(2026-07-22 05:05 にアーカイブ)
フェーズ: Phase 2(iOS)審査提出完了(7/20 未明、WAITING_FOR_REVIEW)+ Web 正式公開(ベータガード撤去済み)
今どこまで進んだか:
- iOS 1.0.0 (3) を審査提出: 提出物(Review Submission)に5項目=アプリ + サブスクグループ + 月額/年額/買い切り。公開方法 AFTER_APPROVAL(承認と同時自動公開)、審査は最大48時間表示。提出時に著作権(2026 Ramanu LLC)/ 配信権(USES_THIRD_PARTY_CONTENT)/ 価格(無料・JPN基準)を API で補完
- 1.0.0 (3) に同梱: 新アイコン(マーク20px下げ)/ カタログ導線再設計(検索上部固定・手入力降格)/ ホームヘッダーのスクロール追従化 / iPhone 縦固定 / Firebase Analytics / Crashlytics
- Web 正式公開: middleware のベータガード撤去 push 済み = 誰でもサインアップ可。GA4 稼働(本番ビルドのみ)
- 認証メール出し分け本番稼働: Send Email Hook(Web=リンクのみ / アプリ=コードのみ)。Codex レビュー3ラウンドで出荷可、本番 E2E 済み。障害時はSupabase ダッシュボードで Hook を無効化すれば従来方式へ即復帰
- App プライバシーは8データタイプ(+製品の操作 / デバイスID / クラッシュデータ、いずれも非関連付け・トラッキングなし)
- QA 掃除完了: RC 大文字 customer・テストユーザー
0dc0a587…削除。goda のテストサブスク / lifetime は QA 用に残す(DECISIONS 2026-07-20)。デモアカ+ascも保持
次にやること:
- 審査結果待ち(結果はメール)。承認 → 自動公開 / リジェクト → 内容確認して対応
- 公開後: カタログ画面スクショの差し替え(新導線版の撮り直し。審査中はメタデータロックのため公開後 or 1.0.1 で)
- 告知(ASO・告知文面は promo-expert 壁打ち)/ Small Business Program 申請
- GA4 定点観測の立ち上げ(「カタログ収載済みサービスの手入力率」等。Subnote 用 ga4 MCP をプロジェクト .mcp.json に足すと Claude Code から見られる)
継続メモ:
- Firebase/GA4: プロジェクト
subnote-ab213、iOS は GoogleService-Info.plist 同梱で有効(plist はコミット済み・公開前提のクライアントキーのみ)。Web の測定 ID は layout.tsx にフォールバック埋め込み(Build variableNEXT_PUBLIC_GA_MEASUREMENT_IDで上書き・空文字で無効化可)。Crashlytics の dSYM はアーカイブ時に自動アップロード(Xcode の「Upload Symbols Failed」警告は無害) - Send Email Hook: 受け口
web/app/api/auth/send-email/route.ts+web/lib/auth-emails.ts。secret は Worker secretSEND_EMAIL_HOOK_SECRET。Web/アプリ判別は redirect_to の origin + pathname/auth/callback完全一致 - Web スクショ撮影レシピ: ローカル Supabase に demo@subnote.me(
5becac41…、本番デモアカ同等データ)シード済み。headless Chrome + CDP + password grant で自作 Cookie 注入(PKCE リンクは別ブラウザで開けない)。次回もこの手順で - ASC スクショ現況: iPhone 7枚(4枚目=Web同期訴求)/ iPad 4枚(3枚目=同期)。生成は
node docs/asc-screenshots/generator/generate.mjs(Codex は設定のみで生成を忘れることがある→サイズ比較で不変検証) - ASC 提出フローの要点: 新 UI は提出物(Review Submission)方式。サブスクはグループと配下の商品を個別に「審査用に追加」。App プライバシー・価格・配信権は API/ブラウザ操作の合わせ技(App プライバシーは公開 API なし)
- SiwA ネイティブの primary 構成(重要):
me.subnote.appは旧me.subnote.Subnoteに group。旧 App ID は絶対に削除しない - RC の app_user_id は小文字 UUID に統一(Android 実装時も同じ規約)
- Entitlement 識別子
premiumは web/lib/entitlements.ts の定数と一致必須 / 手動付与の lifetime は webhook 同期で消える(消えたら再付与 SQL) - iOS アプリ内から Web 決済ページへ誘導しない(anti-steering)
- ADMIN_EMAILS は
ichirokisanuki@gmail.comのみ(/admin ガードは常設) - Apple client secret 2方式併存(iOS=Worker secret で都度署名 / Web=半年更新、期限 2027-01-09、dev-timer #32/#33)
- 旧継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール量監視 / Google OAuth クライアント法人化)は残存
2026-07-17 03:36〜の状況のスナップショット(2026-07-20 03:57 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備 — 審査提出可能(7/17 未明)。スクショ v2(マーケ加工版)が完成し ASC アップロード完了。提出物はすべて揃い、残るは ASC UI での提出操作のみ(IAP 3商品をバージョン 1.0 に追加 → 審査用に追加)
今どこまで進んだか:
- スクショ v2 完成 + 全9枚アップロード(今回 7/17 未明、
26f1406): iPhone 6.9 ×6(①ホーム合計ヒーロー ②カタログ ③一覧$/¥ ④月次チャート ⑤プラン選択 ⑥ダーク)+ iPad 13 ×3(ホーム/カタログ/ダーク)。全件 COMPLETE。v1 は API で削除済み。スタイル=グレーグラデ背景 + 白コピー + 黒ベゼル端末の下端見切り(経緯は DECISIONS 2026-07-17) - 制作体制(分業): raw 撮影・検品・ASC アップロード = Claude Code / 仕上げ = Codex(docs/asc-screenshots/generator/、再生成は
node docs/asc-screenshots/generator/generate.mjs)。依頼書 = docs/codex-task-asc-screenshots.md(「今回のタスク」を差し替えて1枚ずつ往復する方式) - プラン選択画面にサービスアイコン追加(今回): 画面の寂しさ対策でアプリ本体を改修(
ServiceIconにサイズ指定追加 + PlanPickerView にアイコンヘッダー)。シミュレータ検証済み - デモデータの現状(撮り直し時に前提となる): ChatGPT = $20 USD(多通貨訴求用)/
monthly_snapshotsに 2025-08〜2026-06 の11ヶ月バックフィル済み(ヒーローに前月比 ↑3.4% が出る)/ もうすぐ更新は YouTube 7/18・Netflix 7/20(3日以内条件、日が経つと外れるので撮り直し時は要調整) - (7/16 夜までに完了: IAP 3商品 READY_TO_SUBMIT / App Review 情報 / 輸出コンプライアンス / App プライバシー / 有料 App 契約有効。詳細はアーカイブ参照)
次にやること:
- 審査提出(You の ASC UI 操作): バージョン 1.0 ページの「アプリ内課金とサブスクリプション」に READY_TO_SUBMIT の3商品を追加 → 「審査用に追加」で提出(初回 IAP はアプリ本体と同時審査が必須)。公開方法は AFTER_APPROVAL のまま=承認と同時公開。手動公開にするなら提出前に変更
- QA データ掃除: 本番 DB のテストユーザー
0dc0a587…(+ 登録サブスク)/ goda.takeshichan2 の本番テストサブスク / lifetime entitlement(Xcode テスト購入由来)/ RC の大文字 customer923CE947-9D12-4079-ACCA-07FEDD4596D0。※デモアカichirokisanuki+asc@gmail.comは撮り直し用に保持(掃除対象外。$20 ChatGPT・スナップショットもそのまま) - 掃除(任意): 正式公開判断(Web ベータガード撤去 + 告知)/ 手入力トリアージの本番確認 / ~/Downloads の
AuthKey_9MN99J7VFW.p8削除
継続メモ:
- ASC スクショ運用: 必須サイズは 6.9インチ(1320×2868、displayType
APP_IPHONE_67)と iPad 13(2064×2752、APP_IPAD_PRO_3GEN_129)。6.5 は「6.9 が無い場合のみ必須」= ASC 空状態 UI が 6.5 枠を出すのは罠。iPad 13 セットはccb1b77c…新規作成済み。filter[screenshotDisplayType]は効かない(全件取得して判定)。差し替えはDELETE /v1/appScreenshots/{id}→ 再アップロード。現行ビルドはTARGETED_DEVICE_FAMILY=1,2,7のため iPad 枠必須 - スクショ v2 の再生成: raw を差し替えて
node docs/asc-screenshots/generator/generate.mjs(要ローカル Chrome + ImageMagick)。ImageMagick の PNG はメタデータで md5 が毎回変わるので、不変検収はファイルサイズ + 設定差分で行う。月次チャートはmonthly_snapshotsが2件以上ないとセクションごと非表示 - シミュレータ撮影レシピ: ステータスバーは
simctl status_bar override --time 9:41 --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState discharging --batteryLevel 100(cellular 指定を省くと「••••」の圏外風になる)/ AXe の超低速・短距離スワイプ(〜45pt)は認識されない。60〜70pt は慣性で大きく飛ぶ → スクロール中間位置の精密狙いは困難 / 日本語ハード IME はAppleKeyboardsから ja を消しても残る →simctl pbcopy+ 長押しペーストで迂回 / ワードマークアニメで snapshot 期限切れ頻発 → bundled AXe(~/.npm/_npx/…/xcodebuildmcp/bundled/axe)で座標 tap・swipe / iPad は 26.5 ランタイムの iPad Pro 13(096E0C11…)を使う(26.3 の同名機はアプリの最低 OS 未満で install 不可) - Sandbox は有料 App 契約が有効なら実 ASC 商品を返す(未サインインのシミュレータは US ストアフロント = USD 表示。¥ で撮るなら Xcode ▶ Run のローカル StoreKit 設定)
- SiwA ネイティブの primary 構成(重要):
me.subnote.appは旧me.subnote.Subnoteに group(RELATED_APP_CONSENT)。Web の Services IDme.subnote.webも Primary App ID=旧me.subnote.Subnote。旧 App IDme.subnote.Subnoteは SiwA グループの primary なので絶対に削除しない(削除すると Web/iOS 全 SiwA ユーザーの identity が壊れる) - App ID の SiwA capability の OFF→ON は非破壊ではない(保存済み構成リセット + プロファイル無効化)。触るなら再ビルド・再署名・再インストール前提。App ID 設定変更後は必ずプロファイル再生成
- StoreKit ローカル設定は Xcode の ▶ Run でしか有効化されない(CLI・ホーム画面起動は Sandbox 接続)。確認手段は購入シートの
[Environment: Xcode]表示。Xcode 環境の月額は加速時計で約30秒ごとに更新 → webhook 連打(安定検証は Manage Transactions でトランザクション削除 → 買い切り購入) - RC は Xcode 環境の購入も受理して webhook まで飛ばす = 有料 App 契約なしで課金パイプラインのフル検証が可能
- RC の app_user_id は小文字 UUID に統一(DECISIONS 2026-07-16)。Android の RevenueCat 実装時も同じ規約に従う
- 実機ビルドの署名: development は ASC API キー(App Manager、
8M2SWG78S6を~/.config/asc/)で cloud signing 可 / distribution(ASC アップロード)は Xcode サインイン済みアカウントのセッション経由で行う - アカウント削除 API は Bearer 認証のみ(Cookie を見ない = CSRF 構造的防止)。iOS 以外から使うときも同方式で
- Web の SiwA(Services ID
me.subnote.web)ユーザーの Apple トークンは失効できない(Supabase が内部保持し外部に出さない、既知の限界)。iOS ネイティブ分のみ失効可能 - Apple client secret は2方式併存: iOS 失効用は
APPLE_SIGNIN_PRIVATE_KEY(Worker secret、.p8 から都度署名 = 期限管理不要)/ Supabase ダッシュボード用の Web 分は従来通り半年更新(期限 2027-01-09、dev-timer #32/#33 で監視) - RevenueCat キーは write 権限つきに移行済み(旧「Subnote Cloudflare」= 読み取り専用は Revoke 済み)。ローカル
.env.development.localも新キー - ASC MCP は 2 本立て: Subnote の作業は
asc-subnote(プロジェクト.mcp.json、ramanu アカウント)。ユーザーレベルのapp-store-connectは旧 Thomsons 別アカウント - Supabase のダッシュボード管理作業は info@ramanu.co.jp(Owner)で行う。再生成コマンド:
node scripts/gen-apple-secret.mjs --key <p8> --kid 9MN99J7VFW --team 9HZW92277N --client-id me.subnote.web - 本番の SUPABASE_SERVICE_ROLE_KEY は Worker secret のみ(ローカルの
.env.*.localは 127.0.0.1 のローカル Supabase を指す)。本番ユーザー確認は Supabase ダッシュボードの Auth Users で行う - ADMIN_EMAILS(Worker secret)は
ichirokisanuki@gmail.comのみに復元済み(7/16 の統合検証で一時的にテスト用リレーメールを追加 → 検証後除去)。middleware にバンドルされるためwrangler secret put単独では本番反映されない可能性あり(再デプロイ前提)。正式公開時に見直し - Entitlement 識別子
premiumは web/lib/entitlements.ts の定数と一致必須。無料上限変更時は migration のfree_limitとFREE_SUBSCRIPTION_LIMITを両方揃える - 手動付与の lifetime は webhook 同期で上書きされて消える。消えたら lifetime 付与 SQL を再実行
- iOS アプリ内から Web 決済ページへ誘導しない(anti-steering)/ webhook・sk_ キーはパスワードマネージャ管理
- 旧継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール量監視 / Cloudflare リカバリーコード txt 削除 / Google OAuth クライアント法人化)は残存
2026-07-16 22:43〜の状況のスナップショット(2026-07-17 03:36 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備 — 審査提出直前。提出物一式(スクショ v1 / IAP 3商品 READY_TO_SUBMIT / App Review 情報 / 輸出コンプライアンス / App プライバシー / 有料 App 契約)が ASC に揃い、提出はスクショ仕上げ(別セッション)待ちで保留
要点:
- スクショ v1(素のシミュレータ撮影、iPhone 6.9×4 + iPad 13×3)を API アップロード済みだった → 7/17 未明のセッションで「v1 は全然ダメ」と判断し全面作り直し(v2)。v1 は ASC からも削除
- IAP 3商品 READY_TO_SUBMIT / App Review 情報 / 輸出コンプライアンス / 有料 App 契約有効化は完了済み(v2 セッションへそのまま引き継ぎ)
- 当時の次: スクショ仕上げ → 審査提出 → QA データ掃除(審査提出と掃除は新しい「現在の状況」に引き継ぎ)
- (継続メモは新しい「現在の状況」に更新版を記載)
2026-07-16 19:20〜の状況のスナップショット(2026-07-16 22:43 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備。SiwA ネイティブ修復完了 + Web/iOS 統合実証 + IAP フルパイプライン検証済み + ASC 初ビルド 1.0 (1) VALID。残るは審査提出準備(スクショ + IAP メタデータ)と QA データ掃除
要点:
- ヘッダーワードマークの文字ジャンプアニメ(Codex 分業実装、
f837fb4、iOS 18TextRenderer) - SiwA ネイティブ修復(新 App ID を旧 primary
me.subnote.Subnoteに group)+ Web/iOS が同一 Supabase ユーザーに統合されることを実証 - IAP 実機検証フルコンプ(7/15夜)/ ASC 初ビルド 1.0 (1) アップロード済み(VALID)
- 当時の次: 審査提出準備(ペイウォールスクショ → IAP 商品に添付 → 同時提出)/ QA データ掃除 / 有料 App 契約は「未着手 = ブロッカー」と記載していた(→ 22:43 セッション中に合意・有効化を確認し解消)
- (継続メモは新しい「現在の状況」に引き継ぎ済み)
2026-07-16 15:35〜の状況のスナップショット(2026-07-16 19:20 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備。SiwA ネイティブ修復完了(新 App ID を旧 primary me.subnote.Subnote に group)+ Web/iOS 統合実証 + IAP フルパイプライン検証済み + ASC 初ビルド 1.0 (1) VALID。残るは審査提出準備(スクショ + IAP メタデータ)と QA データ掃除
要点:
- SiwA ネイティブ「登録を完了できませんでした」の真因は新 App ID の独立 primary 構成 → 旧 primary に group して解決、実機 SiwA 成功
- 同一 Apple ID で iOS ネイティブ + Web ログイン → Supabase ユーザー1件に集約(同一
sub)を実証 - 次: 審査提出準備 / QA データ掃除 / 有料 App 契約(銀行・税務)は未着手のまま
- (このスナップショット直後の 19:20 セッションでヘッダーワードマークのアニメーションを追加。継続メモは変更なし)
2026-07-16 03:05〜の状況のスナップショット(2026-07-16 15:35 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備。実機 IAP フルパイプライン検証完了 + ASC 初ビルド 1.0 (1) アップロード済み(VALID)。残るは SiwA ネイティブ E2E(当時は Apple 側「反映待ち」が最有力と誤診 → 後に新 App ID の primary 構成問題と判明・解決)と審査提出準備
要点:
- IAP 実機検証フルコンプ(7/15夜): iPhone 14 で [Environment: Xcode] 購入 → RC 受理 → webhook 200 → Supabase 同期 → 無料5件ゲート解放。実 ¥ 価格・失効同期も確認
- バグ2件修正(
e438719): ①スキームの StoreKit 参照パス切れ(Xcode Run でも Sandbox 接続になっていた)②RC app_user_id 大文字 UUID →.lowercased()正規化(DECISIONS 2026-07-16) - ASC 初ビルド 1.0 (1) アップロード済み(7/16 未明、VALID): distribution は Xcode サインイン済みアカウント経由の
-exportArchive(destination=upload) - SiwA ネイティブは「登録を完了できませんでした」で持ち越し中だった(→ 7/16 15:35 セッションで解決)
2026-07-15 15:57〜の状況のスナップショット(2026-07-16 03:05 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備。IAP 実装が組み込み・コミット済み(10d342e)+ RevenueCat ③設定完了 + StoreKit ローカル QA 済み(7/15)。残るは実購入検証(Xcode/Sandbox)とレビュー用スクショ添付での審査提出。Codex QA ループ1周(F-001〜F-010)も本番検証済み
今どこまで進んだか:
iOS IAP 実装が完了・コミット済み(今回 7/15): Codex が RevenueCat SDK(PurchaseManager)/ ペイウォール(PremiumView)/ 無料5件上限導線 / 購入・復元を実装 →
10d342eでコミット。.storekitのテスト価格を本番価格(¥150/¥1,200/¥2,500)に修正(5740da7)RevenueCat ③設定を v2 API で完了(今回 7/15):
premiumに App Store monthly/annual を紐付け(lifetime は既存)、default offering の3パッケージに App Store 商品を追加、appl_公開キー(appl_SSHoBZAPUkHdynsZSYGmsdcAkSu)を取得して Config.swift に反映。作業はproject_configuration権限つき一時キーsubnote-config-tempで実行 → revoke 済みStoreKit ローカル QA 済み(今回 7/15): QA アカウント goda.takeshichan2 でログイン → ペイウォール構成・anti-steering・購入結線・復元・無料5件上限→ペイウォール導線(前回 Codex 未検出箇所)を確認。購入完了と実 ¥ 価格表示は CLI 起動では StoreKit ローカル設定が非活性のため未検証(Xcode実行/Sandbox が必要)
Codex QA 対応完了(今回 7/15): F-001〜F-009(
3ea4dee)= アカウント削除導線 / ログアウト時 OTP リセット / 金額検証 / Dynamic Type・「+」ボタン重なり・長名の UI 崩れ / 認証エラー日本語化 / 法務リンク / カタログ検索空状態。F-010(d6258ae)= 削除 API 502 の解消アカウント削除が完全体で本番稼働:
POST /api/account/delete(Bearer 認証のみ)が Apple トークン失効 → RevenueCat customer 削除 → auth ユーザー削除(cascade)を実行。SiwA 失効はPOST /api/apple/tokenが authorizationCode を refresh token 化してapple_refresh_tokensに保管(migration20260715000001本番適用済み)。fail-closed は premium_until が未来のユーザーのみ(無課金・lifetime は RC 失敗でも削除続行)。本番 E2E で削除 200 + ユーザー消滅を確認済みRevenueCat キー移行完了: write 権限(Customers Read & Write)つき新キーが Worker とローカル env で稼働、旧読み取り専用キー「Subnote Cloudflare」は Revoke 済み
IAP 実装指示書: docs/codex-task-ios-iap.md。商品 ID 案(
me.subnote.app.premium.monthly/.annual/.lifetime)、app_user_id=Supabase UUID 厳守、StoreKit Configuration でのローカル検証(有料契約待ちでも進められる)、審査要件(復元ボタン・法務リンク・anti-steering)、完了報告はdocs/codex-task-ios-iap-REPORT.md無料アプリ契約は検証中のまま / 有料 App 契約(銀行・税務)は未着手 = IAP の Sandbox・商品審査のブロッカー
課金パイプライン(Web)本番稼働・手入力トリアージ本番稼働(本番目視とメール着信確認がまだ)
次にやること:
- 実購入検証(Xcode の Product▸Run か Sandbox Apple ID): 購入完了 → entitlement 有効化 → UI 反映 → webhook → Supabase 同期 / アカウント切替時の premium 分離 / 実 ¥ 価格表示。※CLI(simctl)起動では StoreKit ローカル設定が非活性で購入完了・¥価格が出ない(今回の QA で判明)
- 審査提出準備: ペイウォール(PremiumView)のスクショ → ASC の各 IAP 商品(現状 MISSING_METADATA)にレビュー用スクショ添付 → アプリ本体審査と同時提出
- SiwA ネイティブの実機 E2E(サインイン → 削除 → 端末設定の「Apple でサインイン」一覧から Subnote が消えるか。Supabase Apple プロバイダの Authorized Client IDs に
me.subnote.appが入っているかも確認) - 掃除(任意): QA アカウント goda.takeshichan2 の本番テストサブスク5件 / 正式公開判断(Web ベータガード再削除 + 告知)/ 手入力トリアージの本番確認 / ~/Downloads の
AuthKey_9MN99J7VFW.p8(Cloudflare secret 登録済み・パスワードマネージャ保管確認後に削除)
継続メモ:
- アカウント削除 API は Bearer 認証のみ(Cookie を見ない = CSRF 構造的防止)。iOS 以外から使うときも同方式で
- Web の SiwA(Services ID
me.subnote.web)ユーザーの Apple トークンは失効できない(Supabase が内部保持し外部に出さない、既知の限界)。iOS ネイティブ分のみ失効可能 - Apple client secret は2方式併存: iOS 失効用は
APPLE_SIGNIN_PRIVATE_KEY(Worker secret、.p8 から都度署名 = 期限管理不要)/ Supabase ダッシュボード用の Web 分は従来通り半年更新(期限 2027-01-09、dev-timer #32/#33 で監視) - RevenueCat キーは write 権限つきに移行済み(旧「Subnote Cloudflare」= 読み取り専用は Revoke 済み)。ローカル
.env.development.localも新キー - ASC MCP は 2 本立て: Subnote の作業は
asc-subnote(プロジェクト.mcp.json、ramanu アカウント)。ユーザーレベルのapp-store-connectは旧 Thomsons 別アカウント - Bundle ID は
me.subnote.appで確定。旧 App IDme.subnote.Subnoteは削除しない(Web SiwA の Services ID が参照している可能性) - Supabase のダッシュボード管理作業は info@ramanu.co.jp(Owner)で行う。再生成コマンド:
node scripts/gen-apple-secret.mjs --key <p8> --kid 9MN99J7VFW --team 9HZW92277N --client-id me.subnote.web - ADMIN_EMAILS は Worker secret で上書き中(gmail + Apple テスト用)。正式公開時に見直し
- Entitlement 識別子
premiumは web/lib/entitlements.ts の定数と一致必須。無料上限変更時は migration のfree_limitとFREE_SUBSCRIPTION_LIMITを両方揃える - 手動付与の lifetime は webhook 同期で上書きされて消える。消えたら lifetime 付与 SQL を再実行
- iOS アプリ内から Web 決済ページへ誘導しない(anti-steering)/ webhook・sk_ キーはパスワードマネージャ管理
- 旧継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール量監視 / Cloudflare リカバリーコード txt 削除 / Google OAuth クライアント法人化)は残存
2026-07-13 19:47〜の状況のスナップショット(2026-07-15 06:57 にアーカイブ)
フェーズ: Phase 2(iOS)リリース準備に着手。App Store Connect 側の土台(API キー・Bundle ID・アプリレコード)が完了(7/13 夜)。Web の正式公開は引き続き告知準備待ち
今どこまで進んだか:
- ASC 整備完了(7/13 夜): ramanu アカウント(info@ramanu.co.jp)の ASC API 有効化 → チームキー
subnote-mcp(App Manager、Key ID8M2SWG78S6、.p8 は~/.config/asc/)→ プロジェクト .mcp.json に asc-subnote MCP 新設。Bundle IDme.subnote.appを API 登録 + アプリレコード「Subnote - サブスク管理」作成(Apple ID6790385156、会社名=Ramanu LLC、SKU=subnote、ja)。Xcode の bundle ID もme.subnote.appに統一済み - 無料アプリ契約が検証中(Apple が法人情報確認中、完了メールは info@ramanu.co.jp 宛): TestFlight/審査提出はこれ待ちの可能性。サブスク課金には有料 App 契約 + 銀行口座・税務情報が別途必要(未着手)
- SiwA Web 本番稼働 + relay ドメイン登録済み(7/13 昼。client secret 期限 2027-01-09、dev-timer #32=12/22・#33=1/5 で監視)
- インフラ名義の法人統一済み(Apple/Stripe/RevenueCat/Supabase/Cloudflare。残る個人名義は Google OAuth クライアントのみ)
- 課金パイプライン本番稼働(7/6)・手入力トリアージ本番稼働(7/9、本番目視とメール着信確認がまだ)
- テスト購入の月額サブスクは解約済み(8/6 に自然失効)。管理者アカウントは lifetime 復元済み
次にやること(当時):
- iOS ビルド確認 → アーカイブ → ASC アップロード
- アプリレコードのメタデータ整備(説明文・キーワード・スクショ・App プライバシー)
- Phase 2 残り: アカウント削除機能 / RevenueCat SDK + IAP / SiwA ネイティブ / 認証メールの出し分け
- 正式公開の判断: ベータガード再削除 + 告知
- 手入力トリアージの本番確認
- 小掃除: AuthKey_9MN99J7VFW.p8 削除 / Cloudflare リカバリーコード txt 削除 / Google OAuth 法人名義化
(継続メモは新しい「現在の状況」に引き継ぎ済みのため省略)
2026-07-13 18:00〜の状況のスナップショット(2026-07-13 19:47 にアーカイブ)
フェーズ: Phase 1(Web + 課金)完了・再クローズ中。Apple Developer 法人審査が通過し、SiwA(Sign in with Apple)が Web 本番稼働 + インフラ名義の法人統一も完了(7/13)。正式公開は引き続き告知準備待ち。Phase 2(iOS)のブロッカーは解消され、いつでも着手できる状態
今どこまで進んだか:
- SiwA が Web 本番稼働(今回 7/13): App ID
me.subnote.Subnote/ Services IDme.subnote.web/ 鍵(Key ID9MN99J7VFW、.p8 はパスワードマネージャ)→ client secret(期限 2027-01-09、dev-timer #32=12/22・#33=1/5 登録済み)→ Supabase Apple プロバイダ → Build variableNEXT_PUBLIC_ENABLE_APPLE_LOGIN=1点灯 → 本番 E2E 成功。relay メールドメイン登録も完了(apex に SPF 追加して緑チェック、「メールを非公開」ユーザーへのメール到達性確保) - インフラ名義の法人統一(今回 7/13): Supabase を org「RAMANU LLC」(info@ramanu.co.jp)へ移管(ref/URL/キー無変更・無停止、個人アカウントは Developer メンバー)+ Cloudflare アカウントを法人化(メール変更 + パスワード/2FA + RAMANU LLC にリネーム)。Apple / Stripe / RevenueCat / Supabase / Cloudflare すべて法人名義に。残る個人名義は Google ログイン用 OAuth クライアント(Google Cloud)のみ
- 為替 cron 改善をデプロイ(今回 7/13、
26d7d92): 実行 01:47 UTC へ(Frankfurter の 00:00 過負荷 521 回避)+ 全滅時通知は既存レート72h超のみ - 手入力サービスのトリアージ運用が本番稼働(7/9): /admin/catalog/requests の名寄せ3分類 + 却下記録 + 確認付きバックフィル + 日次ダイジェストメール(09:15 JST)。本番目視とメール着信確認がまだ
- 課金パイプライン本番稼働(7/6): 無料5件ゲート + RevenueCat Web Billing + webhook。実カード E2E 済み、同期は v2 API(web/lib/revenuecat.ts)
- テスト購入の月額サブスクは解約済み(8/6 に自然失効)。管理者アカウントは lifetime 復元済み
次にやること:
- 正式公開の判断: ベータガード再削除(web/middleware.ts の1ブロック)+ 告知(ASO・告知文面は promo-expert 壁打ち候補)
- Phase 2(iOS): アカウント削除機能(審査必須 5.1.1(v)、Apple トークン失効 API 込み)/ RevenueCat SDK + IAP 商品登録 + Small Business Program 申請 / SiwA ネイティブ(relay メールドメイン登録は 7/13 完了済み)/ 認証メールの出し分け(Send Email Hook で Web=リンクのみ・アプリ=コードのみ)
- Phase 3(Android): RevenueCat SDK + Play 提出(組織アカウントなので12人テスター要件免除)
- 手入力トリアージの本番確認(/admin/catalog/requests 目視 + 新しい手入力があった日のダイジェストメール着信)
- 小掃除: Cloudflare リカバリーコード txt(~/Downloads)をパスワードマネージャ保存後に削除 / Google OAuth クライアントの法人名義化(いつか枠)
(継続メモは新しい「現在の状況」に引き継ぎ済みのため省略)
2026-07-09 20:14〜の状況のスナップショット(2026-07-13 18:00 にアーカイブ)
フェーズ: Phase 1(Web + 課金)完了・再クローズ中、運用整備を進行。課金・法務・公開検証は 7/6 に完了済みで、正式公開は告知準備待ち、Apple Developer の法人審査待ちが Phase 2 のブロッカー(ここは変化なし)。今回(7/9)は運用系の新機能=手入力サービス名のトリアージ + 日次メール通知を実装し本番反映まで完了(0e09a8d)
今どこまで進んだか:
- 手入力サービスのトリアージ運用が本番稼働(今回): /admin/catalog/requests が
catalog_id IS NULLの登録を正規化名で名寄せし「未対応 / カタログ一致 / 却下済み」に分類(ユーザー数・金額例・タグ付き)。「カタログに追加」は新規フォームへプリフィル遷移、「追加しない」はcatalog_review_dismissalsに記録(migration20260709000001本番適用済み)。追加後はサービス編集画面の確認付きバックフィルで既存ユーザーの登録に一括紐付け。新しい手入力があった日は毎朝 09:15 JST に Resend でダイジェストメール(cron 失敗通知と同じALERT_EMAIL_TO経路、新 secret 不要)。ローカルでランタイム検証済み(分類・却下除外・25h 窓すべて期待通り) - 課金パイプライン本番稼働(7/6): 無料5件ゲート + RevenueCat Web Billing + webhook。実カードで購入/失敗/解約/同期の E2E 済み。同期は v2 API(web/lib/revenuecat.ts)
- Phase 1 の画面: /premium / 法務3ページ / 設定のプランセクション / 5件上限導線。表記は「Subnoteプレミアム」
- Sign in with Apple: Web 実装済み・フラグ
NEXT_PUBLIC_ENABLE_APPLE_LOGIN非点灯。Apple Developer の法人審査待ちでブロック中 - テスト購入の月額サブスクは解約済み(8/6 に自然失効)。管理者アカウントは lifetime 復元済み
次にやること:
- 今回分の本番確認: デプロイ完了後に /admin/catalog/requests を目視(本番の既存手入力が未対応に出るはず)+ 新しい手入力があった日にダイジェストメールが届くことを確認
- Apple Developer 審査通過後: App ID(
me.subnote.Subnote、SiwA capability)→ Services ID(me.subnote.web、Return URL=Supabase callback)→ 鍵(.p8)→gen-apple-secret.mjsで JWT 生成 → Supabase Apple プロバイダ設定 → Cloudflare Build variable でフラグ点灯 → Web で動作確認 → 半年後の secret 再生成リマインダー登録(dev-timer) - 正式公開の判断: ベータガード再削除 + 告知(ASO・告知文面は promo-expert 壁打ち候補)
- Phase 2(iOS): RevenueCat SDK + IAP 商品登録 + Small Business Program 申請 / アカウント削除機能(審査必須 5.1.1(v)、Apple トークン失効 API 込み)/ SiwA ネイティブ + relay メールドメイン登録 / 認証メールの出し分け(Send Email Hook で Web=リンクのみ・アプリ=コードのみ)
- Phase 3(Android): RevenueCat SDK + Play 提出(組織アカウントなので12人テスター要件免除)
継続メモ:
- Entitlement 識別子
premiumは web/lib/entitlements.ts の定数と一致必須。無料上限変更時は migration のfree_limitとFREE_SUBSCRIPTION_LIMITを両方揃える - 手動付与の lifetime は webhook 同期で上書きされて消える(返金・解約イベントでも発火)。消えたら lifetime 付与 SQL を再実行。手動付与を常用するなら別カラム分離が必要(ROADMAP いつか)
- RevenueCat の「Subnote Cloudflare」キーは顧客情報の読み取り専用(webhook 用として正しい)。設定変更・返金を API でやる場合は権限付きの管理キーを別途発行する
- ローカルの
.env.development.localにREVENUECAT_SECRET_API_KEYあり(REST での調査・検証用) - iOS アプリ内から Web 決済ページへ誘導しない(anti-steering)/ bundle ID は現状維持で確定 / webhook・sk_ キーはパスワードマネージャ管理
- 旧継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール量監視)は残存
2026-07-06 20:10〜の状況のスナップショット(2026-07-09 20:14 にアーカイブ)
フェーズ: Phase 1(Web + 課金)完了・公開検証済み・再クローズ中。ペイウォールと法務3ページが本番稼働、課金の全経路(購入/失敗/解約/同期)を実カードで E2E 検証済み。一般公開はいつでもできる状態(ベータガード1ブロック消すだけ)で、告知準備が整うまで意図的に閉じている。Apple Developer の法人審査待ちが Phase 2 のブロッカー
当時の進捗:
- 課金パイプライン本番稼働: 本番 migration 適用済み(無料5件ゲート稼働)。RevenueCat(Web Billing + 商品3つ + Offering + webhook)と Cloudflare Secrets 設定完了。購入 → entitlement → Supabase 同期 → 画面反映まで実カード(¥150)で検証済み。E2E で発見した障害2件は修正済み= ①RevenueCat 同期は v2 API を使う(v1 は v2 secret key で 403。web/lib/revenuecat.ts、project id
proj560a5f94はコード内定数)②実商品3つを entitlementpremiumにアタッチ済み(placeholder のみアタッチされていた) - Phase 1 の画面: /premium(日本語チェックアウト・メール事前入力・解約済みは「自動更新停止済み」表示・表示時の自己修復同期)/ 法務3ページ(/terms /privacy /tokushoho)/ 設定のプランセクション / 5件上限エラー → プレミアム導線。表記は「Subnoteプレミアム」(カタカナ)で統一
- 公開検証: ベータガードを一度撤去して新規サインアップ E2E 成功(メール着信 → ログイン → ダッシュボード)→ ガード復元(
48e5b73)。認証メールは Web ボタン + アプリ用コード併記の新テンプレ適用済み - Sign in with Apple: Web 実装済み・フラグ
NEXT_PUBLIC_ENABLE_APPLE_LOGINで非表示のまま。secret 生成スクリプト gen-apple-secret.mjs 準備済み。Apple Developer の法人審査待ちで設定作業がブロック中 - テスト購入の月額サブスクは解約済み(8/6 に自然失効、返金なし)。管理者アカウントは lifetime 復元済み
当時の次やること: ①Apple Developer 審査通過後の SiwA 設定一式 ②正式公開の判断(ガード再削除 + 告知)③Phase 2(iOS)④Phase 3(Android)。※このうち①〜④は 7/9 時点でも変化なし(審査待ち継続)
2026-07-03 15:18〜の状況のスナップショット(2026-07-06 20:10 にアーカイブ)
フェーズ: リリース準備。マネタイズ確定=フリーミアム(無料はアクティブ5件 / Premium 月¥150・年¥1,200・買い切り¥2,500)、リリース順序=Web(決済込み)→ iOS → Android、販売主体=法人(合同会社ラマヌ)。課金基盤 Phase 0(コード)完了、Stripe 審査待ち
当時の進捗:
- 課金基盤 Phase 0(コミット
6f194c1、本番 DB 未適用):user_entitlements(RevenueCat からの同期キャッシュ)+ 無料5件ゲート=DB トリガー1箇所で強制 + webhook 受け口 /api/revenuecat/webhook(fail-closed、REST で subscriber 再取得して同期)。ローカル検証済み(SQL 8項目 + curl 4パターン + tsc/lint) - RevenueCat: アカウント(info@ramanu.co.jp)・プロジェクト Subnote・Entitlement・Offering(Monthly/Yearly/Lifetime)作成済み。Apps 登録・商品・webhook 設定は未
- Stripe: 法人アカウント申請提出済み・審査中。明細書表記=SUBNOTE、Managed Payments 不採用、Tax/Climate スキップ
当時の次やること: ①Stripe 審査通過 → Web Billing 接続 ②RevenueCat 残り設定 + Cloudflare Secrets ③本番 migration + lifetime 付与 ④Phase 1(法務3ページ → ペイウォール → ガード解除)⑤Phase 2/3。※この後、7/6 のセッションでほぼすべて完了(詳細は DEVLOG 2026-07-06)
2026-06-23 09:52〜の状況のスナップショット(2026-07-03 15:18 にアーカイブ)
フェーズ: 3プラットフォーム(web/ ios/ android/)が機能・デザインとも揃い本番稼働中。今回はインフラ障害(Supabase 無料枠の auto-pause)の復旧と再発防止に対応
今どこまで進んだか:
- インフラ復旧(今回の主作業・すべて本番反映済み):
- Supabase 無料プロジェクトが7日 inactivity で auto-pause された。原因は keep-alive(日次 cron の
exchange_ratesupsert)が Frankfurter API の成功に完全依存しており、Frankfurter が数日落ちた間 Supabase に無アクセスになっていたこと(旧コードは1通貨失敗で upsert 前に return) - ①Supabase を手動 unpause ②keep-alive を Frankfurter から切り離し(refresh/route.ts=為替取得の前に必ず Supabase を read、1通貨失敗でも全体中断せず取れたぶんだけ upsert、健全性を HTTP ステータスに反映)③cron 失敗時に Resend でメール通知(worker.ts の callInternal で HTTP エラー/例外時に送信、専用キー subnote-worker-cron、本番疎通確認済み)
- Supabase 無料プロジェクトが7日 inactivity で auto-pause された。原因は keep-alive(日次 cron の
- アプリ本体: web/ios/android とも機能・デザイン揃い済み(メール OTP + Google ログイン / ダッシュボード / 登録・編集・解約・削除 / 設定 / 解約履歴 / 月次チャート)。Android にホーム画面ウィジェット、iOS に月送り+ブランドヘッダーまで実装済み(詳細は下記アーカイブ)
- メール基盤: Resend +
subnote.meドメイン認証(DKIM/SPF/DMARC verified)。Resend キーは2本=subnote-smtp(Supabase Auth Custom SMTP 用)/ subnote-worker-cron(worker の cron 失敗通知用)。送信枠は両用途で Resend 無料枠(日100/月3000)を共有
当時の次やること:
- 数日 Supabase が再 pause されないことを確認(keep-alive の実効検証)
- Android Google ログインの End-to-End 確認(実機で
app-debug.apk推奨) - iOS ウィジェット(Android と対称=ホームに月額合計、WidgetKit)
- メール送信量の監視 / cancel_url 残り投入
※この後、新セッション(7/2〜3)でリリース戦略の確定と課金基盤 Phase 0 に入った
当時の継続メモ(プラットフォーム別の詰まりどころは下記アーカイブ参照):
- cron / keep-alive: 為替 cron の Supabase 書き込みは「Frankfurter の成否と無関係に必ず1回 read する」設計に変更済み。cron が失敗すると
ALERT_EMAIL_TOにメールが飛ぶ。Frankfurter(frankfurter.dev)は個人運営の無料 API で数日落ちることがある前提 - Resend env(Worker):
RESEND_API_KEY(=subnote-worker-cron / Secret) /ALERT_EMAIL_FROM(=alerts@subnote.me) /ALERT_EMAIL_TO。未設定ならsendAlertEmailは no-op - iOS シミュレータのタップ自動化は idb 廃止で不可(操作検証は手タップ or コード精査)、Android は adb で自動化可。Google ログインはブラウザ方式 OAuth で SHA-1 不要。その他プラットフォーム別の詳細は下記アーカイブ
2026-06-08 18:06〜の状況のスナップショット(2026-06-23 09:52 にアーカイブ)
フェーズ: 3プラットフォーム(web/ ios/ android/)が機能・デザインとも揃った段階。Android にホーム画面ウィジェット、iOS に月送り(検証済み)+ホームのブランドヘッダーを追加
今どこまで進んだか:
- Android(
android/)= ネイティブ Kotlin + Jetpack Compose + supabase-kt: メール OTP ログイン / ホーム / 新規登録カタログフロー / 編集・解約・削除 / 設定・表示通貨切替 / 解約履歴・復活(本番 Supabase 接続を実機確認済み)。ホーム画面ウィジェット(月額合計)を実機 E2E 検証済み(Glance、アプリのLaunchedEffectがupdateMonthlyTotalWidget()で Glance Preferences に書き込み → ウィジェットは描画専念)。Google ログイン投入済み(ブラウザ方式・E2E 未検証) - iOS(
ios/Subnote/)= SwiftUI: 月次チャートの「月送り」対応を検証済み(.chartXSelectionでバータップ→過去月合計を見出し表示。Xcode 26.5/iPhone 17 Pro でビルド+初期描画+結線精査)。ホーム上部にブランドヘッダー(safeAreaInsetでブランドマーク「S」40pt+ワードマークlargeTitle0.85倍の左寄せロックアップ) - 認証の現状: メール OTP は3プラットフォーム稼働。Google ログインは3プラットフォーム投入済み・Supabase プロバイダ有効済み、Android の E2E のみ未検証
当時の次やること: ① Android Google ログイン E2E(実機推奨) → ② iOS ウィジェット → ③ メール送信量の監視 / cancel_url 残り投入。※この後、新セッション(6/23)で Supabase auto-pause 障害対応に入った
当時の詰まっていること・未決事項:
- iOS シミュレータのタップ自動化手段が消滅: idb-companion は Meta がアーカイブ済み、simctl 単体もタップ非対応。操作検証は 手タップ or コード精査。ビルドは
DEVELOPER_DIR="/Applications/Xcode 26.5.app/Contents/Developer"、xcodebuild -projectは絶対パス指定で(cwd リセットでdoes not existになることがある) - Google ログインの方式メモ: ブラウザ(Custom Tab / ASWebAuthenticationSession)経由の OAuth。Google から見たリダイレクト先は常に Supabase の https callback なので Web タイプ OAuth クライアント1個を3プラットフォーム共有、SHA-1・Android タイプは不要(ネイティブ Google Sign-In に切り替えた場合のみ必要)。debug SHA-1 =
0A:A6:9B:9E:A2:18:60:03:56:64:08:3E:C8:2E:B1:F1:18:18:03:2C - Android ビルド/実行メモ:
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"で./gradlew :app:assembleDebug。emulator(kowai16k) は adb で install/launch/screenshot +adb shell input tap/textで自動化可、座標ズレはadb shell uiautomator dump、ダークモードはadb shell cmd uimode night yes|no|auto。android/local.propertiesは gitignore 済み - 画像 API の制約: スクショ目視は蓄積画像の枚数/総量で弾かれることがある(
many-image requests: 2000 pixels)。節目だけ小サイズ +uiautomator dump中心、目視が要るなら新セッションへ - supabase-kt の注意:
is not nullフィルタが不確実 → 解約済み判定はクライアント側でcanceledAt != null。ExposedDropdownMenuは非修飾呼び出し、アイコンはプロパティ個別 import - メール運用: 実効上限は「Supabase Rate Limit と Resend 配送枠の小さい方」。Resend 無料枠(日100通)が当面の天井。DMARC は
p=none(監視のみ) - iOS のシミュレータは最低 OS 26.5ビルド。Magic Link テンプレに
{{ .Token }}必須 等の従来事項は下記アーカイブ参照
2026-06-08 16:23〜の状況のスナップショット(2026-06-08 18:06 にアーカイブ)
フェーズ: 3プラットフォームが機能・デザインとも揃った段階。Android にホーム画面ウィジェット(月額合計)を追加し実機 E2E 検証まで完了
今どこまで進んだか: Android ウィジェットを Glance で実装・エミュレータで E2E 検証(アプリ表示と一致・タップで起動)。ウィジェットは Supabase を叩かずアプリの LaunchedEffect 経由で Glance Preferences に表示テキストを書き込む責務分離。iOS は月送り対応を着手したが別コミットでビルド未検証のまま。Google ログインは3プラットフォーム投入済み・プロバイダ有効済み、Android E2E のみ未検証
当時の次やること: ① iOS 月送りのビルド検証して仕上げ → ② Android Google ログイン E2E → ③ メール量監視 / cancel_url / iOS ウィジェット。※この後、新セッションで①を検証(idb 廃止でタップ自動化は不可、ビルド+初期描画+結線で検証)し、さらに iOS ホームにブランドヘッダーを追加
2026-06-07 23:26〜の状況のスナップショット(2026-06-08 16:23 にアーカイブ)
フェーズ: 3プラットフォーム(web/ ios/ android/)が機能・デザインとも揃った段階。Android はデザイン未目視項目を実機確認済み + チップ/アイコン質感調整 + Google ログインのコード投入まで完了
今どこまで進んだか: Android はメール OTP / ホーム各機能 / 登録・編集・解約・削除 / 設定 / 解約履歴まで iOS と機能同等、デザインも iOS 品質。前セッションの未目視4点を実機目視済み。今セッションでチップ白ピル化・ServiceIcon 外周枠・推移チャート当月ラベル廃止。Google ログインをブラウザ方式で投入(ボタンタップで Custom Tab 起動まで確認、E2E 未検証)。Google プロバイダは有効済み・Android 用の追加設定は不要と確定
当時の次やること: ① Android Google ログインの E2E確認(実機推奨)→ ② ウィジェット(ホームに月額合計)/ メール量監視 / cancel_url 残り → ③ iOS の月送り対応・ウィジェット。※この後、新セッションで②のウィジェットを仕上げて実機 E2E 検証まで実施。iOS 月送りも着手(ビルド未検証で別コミット保存)
2026-06-07 22:40〜の状況のスナップショット(2026-06-07 23:26 にアーカイブ)
フェーズ: Android 版が iOS と機能同等 + デザインも iOS 品質に到達。デザイン追い込み中(22:14 以降の変更が画像 API 制約で未目視のまま区切った状態)
今どこまで進んだか: Android は配色刷新(systemGroupedBackground 風)+ ホーム/設定/履歴/編集すべて grouped 構成 + グラデーション合計ヒーロー + 白カードチャート + 丸型検索 + 円形 FAB + ログインのブランドマーク。共通パーツ GroupCard/SectionLabel/SearchField/BrandMark。ただし編集フォーム grouped・FAB 円形・チャート切れ修正・ダークモード色味は構成/ビルドのみ確認で未目視
当時の次やること: ① 未目視4点を新セッションで実機目視(チャート切れ/編集余白/FAB円形/ダーク色味)→ ② チップ・アイコン質感調整 → ③ Google ログイン本番化。※この後、新セッションで①②と Google ログインの Android コード投入まで実施。また「Google プロバイダ未設定」「Android は SHA-1 必要」は誤りと判明(プロバイダは有効済み・ブラウザ方式では SHA-1 不要)
2026-06-07 22:14〜の状況のスナップショット(2026-06-07 22:40 にアーカイブ)
フェーズ: Android 版が iOS と機能同等 + 見た目も iOS 品質に到達(デザイン刷新直後)
今どこまで進んだか: 配色を iOS 風ニュートラル(薄グレー背景+白カード+薄い影)に刷新。ホーム/設定/履歴を grouped 化、グラデーション合計ヒーロー、白カードチャート、丸型検索、⋮ 廃止、ログインにブランドマーク。共通パーツ GroupCard/SectionLabel/SearchField/BrandMark を整備
当時の次やること: 編集フォーム grouped 化 / タグチップ・アイコン微調整 / ダークモード実機確認。※この後、編集フォーム grouped 化・FAB 円形・チャート切れ修正まで実施したが画像 API 制約で目視できず未確認のまま区切り
2026-06-07 21:52〜の状況のスナップショット(2026-06-07 22:14 にアーカイブ)
フェーズ: Android 版に着手し、iOS とほぼ機能同等まで一気に到達。モノレポは web/(本番)・ios/(実用)・android/(新規)の3プラットフォーム体制に
今どこまで進んだか:
- Android(
android/)= ネイティブ Kotlin + Jetpack Compose + supabase-kt:- Gradle 8.11.1 / AGP 8.7.3 / Kotlin 2.1.0、compileSdk 35 / minSdk 26、namespace
me.subnote.android、version catalog - 認証: メール OTP(iOS と対称、
signInWith(OTP)→verifyEmailOtp、sessionStatus復元)。セッション永続も確認済み - ホーム: ロゴ画像(Coil) / 検索 / 並び替え / タグ別チップ / もうすぐ更新 / 月次推移チャート(Compose Canvas 自作) / 月額合計カード / 行操作メニュー(⋮)
- 書き込み: 新規登録(カタログ選択→プラン→フォーム自動補完、手入力も可)/ 編集(金額バリデーション)/ 解約 / 完全削除
- 設定: 表示通貨切替 / 解約履歴(復活) / ログアウト
- 第一印象: ネイティブ起動スプラッシュ(indigo+白S) / スケルトン / FAB ハプティクス / アダプティブアイコン
- 集計・通貨換算は Web/iOS と同一仕様。Repository(
SubnoteRepo)に DB アクセス集約、Navigation Compose 結線 - emulator(kowai16k)で本番 Supabase 接続 → OTP ログイン〜全画面を実データ確認
- Gradle 8.11.1 / AGP 8.7.3 / Kotlin 2.1.0、compileSdk 35 / minSdk 26、namespace
- 認証の現状: メール OTP は Web/iOS/Android すべて稼働。Google ログインは Web/iOS にコードあり・Android は未投入。いずれも Supabase の Google プロバイダ有効化が未設定で本番未稼働
- 直近コミット:
24b9216(Android 第一印象磨き込み)/975f286(iOS と機能同等化)/2918fa1(Android 着手)
当時の次やること: Google ログイン本番化 / Android ウィジェット / プラン画面目視確認 / メール量監視。※この後すぐ「見た目が iOS よりダサい」指摘を受けデザイン刷新に着手
2026-06-07 21:00〜の状況のスナップショット(2026-06-07 21:52 にアーカイブ)
フェーズ: メール送信基盤を Resend + 独自ドメイン認証(SPF/DKIM/DMARC)に移行完了。本番ローンチに必要なメール条件をクリア。認証は OTP/マジックリンク + Google OAuth(コードのみ)の2系統
今どこまで進んだか:
- メール基盤: Supabase 共有SMTP → Resend に移行。
subnote.meをドメイン認証(DKIM/SPF/DMARC verified)、Supabase Auth の Custom SMTP に Resend を設定。マジックリンクがnoreply@subnote.meから Gmail INBOX に着信することを確認=本番フロー成功。「1h/4通」制限は解消 - Supabase Auth Rate Limit: 「sending emails」を 200/時 に設定(ただし実効上限は Resend 無料枠=日100通/月3000通で律速)
- 認証: iOS/Web 両方に Google ログイン実装済み(コードのみ)。Supabase の Google プロバイダ有効化は未設定
- iOS: 一覧スワイプで解約/削除、シミュレータ(iPhone 17 Pro / iOS 26.5)でビルド成功
- 直近コミット
4f2b22d(前セッションの .devnotes 更新)。今セッションはコード変更なし(外部サービス設定のみ)
次にやること:
- Supabase の Google プロバイダ設定(有効化 + Google Cloud OAuth クライアントのリダイレクト URI 登録)→ 本番で Google ログイン動作確認
- メール量の監視: Resend ダッシュボードの送信数が日100通/月3000通に近づいたら Resend Pro($20/月・月5万通) へ
- iOS の残り磨き: 月送り対応 / ウィジェット
- cancel_url の残り投入 / Android 着手
詰まっていること・未決事項:
- メール運用メモ: 実効上限は「Supabase Rate Limit と Resend 配送枠の小さい方」。当面は Resend 無料枠(日100通)が天井。DMARC は今
p=none(監視のみ)、運用安定後にquarantine→rejectへ段階強化 - Google ログインは Supabase バックエンド設定が前提(Google プロバイダ有効化 + Google Cloud OAuth クライアント。リダイレクト URI =
https://qbikwqghzuennnfdsmin.supabase.co/auth/v1/callback、Web の Redirect URLs にhttps://subnote.me/auth/callback) - スケーリング観点(おさらい結果): 人数増で効く順 = ① Supabase 同時接続/Pooler 確認、②メールレート(今回対処済み)、③ 月次 snapshot cron の単発実行(数千〜万人でバッチ分割)、④ Workers Paid、⑤ Supabase Pro
- iOS のシミュレータは最低 OS 26.5ビルド。従来からの継続事項(simctl タップ不可、Magic Link テンプレに
{{ .Token }}必須 等)は下記アーカイブ参照
2026-06-07 18:48〜の状況のスナップショット(2026-06-07 21:00 にアーカイブ)
フェーズ: iOS/Web ともに Google ログイン対応。iOS は一覧スワイプで解約/削除も追加。認証手段が OTP/マジックリンク + Google OAuth の2系統に
今どこまで進んだか:
- 認証: iOS/Web 両方に Google ログインを実装(
signInWithOAuth、iOS は URL schemesubnote、Web はsignInWithGoogleサーバーアクション + 既存 callback 流用)。マジックリンク/OTP と併用。ただし Supabase の Google プロバイダ有効化は未設定(コードのみ完了、有効化すれば iOS/Web 同時に動く) - iOS スワイプ操作: 一覧行の左スワイプで「解約」/「削除」(confirmationDialog + cancel_url ページ起動)、
SubscriptionStore.cancel()/delete()に集約 - iOS はシミュレータ(iPhone 17 Pro / iOS 26.5)でビルド成功・起動確認済み(スワイプ実操作のみ手動確認)
- 直近コミット
043dadd(iOS/Web Google ログイン + iOS スワイプ解約/削除)push 済み → Cloudflare Builds で Web 自動デプロイ
次にやること:
- Supabase の Google プロバイダ設定(iOS/Web 共通で有効化 + Google Cloud OAuth クライアントのリダイレクト URI 登録) → 本番で Google ログイン動作確認
- iOS の残り磨き: 月送り対応(過去月 snapshot 閲覧)/ ウィジェット(ホームに月額合計)
- cancel_url の残り投入(niコニコ / DMM TV / 楽天系 / dマガジン / FOD 等)
- Android 着手(モノレポに
android/)
詰まっていること・未決事項:
- Google ログインは Supabase バックエンド設定が前提(Google プロバイダ有効化 + Google Cloud OAuth クライアント。リダイレクト URI =
https://qbikwqghzuennnfdsmin.supabase.co/auth/v1/callback、Web の Redirect URLs にhttps://subnote.me/auth/callback) - iOS のシミュレータは最低 OS 26.5ビルドなので 26.5 ランタイムのデバイスでしか動かない
- 従来からの継続事項(iOS Run 自動化の simctl タップ不可、Magic Link テンプレに
{{ .Token }}必須、MemberImportVisibility、Info.plist の置き場、集計ロジック3箇所、service role key ローテ推奨 等)は下記アーカイブ参照
2026-06-05 20:55〜の状況のスナップショット(2026-06-07 18:48 にアーカイブ)
フェーズ: iOS が Web の主要機能をほぼ完全カバー + 第一印象(アイコン/起動/質感)も整い、プロダクトとして見せられるレベルに到達
今どこまで進んだか:
- リポ構成 = モノレポ:
web/(Next.js 本番稼働)/ios/(SwiftUI)/ 将来android/。supabase/・.devnotes/・.env.*はルート共有。Cloudflare Builds の Path は/web - iOS(
ios/Subnote/)= Web 機能をほぼ網羅:- 認証: メール OTP ログイン、
authStateChangesでセッション復元 - 閲覧: 一覧(換算月額合計・カタログロゴアイコン)/ 検索 / 並び替え / タグ別フィルタ / 「もうすぐ更新」/ 月次推移チャート(Swift Charts、最大12本ローリング) / pull-to-refresh
- 書き込み: 新規登録(カタログ選択フロー=検索+カテゴリ絞り込み→プラン→自動補完、手入力も可)/ 編集(バリデーション付き)/ 解約 / 削除
- 設定: 表示通貨切替(合計再換算)/ ログアウト / 解約履歴(復活+確認ダイアログ)
- 第一印象: アプリアイコン=Web 公式マーク(スタックカード+セリフS)/ 白スプラッシュ+ロゴ演出 / アクセント indigo / スケルトン / ハプティクス
- 本番 Supabase 接続(publishable key を
Configに埋め込み)
- 認証: メール OTP ログイン、
- 本番 Web:
https://subnote.me(クローズドベータ継続) - カタログ: 291 サービス / 324 プラン / 289 ロゴ / default_tags 全件。正は本番(sync に上書き防止ガード)
- Cron: 為替レート(日次)+ monthly_snapshots(月初、6/1 初発火確認)を Cloudflare Cron に集約
- cancel_url: 本番に計 24 件投入済み
次にやること:
- iOS の磨き(いずれか): スワイプ削除/解約 / 月送り対応(過去月 snapshot 閲覧)/ ウィジェット(ホームに月額合計、ネイティブの強み)
- cancel_url の残り投入(niコニコ / DMM TV / 楽天系 / dマガジン / FOD 等、URL 要確認)
- Android 着手(モノレポに
android/) - 今月/今年の支出履歴ビュー強化(※「まだ不要」と保留中)
詰まっていること・未決事項:
- iOS の Run 自動化:
xcodebuild(DEVELOPER_DIR="/Applications/Xcode 26.5.app/Contents/Developer")→xcrun simctl terminate/install/launch→simctl io screenshotで「Stop→Run→スクショ目視」を Claude が実行可。ただし simctl にタップ機能が無いため操作を伴う検証は要 You タップ(idb 等で自動化は可) - iOS は OTP 方式のため Supabase の Magic Link テンプレートに
{{ .Token }}が入っている前提(外すとコードログイン不可、テンプレ反映に数分ラグ) - iOS Supabase は MemberImportVisibility で、メンバーアクセスに
import Auth/import PostgREST等の明示 import が必要 - iOS はファイルシステム同期グループ方式:
Info.plistはソースフォルダ内に置くと Copy Bundle Resources に二重登録されるので.xcodeprojと同階層に置く(INFOPLIST_FILE = Info.plist)。色アセット名からColor.brandIndigo等が自動生成されるので手書き extension と衝突注意 - iOS の編集では memo を扱わない(
Subscriptionモデル未対応、誤消去防止のため除外) - iOS のブランド素材は
ChatGPT Image...png(ユーザー提供 1254px、~/Downloads)由来。SVG 等の元データは無いので、必要なら再書き出しを依頼 - 本番 service role key をチャットに貼って実行した(cancel_url 投入時)。念のためローテーション推奨(未対応・任意)
web/.env.*は ルートへの symlink(各環境で張り直す運用)next_billing_dateは単発日付のまま(周期繰り越しは将来課題)- 集計ロジックが TS(
calcTotal)/ SQL(record_monthly_snapshots)/ iOS(convertedMonthly)の 3 箇所にある規律 - Google Play の名前検索の自動化手段は無し / catalog 作業前に Docker Desktop 起動確認
- 「タグの色分け」は当面実装しない / 独自 SMTP・マネタイズ方針は保留
2026-06-05 18:49 時点のスナップショット(2026-06-05 20:55 にアーカイブ)
フェーズ: iOS が 閲覧(一覧/検索/ソート/タグ別/もうすぐ更新)+書き込み(登録/編集/解約/削除)まで揃い実用レベルに到達(※この後、第一印象パック + カタログ選択 + 設定 + 解約履歴 + チャート + 質感 + バリデーションを追加)
今どこまで進んだか:
- モノレポ(web/ ios/)、iOS は メール OTP ログイン + 一覧/検索/ソート/タグ別/もうすぐ更新 + 登録/編集/解約/削除
- 本番 subnote.me 稼働、カタログの正は本番、cancel_url 計24件、Cron 集約、monthly_snapshots 6/1 初発火確認
- Web UI: 月次チャートのホバー / 解約中間ダイアログ / 更新日リマインダー=アプリ内表示 / ダークモード / タグサジェスト
当時の次やること: iOS のカタログ選択フロー / 設定 / 解約履歴 / アプリアイコン・起動画面
2026-06-04 21:50 時点のスナップショット(2026-06-05 18:49 にアーカイブ)
フェーズ: iOS(SwiftUI)が ログイン → サブスク一覧(換算合計 + アイコン)まで稼働。Web の主要な閲覧体験が iOS でも揃った(※この後、検索/ソート/タグ別/もうすぐ更新/登録/編集/解約/削除 を追加して実用レベルに)
今どこまで進んだか:
- モノレポ(web/ ios/)、iOS は メール OTP ログイン + サブスク一覧(換算合計・ロゴアイコン・pull-to-refresh)
- 本番 subnote.me 稼働、カタログの正は本番、cancel_url 計24件、Cron 集約、monthly_snapshots 6/1 初発火確認
- Web UI: 月次チャートのホバー / 解約中間ダイアログ / 更新日リマインダー=アプリ内表示 / ダークモード / タグサジェスト
当時の次やること: iOS の機能拡充(閲覧系 or 書き込み系)
2026-06-04 20:40 時点のスナップショット(2026-06-04 21:50 にアーカイブ)
フェーズ: Web MVP がコア機能ひと通り完成。モノレポ化(web/ ios/ android/)を実施し iOS(SwiftUI)着手の直前(Xcode プロジェクト作成前)
今どこまで進んだか:
- モノレポ化完了(web/ に Next.js、ios/ は README のみ、Cloudflare Builds の Path を /web に)
- 本番 subnote.me 稼働、カタログの正は本番、cancel_url 計24件、Cron 集約、monthly_snapshots 6/1 初発火確認
- Web UI: 月次チャートのホバー / 解約中間ダイアログ / 更新日リマインダー=アプリ内表示 / ダークモード / タグサジェスト
当時の次やること: iOS 着手(Xcode で SwiftUI プロジェクト作成 → Supabase SDK → ログイン)
2026-06-04 16:56 時点のスナップショット(2026-06-04 20:40 にアーカイブ)
フェーズ: 更新日リマインダー(アプリ内「もうすぐ更新」表示)まで実装完了。MVP のコア機能が一通り揃い運用フェーズへ(※この後モノレポ化を実施)
今どこまで進んだか:
- 本番 subnote.me 稼働、カタログの正は本番(sync 上書きガード設置)
- Cron 集約(monthly_snapshots 6/1 初発火確認)、cancel_url 計24件投入
- 月次チャートのホバー / 解約中間ダイアログ / 更新日リマインダー=アプリ内表示(通知なし)/ ダークモード / タグサジェスト
- ※リポはまだフラット構成(web/ 移行前)
当時の次やること候補: iOS 技術選定 / cancel_url 残り投入 / AGENTS.md・output_logo 片付け / 支出履歴ビュー(保留)
2026-06-01 18:28〜の状況のスナップショット(2026-06-04 16:56 にアーカイブ)
フェーズ: monthly_snapshots cron 初発火確認済み + 月次チャートのホバー + 解約中間ダイアログ + cancel_url 主要サービス投入まで完了
今どこまで進んだか:
- カタログ完成 + 本番 sync 済み(291/324/289 + default_tags)
- Cron インフラ集約、monthly_snapshots 6/1 初発火成功確認
- 月次チャートのホバーツールチップ
- 解約中間ダイアログ(cancel_url 登録済みのみ)
- cancel_url 主要 11 件を本番直接投入(正は本番側)
- ダークモード / タグサジェスト 実装済み
当時の次やること候補: sync 上書き対策 / cancel_url 追加投入 / リマインダー方針 / iOS 技術選定 / 支出履歴ビュー(保留)
2026-05-31 20:13〜の状況のスナップショット(2026-06-01 18:28 にアーカイブ)
フェーズ: ダークモード + タグサジェスト + カタログ default_tags まで実装完了。アプリの基礎 UX が一通り揃った状態
今どこまで進んだか:
- 本番 URL:
https://subnote.me(クローズドベータ継続) - カタログ完成 + 本番 sync 済み: 291 サービス / 324 プラン / 289 ロゴ / default_tags 全件投入済み
- 管理画面
/adminをADMIN_EMAILS専用ガードで保護、管理ハブ + カタログ管理 + ロゴ取得自動化 - Cron インフラを Cloudflare に集約(為替レート日次 09:00 JST + monthly_snapshots 月初 09:05 JST、6/1 初発火予定)
- 解約まわり: 「解約」「削除」分離 UI、
/history、復活ボタン、集計除外 - ダークモード切替(ライト/システム/ダーク 3 択、localStorage 永続)
- タグサジェスト(過去タグ chip、カテゴリ別フィルタ + フォールバック)
次にやること(候補): monthly_snapshots cron 初発火確認 / ドッグフーディング / 支出履歴ビュー / リマインダー方針 / iOS 技術選定 / cancel_url 投入
2026-05-31 20:13 時点のスナップショット
フェーズ: 解約まわり一連を実装完了。Cloudflare cron も本番で初発火確認。次は UX 系の小〜中タスク or ドッグフーディング
今どこまで進んだか:
- 本番 URL:
https://subnote.me - カタログ完成 + 本番 sync 済み: 291 サービス / 324 プラン / 289 ロゴ
/adminを middleware のADMIN_EMAILS専用ガードで保護、管理ハブ + カタログ管理 + ロゴ取得自動化- Cron インフラを Cloudflare に集約(為替レート日次 + monthly_snapshots 月初)
- 解約まわり一連完了: 「解約」「削除」分離 UI、
/history、復活ボタン
次にやること(優先順):
- monthly_snapshots cron が 6/1 09:05 JST に初発火するか動作確認
- 機能候補: 解約まわり / ドッグフーディング / ダークモード / タグまわり / 支出履歴ビュー
- 更新日リマインダー方針決定、iOS 技術選定 など
詰まっていること・未決事項:
- Google Play の名前検索を自動化する手段は現状なし
AGENTS.md/output_logo/は untracked- 独自 SMTP / マネタイズ方針は保留
2026-05-31 18:29 時点のスナップショット
フェーズ: 本番運用インフラ周りの磨き込み完了(管理画面 / cron 移行 / 月次集計自動化)。次は機能追加 or ドッグフーディング
今どこまで進んだか:
- 本番 URL:
https://subnote.me(Cloudflare Workers + 独自ドメイン、メアド許可リストでクローズドベータ継続) - カタログ完成: 291 サービス / 324 プラン / 289 ロゴ、本番 Supabase に sync 済み
/admin認可リファクタ完了: middleware に/admin/*専用ガード(ADMIN_EMAILS既定ichirokisanuki@gmail.com)、本番でも You のみアクセス可。管理ハブ/admin新設- Cron インフラを Cloudflare に集約: GH Actions の課金エラーから移行。
worker.ts新設で OpenNext の worker.js をラップ - 月次スナップショット自動記録: Postgres SQL 関数で 10 万人想定の集合演算、月初 cron
5 0 1 * *
次にやること(優先順):
- 明日朝 09:00 JST に為替レート cron が Cloudflare から正常発火するか動作確認
- monthly_snapshots cron は 6/1 09:05 JST に初発火予定
- 解約まわり一連 / ドッグフーディング / ダークモード etc.
詰まっていること・未決事項:
- Google Play の名前検索を自動化する手段は現状なし
AGENTS.md/output_logo/は untracked- 独自 SMTP / マネタイズ方針は保留
2026-05-30 16:36 時点のスナップショット
フェーズ: カタログ Phase B(データ投入 + ロゴ整備)完了。次は本番への sync が最大の宿題
今どこまで進んだか:
- 本番 URL:
https://subnote.me(Cloudflare Workers + 独自ドメイン、GitHub Actions cron で為替レート日次更新、メアド許可リストでクローズドベータ) - カタログを 291 サービス / 324 プランまでローカル投入
- ロゴ登録 289 / 291(
is_published=falseの 2 件は対象外) - ロゴ取得自動化機能を実装(iTunes 検索 + Google Play URL hybrid、128×128 保存)
- 管理画面ガードを
ENABLE_ADMINフラグ方式(ローカル限定) - カタログ一覧のロゴクリックで直接アップロードできる
LogoCell - CATEGORIES に「デザイン」を追加
次にやること(優先順):
- ローカル → 本番 sync スクリプト(カタログ + Storage 画像)
- monthly_snapshots の月初自動記録
- ドッグフーディング再開
- リマインダー実装方針の決定
- iOS アプリの技術選定
詰まっていること・未決事項:
- 本番 sync の具体手段
AGENTS.md/output_logo/は untracked- 独自 SMTP / マネタイズ方針は保留
2026-05-21 23:00 時点のスナップショット
フェーズ: サブスクカタログ機能(マスタ + 管理画面 + カタログベース追加 UI)実装完了、Phase B(データ投入)着手前
今どこまで進んだか:
- 本番 URL:
https://subnote.me(Cloudflare Workers + 独自ドメイン、GitHub Actions cron で為替レート日次更新) - データモデル拡張:
service_catalog/service_plans/service-logosStorage bucket /subscriptions.catalog_id - サブスク追加 UI を カタログベース に刷新(検索 + カテゴリチップ + グリッド、プラン選択スキップ最適化、手動入力フォールバック)
- ロゴ表示(catalog_id 経由で logo_url を join 取得、フォールバックはイニシャル)
- 管理画面
/admin/catalog(ローカル限定、サービス + プラン CRUD + ロゴアップロード) - ローカル DB に Netflix / Spotify / Amazon Prime / iCloud+ / GitHub Copilot を投入してテスト
ModalShellにwidthプロップ追加(サブスク追加は 2xl で広く)
次にやること(優先順):
- Phase B 本番運用: 管理画面でサービス + プラン + ロゴを 30〜100 件投入
- ローカル → 本番 sync スクリプト作成(
service_catalog/service_plansの中身と Storage の画像を本番にコピー) - ドッグフーディング再開
monthly_snapshotsの月初自動記録(GitHub Actions cron に追加、全ユーザーループの管理 endpoint が必要)- リマインダー実装方針の決定(メール or Web Push)
- iOS アプリの技術選定と着手判断
詰まっていること・未決事項:
AGENTS.mdとoutput_logo/が untracked のまま残っている(ユーザー側で別途管理 or.gitignoreに追加を要検討)- ローカル → 本番 sync の具体実装(Supabase Storage copy はバケット間転送が必要、psql で
service_catalogを dump+restore する案が現実的) - 独自 SMTP 切替の判断は引き続き保留
- マネタイズ方針は保留
2026-05-18 12:15 時点のスナップショット
フェーズ: 本番稼働 + Web アイコン整備 + 為替 cron 化済、ドッグフーディング期間(〜1週間)
今どこまで進んだか:
- 本番 URL:
https://subnote.me(Cloudflare Workers + 独自ドメイン) - Cloudflare Workers Builds(GitHub 連携、Build cache 有効)で
git push→ 自動 build/deploy - 為替レートは GitHub Actions cron で毎日 09:00 JST に自動更新(CRON_SECRET 認証)
- Web アイコン(紫 S)を ファビコン / Apple Touch Icon に反映
- ロゴに「サブスクをスマートに管理」のサブタイトル + md サイズ拡大
- ログインボタンのデフォルト色を brand-500 に(押せる感を強化)
- 一通りの CRUD + 月切替 + 棒グラフ + 検索 + ソート + 設定(即時保存)+ 連打防止 / autoFocus が本番で安定動作
次にやること(優先順):
- ドッグフーディング(実データで1週間使い込む)
- ドッグフーディングからの改善反映
- iOS アプリの技術選定と着手判断
- monthly_snapshots の月初自動記録
- リマインダー実装方針の決定
詰まっていること・未決事項:
- iOS の技術選定(Web 資産活用するか、ネイティブで作り直すか)
- monthly_snapshots cron は全ユーザーをループする必要がある → 認証なしの管理 endpoint を別途設計
- 独自 SMTP 切替の判断(個人利用なら Supabase 共有で当面 OK)
- マネタイズ方針は引き続き保留
2026-05-17 18:19 時点のスナップショット
フェーズ: 独自ドメイン subnote.me で本番稼働中、自動デプロイ運用、ドッグフーディング待ち
今どこまで進んだか:
- 本番 URL:
https://subnote.me(Cloudflare Workers + 独自ドメイン) - Cloudflare Workers Builds(GitHub 連携、Build cache 有効)で
git push→ 自動 build/deploy - ローカルから直 deploy(
pnpm run deploy)も可能(緊急時用、keep_vars: trueで安全) - Service Role Key 含む全シークレットは Cloudflare Dashboard で Secret として一元管理
- Supabase の Site URL / Redirect URLs は
https://subnote.meのみ(旧 URL は削除済) - 一通りの CRUD + 月切替 + 棒グラフ + 検索 + ソート + 設定(即時保存)が本番で動作
- 連打防止 / autoFocus / モーダル閉じ確実化など UX 細部も対応済
次にやること(優先順):
- ドッグフーディング(実データを入れて自分で使い込む、改善点抽出)
monthly_snapshotsの月初自動記録(Cloudflare Cron Triggers が第一候補)- 為替レート更新の cron 設定(同じ cron 基盤に乗せる)
- リマインダー実装方針の決定(メール / Web Push / アプリ内)
- 必要に応じた UI 微調整
詰まっていること・未決事項:
- スナップショット自動記録の cron 手段選定(Cloudflare Cron Triggers が一番自然)
- 独自 SMTP 切替の判断タイミング(個人利用なら Supabase 共有 SMTP で当面 OK)
- マネタイズ方針は引き続き保留
2026-05-17 14:45 時点のスナップショット
フェーズ: Cloudflare Workers 本番デプロイ済、後処理待ち
今どこまで進んだか:
- Vercel デプロイを経て、Cloudflare Workers (OpenNext for Cloudflare) に移行完了
- 本番 URL:
https://subnote.ichirokisanuki.workers.dev - マジックリンクログイン → ダッシュボードまで本番で動作確認 OK
- Supabase 本番プロジェクト稼働(
qbikwqghzuennnfdsmin)、マイグレーション適用済 - ローカル env を
.env.development.localに整理、.env.production.localには NEXT_PUBLIC_* のみ wrangler.jsoncにkeep_vars: true設定済(deploy 時に Cloudflare Secret 保持)- 細かい改善: ログイン送信ボタン連打防止 / メアド pill 表示・非表示トグル / ヘッダーからログアウト削除
次にやること(優先順):
- Service Role Key を rotate(セッション中に複数回スクショで露出したため、最後にもう一度 rotate して Cloudflare Dashboard で Secret 更新)
- コミット & push(OpenNext 関連の未コミット変更)
- GitHub 連携 + Cloudflare Workers Builds で自動デプロイに切替(ローカル deploy の WARNING と secret 漏洩リスクを根本解消)
- Vercel プロジェクト削除
- Supabase 認証 URL から vercel.app を削除(Cloudflare URL のみに)
- ドッグフーディング(実データ投入)
- 月初スナップショット記録の cron 設定(GitHub Actions or Supabase pg_cron)
- リマインダー実装方針の決定
詰まっていること・未決事項:
wranglerの WARNING で Cloudflare Dashboard の Secret 値が露出される仕様は GitHub Builds に切替えるまで残る- ドッグフーディング起点での改善ネタはまだ無し(実利用してから)
- マネタイズ方針は引き続き保留
2026-05-17 11:59 時点のスナップショット
フェーズ: Web MVP + デザイン磨き完了、ドッグフーディング/本番デプロイ準備
今どこまで進んだか:
- コア機能(認証 / ダッシュボード / サブスク CRUD / 設定 / 為替レート取得)に加えて、月切替・前月比・棒グラフ・タグフィルタ・ソート・検索・件数表示・相対日付を実装
- Claude Design 寄せのビジュアル磨き完了: indigo 系のブランドカラー、Logo 統一、Lucide アイコン、ログイン/ダッシュボード/モーダル/設定の各画面を刷新
monthly_snapshotsテーブルと/api/snapshots/recordを追加(手動 curl / ブラウザでスナップショット記録可)- ローカルで一通り触れる状態
次にやること:
- 自分で実データを入れてドッグフーディング(Netflix / Spotify / Adobe / GitHub Copilot など)
- 月初スナップショット記録の自動化を本番で仕込む(Vercel Cron / pg_cron / GitHub Actions のどれか選定)
- 本番デプロイ(Vercel + Supabase 本番プロジェクト連携)
- リマインダー実装方針の決定(メール / Web Push / アプリ内通知)
- iOS / Android の技術選定(後回し)
詰まっていること・未決事項:
- スナップショット記録の cron 手段(Vercel Cron が第一候補だが pg_cron も検討余地)
- 本番デプロイのタイミング(もう少しローカルで触ってからか、早めに本番出すか)
- shadcn/ui は今のところ未導入で十分形になった(必要になったら入れる)
- マネタイズ方針は引き続き保留
2026-05-17 09:58 時点のスナップショット
フェーズ: Web MVP コア機能完成、ドッグフーディング着手前
今どこまで進んだか:
- 認証(マジックリンク)/ ダッシュボード / サブスク CRUD(一覧・登録モーダル・編集モーダル・削除)/ 設定(表示通貨切替)/ 為替レート取得バッチが一通り動く状態
- ローカルで http://localhost:3000 から実際に登録・編集・削除・通貨切替まで動作確認済み
- Supabase ローカルスタック(Docker)で稼働、Mailpit でマジックリンク受信
- Next.js 16 の
middleware → proxyリネーム警告も解消済み - 一度コミット & push で記録予定(session-wrap-up で実施)
次にやること:
- 自分で実データ(実際の Netflix / Spotify など)を入れてドッグフーディング
- 使ってみての気付き・改善点を ROADMAP「今四半期」セクションに反映
- UI ブラッシュアップ(必要なら shadcn/ui 導入を検討)
- リマインダー実装方針の決定(メール / Web Push / アプリ内通知のどれか)
- 本番デプロイ(Vercel + Supabase 本番プロジェクト連携)
- iOS / Android の技術選定(着手はもっと先)
詰まっていること・未決事項:
- 本番デプロイをいつやるか(ローカルでもう少し触ってからか、早めに本番出すか)
- 為替レート更新の cron 手段(Vercel Cron / Supabase pg_cron / GitHub Actions のどれか)
- スタイリングの最終的なまとまり(現状は素の Tailwind v4、shadcn/ui 導入は未定)
- マネタイズ方針は引き続き保留
2026-05-16 20:23 時点のスナップショット
フェーズ: 設計完了、実装着手前
今どこまで進んだか:
- MVP のコア機能、技術スタック(Next.js + Supabase)、データモデル、画面構成まで設計完了
- CLAUDE.md にプロジェクト概要・設計事項を反映済み
- まだコードは1行も書いていない(プロジェクト初期化前)
次にやること:
- Next.js プロジェクト初期化(
create-next-appで App Router + Tailwind + TypeScript) - Supabase プロジェクトを作成(ダッシュボードで)、URL / anon key を取得
- 環境変数を
.env.localに設定(.gitignore確認) - データモデルの SQL マイグレーションを
supabase/migrations/に配置- subscriptions / exchange_rates / user_settings の3テーブル + RLS
- Supabase クライアント(@supabase/ssr)を組み込み
- ログイン画面(マジックリンク送信フォーム)から作る
詰まっていること・未決事項:
- スタイリングは未確定(Tailwind + shadcn/ui を第一候補にしている)
- 為替レート API の具体的な選定(exchangerate-host が第一候補、無料・登録不要)
- 為替レート更新のスケジューラ(Supabase の pg_cron か、Vercel Cron Jobs か)
- リマインダーの実現方法(メール / Web Push / アプリ内表示)は MVP 後回し
- マネタイズ方針は別途検討
ROADMAP(計画)
ロードマップ
今週
- Next.js プロジェクト初期化(App Router + TypeScript + Tailwind)
- Supabase プロジェクト作成 + 環境変数設定
- データモデルの SQL マイグレーション作成(subscriptions / exchange_rates / user_settings + RLS)
- Supabase クライアント (@supabase/ssr) を組み込み
- Service Role Key を rotate(session-wrap-up 中に露出したため)
- OpenNext / Cloudflare 移行の未コミット変更をコミット & push
- GitHub 連携 + Cloudflare Workers Builds で自動デプロイに切替
- Vercel プロジェクト削除
- Supabase Redirect URLs から vercel.app を削除
- 独自ドメイン subnote.me 取得 + Cloudflare Custom Domain 設定 + 全 URL を subnote.me に揃える
今月
- ログイン画面(マジックリンク送信フォーム)
- 認証コールバック処理
- ダッシュボード(月額合計、タグ別集計、サブスク一覧、検索ボックス)
- サブスク登録/編集モーダル(intercepted route)+ 削除
- 設定画面(表示通貨切替、ログアウト、即時保存方式)
- 為替レート取得バッチ(Frankfurter API から日次キャッシュ)
- 月切替 + 前月比 + 過去12ヶ月の棒グラフ(monthly_snapshots ベース)
- タグフィルタ / ソート / イニシャルアイコン色 / 相対日付(DashboardBoard 刷新)
- Claude Design 寄せのビジュアル磨き(ブランドカラー / Logo / 各画面)
- 一通り動く Web MVP を自分で使い始める(ドッグフーディング、〜1週間)
- Web アイコン整備(紫 S ロゴ、サブタイトル「サブスクをスマートに管理」、md サイズ拡大)
- 為替レート更新の cron 設定(GitHub Actions、日次 09:00 JST)
- サブスクカタログ機能(service_catalog / service_plans + カタログベースの追加 UI + Phase D ロゴ表示)
- 管理画面
/admin/catalog(ローカル限定、サービス + プラン CRUD + ロゴアップロード) - Phase B(ローカル): カタログにサービス + プラン + ロゴを投入 → 291 サービス / 324 プラン / 289 ロゴ完了
- 管理画面ガードを
ENABLE_ADMINフラグ方式に切替(→ さらに 5/30 に middleware のADMIN_EMAILS専用ガードに刷新済み) - ロゴ取得自動化機能(
/admin/catalog/logo-auto、iTunes 検索 + Google Play URL 取得 hybrid、128×128 統一保存、Play ストア検索リンク + URL 貼り付け + ローカルアップロード対応) - ローカル → 本番 sync スクリプト(
service_catalog/service_plans+ Storageservice-logosの 289 画像転送)→ scripts/sync-catalog-to-prod.mjs、本番反映済み -
/admin認可を middleware のADMIN_EMAILS専用ガードに刷新 + 管理ハブ/adminインデックスページ新設(本番でも You だけアクセス可、ローンチ後も/adminだけ絞り続けられる構造) - 為替レート cron を Cloudflare Cron Triggers に移行(GH Actions の課金エラーで連鎖障害を踏んだため、Cloudflare 1 プラットフォームに集約)
- マネタイズ方針の確定(フリーミアム: 無料5件 / Premium 月¥150・年¥1,200・買い切り¥2,500。課金基盤は RevenueCat + Web Billing。詳細は DECISIONS 2026-07-03)
- 課金基盤 Phase 0(
user_entitlements+ 無料5件ゲートの DB トリガー + RevenueCat webhook 受け口。ローカル検証済み、コミット6f194c1。本番 migration 適用は未) - RevenueCat / Stripe の法人アカウント開設(RevenueCat はプロジェクト・Entitlement・Offering まで作成済み。Stripe は審査提出済み・審査中。明細書表記=SUBNOTE)
- Stripe 審査通過確認 → RevenueCat に Web Billing 接続(7/6 完了)
- RevenueCat の残り設定(Apps 登録、商品3つ、Offering、webhook、Cloudflare に Secret 2つ。E2E で発覚した「実商品の entitlement 未アタッチ」も解消済み)
- 本番 migration 適用(20260702000001)+ 管理者アカウントに lifetime 付与
- リリース Phase 1(Web + 課金): 法務3ページ + ペイウォール(日本語チェックアウト・解約済み表示・自己修復同期)+ 実カードでの課金 E2E(購入/失敗/解約/同期、障害2件修正=DECISIONS 2026-07-06)+ ベータガード撤去 → 新規サインアップ E2E 成功 → 告知準備まで再クローズ(DECISIONS 2026-07-06)
- Sign in with Apple の Web 実装(フィーチャーフラグ制・本番未点灯)+ client secret 生成スクリプト
web/scripts/gen-apple-secret.mjs - 手入力サービス名のトリアージ運用(/admin/catalog/requests で名寄せ・3分類 + 却下記録テーブル + プリフィル付きカタログ追加 + 確認付きバックフィル + 日次ダイジェストメール 09:15 JST。本番反映済み
0e09a8d。詳細は DECISIONS 2026-07-09) - Apple Developer 法人審査の通過 → SiwA 設定一式完了・Web 本番稼働(7/13): App ID
me.subnote.Subnote/ Services IDme.subnote.web/ 鍵作成 → secret 生成(期限 2027-01-09)→ Supabase Apple プロバイダ設定 → フラグ点灯 → 本番 E2E 成功。dev-timer #32(12/22)/ #33(1/5)登録済み。relay メールドメイン登録も前倒し完了(apex SPF 追加で緑チェック) - インフラ名義の法人統一(7/13): Supabase を org「RAMANU LLC」へ移管(無停止・ref/キー無変更)+ Cloudflare アカウントを法人化(info@ramanu.co.jp + パスワード/2FA + RAMANU LLC にリネーム)。Apple/Stripe/RevenueCat/Supabase/Cloudflare すべて法人名義に。詳細は DECISIONS 2026-07-13
- App Store Connect のリリース土台(7/13 夜): ASC API キー発行(
subnote-mcp)+ asc-subnote MCP 新設 + Bundle IDme.subnote.app登録(Xcode 側も統一、DECISIONS 2026-07-13)+ アプリレコード「Subnote - サブスク管理」作成(Apple ID6790385156、会社名=Ramanu LLC)。※無料アプリ契約が検証中=TestFlight 前提条件、サブスク課金には有料 App 契約 + 銀行・税務情報が別途必要 - iOS の Codex QA ループ1周(F-001〜F-010 全10件対応、7/15): アカウント削除導線・法務リンク・金額検証・Dynamic Type/レイアウト崩れ・認証エラー日本語化・削除 API 502 の解消まで。本番 E2E 検証済み。詳細は DEVLOG 2026-07-15
- 正式公開(Web): ベータガード撤去 push(7/20 完了、全開放)。告知は下記の残タスクへ分離
- iOS 1.0.0 審査通過 → 正式リリース(7/23)(提出 7/20 → 1回目リジェクト 7/22=Guideline 3.1.2 規約リンク欠落 → 2回目リジェクト 7/22=Guideline 2.3.10 説明文の Android 言及、いずれもメタデータ修正で再提出 → 7/23 審査通過 → 公開ボタン押下で正式リリース。1.0.0 初回は段階的リリース対象外=全ユーザー即時公開。Crashlytics 疎通確認済み。詳細は DEVLOG 2026-07-22〜23 / DECISIONS 2026-07-22)
- カタログ画面スクショの差し替え(新導線版の撮り直し。審査中はメタデータロックのため公開後 or 1.0.1)
- 告知(ASO・告知文面は promo-expert 壁打ち)
- Small Business Program 申請
- GA4 定点観測の立ち上げ(カタログ収載済みサービスの手入力率など。Subnote 用 ga4 MCP 追加)
- iOS アプリ内レビュー導線(前提: 審査通過 → 数日運用して致命的な問題なし): ①好機に System の星評価プロンプト(SwiftUI
requestReview/SKStoreReviewController。Apple 側で年3回まで制限・強制不可なので「サブスク登録完了」等のポジティブな瞬間に控えめに)②設定画面に「レビューを書く」ボタン(App Store write-review 直リンクapps.apple.com/app/id6790385156?action=write-review、ユーザー起点で無制限・確実に開ける)。計測は「表示/完了」ではなく「要求した事実」だけをイベント化(product-ops 知見) - リリース Phase 2(iOS): アカウント削除機能(7/15 完了: 5.1.1(v) 対応、Apple トークン失効・RevenueCat 後始末込みで本番稼働)/ SiwA ネイティブ(7/16 完了: 「登録を完了できませんでした」の真因=新 App ID
me.subnote.appを独立 primary にしていたが Apple バックエンドで機能せず → 旧 primaryme.subnote.Subnoteに group して解決。実機 SiwA 成功 + Web/iOS が同一 Supabase ユーザーに統合されることも実証。Supabase Authorized Client IDs も設定済み確認。DECISIONS 2026-07-16 / DEVLOG 2026-07-16)/ RevenueCat SDK + IAP 商品登録(7/15-16 完了: Codex 実装 + 実機で購入→webhook→Supabase 同期→ゲート解放のフルパイプライン検証済み、DEVLOG 2026-07-16。Sandbox 実購入・商品審査は有料 App 契約が前提のまま)+ Small Business Program 申請 / 認証メールの出し分け(Send Email Hook、DECISIONS 2026-07-06)/ ASC 初ビルド 1.0 (1) アップロード済み(7/16、アイコン登録・VALID) → スクショ・説明文(7/16 夜完了: スクショ v1 を iPhone 6.9×4 + iPad 13×3 で ASC アップロード、IAP 3商品にレビュー用スクショ添付 + グループ表示名/販売地域補完で READY_TO_SUBMIT、App Review 情報・輸出コンプライアンス設定済み、有料 App 契約 有効化。DEVLOG 2026-07-16 22:43)→ スクショ仕上げ(7/17 完了: v2 = iPhone 6.9×6 + iPad 13×3 をマーケ加工(Codex 分業)で制作し ASC アップロード完了・v1 削除。DEVLOG 2026-07-17 / DECISIONS 2026-07-17)→ IAP 込みで v1 審査提出(7/20 完了: 提出物5項目=アプリ 1.0.0 (3) + サブスクグループ + 月額/年額/買い切りで提出、WAITING_FOR_REVIEW。認証メール出し分け(Send Email Hook)も 7/20 本番稼働済み。残: Small Business Program 申請) - リリース Phase 3(Android): RevenueCat SDK + Play Billing 商品登録 + SiwA(ブラウザ OAuth)→ 提出(組織アカウントなので12人テスター要件は免除)。7/23 大きく前進: ①パリティ依頼書 docs/codex-task-android-parity.md → Codex 実装完了(未コミット・レビュー待ち、REPORT あり)②Play Console 土台完了=アプリ「Subnote - サブスク管理をスマートに」作成 + お支払いプロファイル RAMANU LLC + アカウントグループ + 15% サービス手数料登録 ③Firebase Android 登録 + google-services.json 配置。7/23 午後: 実装レビュー→コミット済み(
d90c3cf+ キー設定6a4e880)、RevenueCat Play 連携完了(SA/API有効化/3権限招待/RTDN 疎通まで)。残: upload 署名キー→AAB→内部テスト / Play Billing 商品3種作成→RC 紐付け→購入 E2E / ストア掲載整備(説明文は iOS 言及禁止・スクショ・Data safety・レーティング)/ You=銀行口座(お受け取り方法)追加
今四半期
- 本番デプロイ(Cloudflare Workers + Supabase 本番プロジェクト、subnote.me で稼働中)
- 独自ドメイン稼働(subnote.me、最初から本番ドメイン採用)
- 為替レート更新の cron 設定(GitHub Actions cron で日次運用)
- ドッグフーディングからの改善反映
- 更新日リマインダーの実装(メール or Web Push → 通知はやめ、アプリ内「もうすぐ更新」表示で実現。
next_billing_dateが 3 日以内のサブスクをダッシュボード最上部に集約。詳細は DECISIONS 2026-06-04) - monthly_snapshots の月初自動記録(Cloudflare Cron Triggers
5 0 1 * *+ Postgres SQL 関数record_monthly_snapshotsで全ユーザー一括 upsert、2026-06-01 09:05 JST に初発火成功を確認=ダッシュボードで 2026年5月に値が出る) - iOS 版の技術選定と着手判断(→ SwiftUI(ネイティブ)に決定。Android は将来別実装。詳細は DECISIONS 2026-06-04)
- リポのモノレポ化(
web/ ios/ android/構成。Web 一式をweb/へ移動、supabase//.devnotes//.env.*はルート共有、Cloudflare Builds の Path を/webに変更) - iOS アプリ着手(SwiftUI プロジェクト作成 + supabase-swift 導入 + メール OTP ログイン + ホームのサブスク一覧=換算月額合計・カタログロゴアイコン・pull-to-refresh まで実装)
- iOS の機能拡充(閲覧系: 検索・並び替え / タグ別集計フィルタ / もうすぐ更新、書き込み系: 新規登録・編集・解約・削除 まで実装=実用レベルに到達)
- iOS の残タスク(カタログ選択フロー / 設定画面=表示通貨切替・ログアウト / 解約履歴=復活 / アプリアイコン・起動画面 / 月次推移チャート / 質感=スケルトン・ハプティクス / フォームバリデーション まで実装)= iOS が Web 機能をほぼ網羅 + 第一印象も整備
- iOS の追加磨き・スワイプ削除/解約(一覧行の左スワイプで解約/削除、confirmationDialog + cancel_url ページ起動、
SubscriptionStoreに集約) - iOS/Web に Google ログイン(
signInWithOAuth、マジックリンク/OTP と併用。iOS=URL schemesubnote、Web=signInWithGoogleサーバーアクション + 既存 callback 流用。※Supabase の Google プロバイダ有効化は別途必要。詳細は DECISIONS 2026-06-07) - メール送信基盤を Resend + 独自ドメイン認証に移行(
subnote.meの SPF/DKIM/DMARC を Cloudflare DNS に登録 → Supabase Auth の Custom SMTP を Resend に設定 → マジックリンクがnoreply@subnote.meから INBOX 着信を確認。Supabase Rate Limit「sending emails」200/時。「1h/4通」制限を解消。詳細は DECISIONS 2026-06-07) - Android アプリ着手 → iOS と機能同等まで到達(ネイティブ Kotlin + Jetpack Compose + supabase-kt。メール OTP ログイン / ホーム=ロゴ・検索・並び替え・タグ別・もうすぐ更新・月次チャート / 新規登録カタログフロー / 編集・解約・削除 / 設定・表示通貨切替 / 解約履歴・復活 / スプラッシュ・スケルトン・ハプティクス。emulator で本番 Supabase 接続・全画面実データ確認。詳細は DECISIONS 2026-06-07)
- iOS 月次チャートの「月送り」対応(バータップ=
.chartXSelectionで過去月の合計を見出しに表示、未選択時は当月。Xcode 26.5/iPhone 17 Pro でビルド+初期描画+結線を検証。タップ実挙動のスクショは idb 廃止で自動化不可のためコード精査で代替) - iOS ホーム上部にブランドヘッダー(
safeAreaInsetでブランドマーク「S」40pt + Subnote ワードマーク(largeTitle0.85倍)の左寄せロックアップ。inline ナビバーに並び替え/歯車を残す。高級感狙い) - ヘッダーワードマークの文字ジャンプアニメーション(ランダム1文字が時々跳ねる + タップで即発動。iOS 18
TextRenderer、Codex 分業実装、f837fb4。DECISIONS 2026-07-16) - iOS ウィジェット(Android と対称=ホームに月額合計、WidgetKit)
- Android デザインの実機目視確認 + 質感調整(前セッション未目視4点を確認=チャートラベル切れ修正/編集フォーム grouped/FAB 円形/ダークモード色味、いずれも良好。チップを白ピル+ハイライン枠化・アイコン外周枠・推移チャートの当月浮きラベル廃止)
- Android ホーム画面ウィジェット(月額合計)(Glance。アプリのホーム読み込み時に計算済みテキストを Glance Preferences へ書き込み → ウィジェットは描画専念。エミュレータでアプリ表示とウィジェット表示の一致・タップで
MainActivity起動を E2E 確認) - Android の追加(プラン選択画面の目視確認)
- Supabase の Google プロバイダ有効化を確認 + Android に Google ログインコード投入(
/auth/v1/settingsでexternal.google: true。Android はブラウザ方式 OAuth で iOS/Web と同設定を共有=SHA-1 不要。詳細は DECISIONS 2026-06-07) - Android Google ログインの End-to-End 動作確認(実機推奨)
- メール送信量の監視 → 日100通/月3000通に近づいたら Resend Pro($20/月) へ昇格
- Supabase auto-pause からの復旧 + keep-alive 堅牢化(7日 inactivity で pause された原因=keep-alive の
exchange_ratesupsert が Frankfurter API の成功に依存。手動 unpause + refresh ルートを「為替取得の前に必ず Supabase を read」に変更し Frankfurter の生死から切り離し。詳細は DECISIONS 2026-06-23) - cron 失敗時のメール通知(Resend、worker 専用キー
subnote-worker-cron。worker.ts の callInternal で HTTP エラー/例外時にALERT_EMAIL_TOへ送信、本番疎通確認済み。cron が静かに失敗し続けても気づける。詳細は DECISIONS 2026-06-23)
いつか
- iOS アプリ(→ 今四半期に着手へ前倒し)
- Android アプリ(→ 今四半期に着手へ前倒し、iOS と機能同等まで到達)
- ダークモード切替 UI(設定画面に「ライト / システム / ダーク」3 択、デフォルトはシステム連動。Tailwind v4 を class ベースに、
localStorage永続、FOUC 防止 inline script) - サブスクのプリセット選択(カタログ機能として実装、
service_catalog+service_plansベースで選択 → プラン → フォームのフロー) - 削除ボタンを「解約」にリネーム + 解約ページを開くボタン(編集モーダルを「解約する」+「完全に削除」に分離、
service_catalog.cancel_urlが設定されていればリンク表示) - 解約時の「解約はお済みですか?」中間ダイアログ(cancel_url 登録済みサービスのみ。「解約ページを開く」は新タブで開くだけでダイアログ維持→戻って「解約は済んでいる」で最終確認→解約。URL 未登録は従来通り即確認)
- 月次推移チャートのバーをホバーで料金ツールチップ表示(即時表示、遅延あり native title を廃止)
-
service_catalog.cancel_urlのデータ投入(本番に計 24 件投入済み=主要な動画/音楽/電子書籍をほぼ網羅。残り=niコニコ/DMM TV/楽天系/dマガジン/FOD 等はクリーンな URL 要確認。sync 上書きリスクはsync-catalog-to-prod.mjsの実行ガードで対処済み) - 解約済みサブスクの履歴保存(
subscriptions.canceled_at列追加、集計から除外、/historyページ + 「今月の解約合計」サマリ + 復活ボタン、record_monthly_snapshotsもcanceled_at IS NULLでフィルタ) - 本番運営者向けの管理画面(middleware の
ADMIN_EMAILS専用ガードに刷新し本番でも You だけアクセス可、/adminインデックスページ新設) - タグのサジェスト表示(過去タグの chip 提示、カテゴリ別フィルタ、カテゴリ不明時は 2+ カテゴリの汎用タグだけ、ブラウザ form 履歴を抑制)
- カタログ
default_tagsを 291 件一括投入(機能カテゴリ + ベンダー、2〜3 個/サービス、用途タグは含めない) - タグの色分け(自動 or ユーザー指定) シンプル優先で当面実装しない
- 今月/今年の支出履歴ビュー
- 手動付与 entitlement の分離カラム化(現状は webhook 同期が手動の lifetime を上書きして消す。手動付与=プロモコード等を常用するなら必要)
- Google ログイン用 OAuth クライアント(Google Cloud)の法人名義化(インフラ名義統一の残り1件)
- Cloudflare 法人アカウントに同居する個人ゾーン(crosstechlab.com 等)の分離(必要になったら NS 切替で個別に)
- サービス名サジェスト(公式アイコン・標準料金プランの補完、※カタログ機能で代替済み)
- マネタイズ方針の検討(→ 2026-07-03 に確定、今月セクションのリリース Phase 1〜3 に展開済み)
DECISIONS(意思決定)
意思決定記録
このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。
2026-07-23: ストア共通の正式アプリ名は「Subnote - サブスク管理をスマートに」(Play も ASC 実名称に統一)
背景: Play Console でのアプリ作成時、DECISIONS 2026-07-13 の記録「Subnote - サブスク管理」を基に入力したところ、ASC の実アプリ名は「Subnote - サブスク管理をスマートに」に変更済み(asc-subnote MCP で確認)で記録が古かった。
決定: ストア共通の正式名称を「Subnote - サブスク管理をスマートに」とし、Play のアプリ名もこれで作成。
理由: ストア間でブランド表記を揃える。22/30 文字で Play の30字制限にも収まる。2026-07-13 の記録は当時の決定として残し、本エントリを最新の正とする。
2026-07-23: Play のカード明細書表記は「RAMANU」(Stripe の SUBNOTE とは使い分け)
背景: Play お支払いプロファイルの「クレジットカード明細書の名前」はプロファイル(アカウント)全体共通で、将来別アプリ(Pocket Museum 等)を Play で売る場合も同じ表記になる。Stripe(Web 決済)は DECISIONS 2026-07-03 で SUBNOTE にしていた。
決定: Play は「RAMANU」。Stripe の SUBNOTE はそのまま維持。
理由: Play の欄は製品単位でなく販売者単位のため、製品名 SUBNOTE だと別アプリ販売時に不整合。Play 購入は明細に GOOGLE * プレフィックスが付き文脈が補われるため、Stripe 直課金ほどチャージバック誤認の懸念が高くない。
2026-07-23: Play の関連デベロッパーアカウント申告は「該当なし」で保存
背景: 15% サービス手数料登録の前提としてアカウントグループ「RAMANU LLC」を作成。You が持つ他の Play デベロッパーアカウント(ichirokisanuki 個人 / THOMSON)の申告要否を判断する必要があった。
決定: いずれも申告せず、申告フォーム2問(法人所有の他アカウント / 類似ブランドのアプリ)とも「いいえ」で保存。
理由: ichirokisanuki 個人はお支払いプロファイルを持たず Google の仕様上グループに追加不可(追加を試みてエラーで拒否された)=収益合算の対象になり得ない。実際 2025-01-31 に本人確認未完了で Google Play から削除済み。THOMSON は支配外のため対象外。
2026-07-22: サブスク審査要件の利用規約リンクは自前 subnote.me/terms を採用(Apple 標準 EULA ではなく)
背景: iOS 1.0.0 が Guideline 3.1.2 でリジェクト(自動更新サブスクに対し利用規約(EULA)の機能リンクがメタデータに無い)。App説明文かカスタム EULA 欄に規約リンクを入れる必要が生じた。向き先の候補は (A) 自前 subnote.me/terms、(B) Apple 標準 EULA(apple.com/.../stdeula/) の2つ。
決定: (A) 自前 subnote.me/terms を採用し、App説明文末尾に利用規約・プライバシーポリシーのリンクを追記。
理由: subnote.me/terms・/privacy は本番稼働済み(200 確認)の実在する自前規約ページで、オンブランド。Apple 標準 EULA は Apple の指摘文言に文字通り一致するが汎用規約で、自前の規約があるのにわざわざ Apple 汎用を指すのは不自然。多くのサブスクアプリと同じく自前規約リンクで審査を通す方針。
2026-07-20: Pull to Refresh のカスタム演出は不採用(標準維持)
背景: リフレッシュの体感改善としてブランドカード10枚が円環展開するカスタム演出を実装した。
決定: 実装・動作確認まで完了したが採用せず、システム標準のスピナーに戻す。
理由: 実機で触った結果、標準の方が好みという You の判断。「試して違ったら捨てる」を許容するプロセスを優先。
2026-07-20: iPhone は縦固定、iPad は全方向対応を維持
背景: 横画面レイアウトは一度も検品しておらず、審査に未検証の画面状態を出すリスクがあった。
決定: iPhone は Portrait のみ、iPad は全4方向のまま(1.0.0 に同梱)。
理由: 縦スクロール型ダッシュボードに横画面の実用価値がほぼない。iPad の向き固定は Requires Full Screen 宣言 = Split View/Slide Over 放棄が必要で失うものが大きい。
2026-07-20: Crashlytics を 1.0 に同梱(提出前導入)
背景: クラッシュ可視性が Apple 標準(診断共有に同意した端末のみ)しかなかった。1.0.1 送りも検討。
決定: 提出前に FirebaseCrashlytics + dSYM 自動アップロードを組み込み、ビルド3として提出。
理由: ローンチ直後こそクラッシュ把握が重要で、Firebase 基盤(plist・プライバシー申告フロー)が同日整備済みで追加コストが半日で収まったため。
2026-07-20: QA データは goda 関連を残す(テストユーザーのみ削除)
背景: 公開前の QA データ掃除で、RC の大文字 customer / SiwA テストユーザー / goda のテストサブスク / Xcode 購入由来 lifetime が対象だった。
決定: 前2者は削除。goda のテストサブスクと lifetime entitlement は残す。
理由: goda は継続利用する QA アカウントで、テストデータとプレミアム状態はむしろ検証に有用。デモアカ +asc も撮り直し用に従来どおり保持。
2026-07-19: カタログ登録画面の導線を再設計(検索上部固定・手入力降格)
背景: 検索が iOS 26 標準の下部配置で気づかれず、最上部のアクセント色「手入力で登録」カードに初回ユーザーが流れる懸念(手入力トリアージ運用で実害も確認済み)。三者議論(Claude/ChatGPT/You、.devnotes/discussions/2026-07-19-catalog-entry-flow.md)で検討。
決定: 検索は上部固定の自前フィールド(白地 + アクセント枠 = A案)。手入力は見出し右の控えめリンク + 検索0件時の3段構え(カテゴリ解除 → プリフィル付き手入力)に降格。マイクロコピーは実装後に不採用。
理由: 「例外経路が主役・主経路が脇役」の優先度逆転が本質原因(両AI一致)。検索位置は GPT が「下部維持・計測後判断」を主張したが、プロダクトオーナー自身の実機での「気づけない」体感を初回ユーザーの代弁とみなし上部を採用。
2026-07-19: 行動分析は GA4 + Firebase Analytics で統一
背景: Web/iOS とも計測ゼロで、審査提出前に行動分析基盤を入れると決めた。TelemetryDeck / PostHog も比較。
決定: Web = gtag(@next/third-parties、本番ビルドのみ、測定IDはコード埋め込み)、iOS = FirebaseAnalytics を同一 GA4 プロパティに統合。ユーザー ID は紐付けず「ユーザーに関連付けられないデータ」として申告。
理由: 無料・クロスプラットフォーム統合ビュー・既存の ga4 MCP 運用を Subnote 用に複製できる運用親和性。IDFA 非アクセス構成(ATT 不要)でプライバシー申告も軽い。
2026-07-17: App Store スクショ v2 の構成・スタイル・制作体制を確定
背景: v1(素のシミュレータスクショ)は訴求力ゼロで作り直しが必要だった。枚数・場面・加工スタイル・制作の進め方を決める必要があった。
決定: iPhone 6.9 は6枚構成(①ホーム合計ヒーロー ②カタログ ③一覧$/¥混在 ④月次チャート ⑤プラン選択 ⑥ダーク)、iPad 13 は同スタイル3枚。加工スタイルは「グレーグラデ背景 + 白太字コピー + 黒ベゼル端末の下端見切り」(generator/template.html が正)。制作は raw 撮影=Claude Code / 仕上げ=Codex(テンプレ + shots.json 差し替え式)の分業で、1枚ずつ生成→レビューを往復する。
理由: インディゴ背景はのっぺりして安っぽく(You 判断)、白背景はアプリ画面と同化したため、奥行きのあるグレー × 改善後の構図のハイブリッドが最良だった。当初構成はホーム由来3枚(ホーム/一覧/もうすぐ更新)で画変わりが弱く、「もうすぐ更新」は独立枠を廃止して①のバッジ表示で代替し、代わりにプラン選択を採用。一括自動生成はレビュー粒度が粗くなるため、1枚ずつが You の意向。
2026-07-17: プラン選択画面にサービスアイコンを表示する(アプリ改修)
背景: スクショ⑤の素材となるプラン選択画面が「プランカード1枚だけ + 大量の空白」で寂しく、マーケ画像として弱かった。
決定: スクショ加工でごまかすのではなく、アプリ本体のプラン選択画面(PlanPickerView)にサービスアイコン(76pt)をセンター表示する改修を入れた(ServiceIcon にサイズ指定を追加、26f1406)。
理由: スクショ都合だけでなく実アプリの画面改善としても妥当(どのサービスのプランを選んでいるかが視覚的に明確になる)。加工での偽装は審査上もリスクがある。
2026-07-16: IAP レビュー用スクショは Sandbox の USD 表示のまま提出する
背景: IAP 3商品のレビュー用スクショとして CLI 起動のシミュレータでペイウォールを撮影したところ、未サインイン時のストアフロントが US になるため実 ASC 商品が USD($0.99/$6.99/$14.99)で表示された。¥ 表示にするには Xcode ▶ Run(ローカル StoreKit 設定の有効化)での手動撮り直しが必要だった。
決定: USD 表示のまま3商品のレビュー用スクショとして添付・提出する。撮り直しはしない。
理由: レビュー用スクショは審査官しか見ない資料で、目的は「IAP がアプリ内のどこで売られているか」の提示。Sandbox から実商品が取得・表示された証跡としてはむしろ実態に忠実で、通貨が USD であることが原因のリジェクトは実務上起きない。撮り直しには Xcode GUI の手動 Run が必要でコストに見合わない。気になれば後日 DELETE → 再アップロードで差し替え可能。
2026-07-16: App Review はデモアカウント「不要」+ 即時登録メモ方式で提出する
背景: Subnote はログイン必須アプリのため、App Review のサインイン情報が空だとガイドライン 2.1 でリジェクトされうる。ただしメール OTP はレビュアーがコードを受信できず、従来型のデモアカウント(メール + パスワード)は提供できない。
決定: demoAccountRequired=false とし、notes に「Sign in with Apple またはメール OTP でアプリ内から即時にアカウント作成できる」旨の英文メモを設定する(API で設定済み)。固定 OTP のデモアカウントは実装しない。
理由: SiwA によりレビュアー自身が数タップで実アカウントを作成でき、全機能に到達できるため、デモアカウントなしで通るのが通例。固定 OTP バックドアは審査のためだけに認証系へ追加リスクと工数を持ち込むことになる。万一 2.1 で差し戻された場合にのみデモアカウント実装を検討する。
2026-07-16: ワードマークの文字単位アニメーションは TextRenderer で実装する
背景: ヘッダーの「Subnote」に「時々文字が動く」演出を入れるにあたり、SwiftUI での文字単位アニメーションは「Text を1文字ずつ分解して HStack に並べる」か「iOS 18 の TextRenderer / TextAttribute で描画レイヤーだけ変形する」かの選択があった。
決定: TextRenderer / TextAttribute を採用(customAttribute で文字インデックスを付与し、GraphicsContext で該当スライスだけ translate/scale)。演出は「ランダム1文字ジャンプ(squash & stretch)+ タップ即発動」、頻度は表示4秒後 + 20〜40秒ランダム。実装は AnimatedWordmark.swift に分離(f837fb4)。
理由: HStack 分解はカーニングが崩れレイアウトにも影響する。TextRenderer はテキストレイアウトを保ったまま描画だけ動かせるため、レイアウトシフトなし・Reduce Motion 対応・クリッピング回避(displayPadding)が素直に書ける。今後 iOS で文字単位の演出を足すときも同方式に従う。なお Text の + 連結は iOS 26 で非推奨のため Text 補間で連結する(customAttribute は補間を通っても保持される、実機確認済み)。
2026-07-16: SiwA ネイティブは新 App ID me.subnote.app を旧 primary me.subnote.Subnote に group する
背景: iOS ネイティブの Sign in with Apple 実機 E2E が Apple 純正シートで「登録を完了できませんでした」と失敗し続けた。切り分けの結果、新 App ID me.subnote.app を独立 primary として SiwA 有効化していたが、Portal/ASC でフラグは立っているのに Apple 認証バックエンドで機能せず、別端末×別 Apple ID でも全滅(アカウント要因・端末要因・レート制限・署名要因・「反映待ち」はすべて棄却)。一方、旧 primary me.subnote.Subnote(Web SiwA が使用)は稼働していた。
決定: me.subnote.app を「単独 primary」から「旧 me.subnote.Subnote に group(RELATED_APP_CONSENT)」へ変更する。SiwA の primary は旧 App ID に一本化し、Web(Services ID me.subnote.web) / iOS(App ID me.subnote.app) の両方を同じ primary の下に集約する。旧 App ID は削除しない(グループの primary であり、消すと全 SiwA ユーザーの identity が壊れる)。
理由: 壊れた新 primary を「直す」(capability の OFF→ON 再プロビジョニング)は非破壊でなく(構成リセット + プロファイル無効化)確度も低い。稼働実績のある primary に載せ替える方が、故障の機序に関わらず確実で、Apple 推奨構成(同一サービスは1 primary に集約)でもある。sub は developer team スコープなので grouping で identity は変わらず、実機で SiwA 成功 + Web/iOS が同一 Supabase ユーザーに統合されることを実証済み。aud はネイティブ側 me.subnote.app のままなので Supabase の Authorized Client IDs 設定は不変。三者議論の詳細: discussions/2026-07-16-siwa-signup-not-completed.md
2026-07-16: RevenueCat の app_user_id は小文字 UUID に統一
背景: 実機 IAP 検証で、iOS が UUID.uuidString(大文字)を app_user_id に渡しており、Web(小文字)と同一ユーザーの RevenueCat customer が分裂していた。RC の customer ID は case-sensitive のため webhook 同期(小文字で引き直す実装)が 404 になり、iOS の課金が Supabase の user_entitlements に反映されず、課金済みでも無料5件ゲートに弾かれる状態だった。
決定: RevenueCat の app_user_id は全プラットフォームで小文字の Supabase UUID に統一する。iOS は PurchaseManager 内(configure / handleSignedIn の両方)で .lowercased() に正規化する(e438719)。
理由: Web・webhook・アカウント削除 API はすべて小文字前提で実装済みのため、iOS 側を合わせるのが最小変更で安全。呼び出し側でなく PurchaseManager 内で正規化することで将来の呼び出し追加でも規約が崩れない。Android の RevenueCat 実装時も同じ規約に従うこと(Kotlin の UUID.toString() は小文字だが要確認)。
2026-07-15: iOS v1 は IAP を組み込んでから審査提出する(実装は Codex に委任)
背景: Codex QA ループの完了で iOS はいつでも提出できる状態になったが、アプリ内課金(RevenueCat SDK + IAP)が未実装だった。「IAP なしの無料アプリとして先に出し v1.1 で課金を足す」案と「IAP を組み込んでから出す」案があった。
決定: IAP を組み込んでから v1 を提出する。実装は Codex に委任し、指示書 docs/codex-task-ios-iap.md で依頼する(Claude Code はレビュー・コミット担当)。
理由: リリース直後からマネタイズ導線を揃えたい意向。有料 App 契約待ちの期間も StoreKit Configuration によるローカル検証で実装は先行できるため、待ち時間の無駄が少ない。指示書方式にしたのは、QA ループで確立した Codex との分業(ファイル受け渡し・役割の明文化)が機能していたため。
2026-07-15: アカウント削除の fail-closed は「課金がアクティブなユーザー」だけに限定
背景: アカウント削除 API は当初、RevenueCat customer 削除の失敗で全ユーザーの削除を中断する設計(fail-closed)にしていた。ところが本番の RevenueCat キーが読み取り専用で DELETE が常に 403 になり、無課金の QA アカウントすら削除できない事故(F-010、全ユーザー 502)が起きた。
決定: fail-closed の対象を「premium_until が未来のユーザー(自動更新の課金が生きている可能性がある)」だけに限定。無課金・lifetime ユーザーは RevenueCat 側が失敗しても削除を続行しログに残す。あわせてキー自体も write 権限つきに移行し、旧読み取り専用キーは Revoke した。
理由: fail-closed の本来の目的は「削除後も請求が続く事故(置き去り課金)の防止」であり、請求リスクがないユーザーまで削除をブロックするのは過剰。ユーザーの削除権(審査 5.1.1(v) の必須要件でもある)を外部 API の失敗で塞がない設計にした。
2026-07-15: SiwA を v1 に残し、アカウント削除時の Apple トークン失効を自前実装
背景: 審査ガイドライン 5.1.1(v) はアプリ内アカウント削除を必須とし、SiwA 提供アプリにはさらに「削除時に Apple の REST API でトークンを失効させる」付帯要件がある。Supabase は Apple の refresh token を外部に出さないため標準機能では満たせず、「v1 から SiwA(と 4.8 の関係で Google も)を外す」案と「失効の仕組みを自前実装する」案があった。
決定: SiwA を v1 に残し、失効を自前実装。iOS がサインイン時の authorizationCode をサーバに渡し、サーバが refresh token に交換して apple_refresh_tokens(service role のみ)に保管、削除時に /auth/revoke を呼ぶ。client secret は静的 JWT ではなく .p8(Worker secret)から都度 ES256 署名で生成する。
理由: Web では Apple ログインが本番稼働済みで、iOS だけ外すと「Web で Apple 登録した人が iOS でログインできない」ちぐはぐな体験になるため。都度署名方式にしたのは Apple の client secret 上限6ヶ月の貼り替え運用(Supabase 用で既に負担)を増やさないため。既知の限界として、Web OAuth 経由のトークンは Supabase 内部保持のため失効不可(iOS ネイティブ分のみ対象)。
2026-07-13: iOS の Bundle ID を me.subnote.app に変更(旧確定 me.subnote.Subnote を覆す)
背景: App Store Connect にアプリレコードを作る段になり、紐付ける Bundle ID を最終確定する必要があった。既存 Xcode プロジェクトは me.subnote.Subnote(Xcode デフォルト命名 = プレフィックス + プロジェクト名)で、以前「これで確定」としていた。
決定: me.subnote.app に変更して統一。Xcode の PRODUCT_BUNDLE_IDENTIFIER も書き換え、アプリレコードもこちらに紐付けた。旧 me.subnote.Subnote の App ID は削除しない(Web SiwA の Services ID me.subnote.web がプライマリ App ID として参照している可能性があるため温存)。
理由: 未リリースの今なら変更コストがゼロ(リリース後は変更不可)。me.subnote.Subnote は大文字混じりで Xcode 都合の命名なのに対し、me.subnote.app はプロダクトドメイン(subnote.me)基準で意図が明確。法人ドメイン基準の co.ramanu.subnote 案は、プロダクトの独立性を優先して不採用。
2026-07-13: App Store の会社名=「Ramanu LLC」/ アプリ名=「Subnote - サブスク管理」
背景: ASC の最初のアプリ作成時にのみ設定できる「会社名」(全アプリ共通の開発元表示・以後変更不可)と、アプリ名を決める必要があった。アプリ名「Subnote」単体は他開発者が使用済みで登録不可だった。
決定: 会社名は「Ramanu LLC」。アプリ名は「Subnote - サブスク管理」。
理由: 会社名は Developer Program の登記名 RAMANU LLC に準拠した表記(「Ramanu Inc.」は株式会社相当の表記で合同会社には不適切。Google 合同会社 → Google LLC と同パターン)。アプリ名はブランド名 + 用途タグラインの定番形式で、名前ユニーク制約を回避しつつ検索キーワード「サブスク」も拾える。アプリ名は会社名と違い後からバージョン更新時に変更できるため、リリース後の調整余地もある。
2026-07-13: Supabase の法人移管は「新法人アカウント + org 間 transfer」方式(メール変更ではなく)
背景: 販売主体(合同会社ラマヌ)に合わせて Apple / Stripe / RevenueCat は法人アカウントだが、Supabase だけ個人アカウント(ichirokisanuki@gmail.com)だった。アプリ・プロジェクトが増える前に名義を揃えたい。
決定: info@ramanu.co.jp で新アカウント + org「RAMANU LLC」を作成し、既存の本番プロジェクトを org 間 transfer で移管。個人アカウントは新 org に Developer ロールで招待し、移管は個人アカウント側(移管元 Owner かつ移管先メンバー)から実行。今後のダッシュボード管理作業は会社アカウントで行う。
理由: transfer はプロジェクト ref / URL / API キー / DB / Auth 設定を一切変えず無停止で完了する(コード・Secrets の変更ゼロ)。既存アカウントのメール変更(A案)は最速だが個人と法人が分離できない。個人アカウントを Owner/Admin で招待すると Free 枠が org 横断で合算判定される制約があるため Developer 招待が正解(移管要件は「メンバーであること」で足りる)。
2026-07-13: Cloudflare は「アカウント自体を法人化」方式(新アカウントへの完全移行はしない)
背景: Supabase と同様に Cloudflare も個人アカウント運用だった。ただし Cloudflare には Supabase のようなプロジェクト移管機能がなく、新アカウントへ移すには Worker 再構築(Workers Builds 再接続・Secrets 再投入・カスタムドメイン・NS 切替)が必要。さらに subnote.me は Cloudflare Registrar 登録のため、アカウント間移動はレジストラ転送を伴い高コスト。
決定: 既存アカウントをそのまま法人化する: ログインメールを info@ramanu.co.jp に変更(Google ログイン運用だったため forgot-password でパスワードを先に設定)+ 2FA 設定 + アカウント名を「RAMANU LLC」にリネーム。請求プロファイルの法人名義化は Paid 化時に。
理由: 移行作業ゼロ・ダウンタイムゼロ・Registrar 問題も回避。会社ドメイン ramanu.co.jp が既に同アカウントにあり、実態が会社インフラだった。個人寄りのゾーン(crosstechlab.com 等)が同居するが、Free ゾーンは必要になったとき個別に NS 切替で外へ出せる。完全分離(B案)は外部メンバーへの権限付与や資産切り出しの必要が出てから再検討。
2026-07-09: 手入力サービス名のトリアージ運用(暗黙シグナル検知 + 却下テーブル + 確認付きバックフィル + 日次メール)
背景: ユーザーがカタログ検索でヒットせずサービス名を手入力した場合に、運営が「カタログに追加する価値があるか」を判断して容易に追加できる運用の仕組みが必要になった。
決定: ①検知はユーザー側 UI を変えず subscriptions.catalog_id IS NULL の暗黙シグナルを使う(「追加リクエスト」ボタンは付けない)②却下状態は catalog_review_dismissals(正規化名 PK)で管理し、/admin/catalog/requests で「未対応 / カタログ一致 / 却下済み」にトリアージ ③カタログ追加後の既存登録への紐付け(バックフィル)は件数提示 + 確認ダイアログを挟んで実行 ④気づき方は日次メール(毎日 09:15 JST、新しい手入力があった日のみ、既存の Resend 基盤を再利用)。
理由: ①手入力という行動自体が完全なシグナルで追加 UI は摩擦にしかならない ②カタログ追加済みの名前は normalized_name 一致で自動的に一覧から消えるため、記録が必要なのは「却下」だけ — テーブルを最小に保てる ③ユーザーの行を運営側から書き換える操作なので無確認では実行しない ④admin バッジ案・ntfy 案もあったが、運営者(You)の希望でメール + 管理画面リンクに。cron 失敗通知と同じ経路なので新しいインフラ・secret が不要。
背景: iOS は Google ログインを載せている以上、審査ガイドライン 4.8 で Apple ログイン等の提供が必須。一度「iOS から Google を外して SiwA 非実装」に傾いたが、参考にしている MoneyForward ME が Apple ログインを提供していることを確認し再検討した。SiwA には「Hide My Email で relay アドレスになった人が他プラットフォームで別アカウント化する」問題がある。
決定: Sign in with Apple を採用し、3プラットフォーム同時に投入する(Apple で始めた人がどの環境でも Apple ボタンで同一アカウントに入れるようにする)。Web 実装は完了済み(フィーチャーフラグ NEXT_PUBLIC_ENABLE_APPLE_LOGIN で Apple Developer / Supabase 設定完了まで非表示)。iOS / Android ネイティブは Phase 2。
理由: ワンタップログインの摩擦削減と 4.8 対応を両立でき、relay アドレス問題は「全プラットフォームに Apple ボタン」で構造的に回避できる。付随コストも整理済み: ①アカウント削除機能が必須(5.1.1(v)、SiwA 有無に関係なく iOS リリース要件)②Web/Android 用 client secret(JWT)は最長6ヶ月で失効し半年ごとの再生成が必要 → 生成スクリプト(web/scripts/gen-apple-secret.mjs、依存ゼロ)を用意し、リマインダー運用でカバー ③relay 宛メール到達には Apple へのドメイン登録が必要。
2026-07-06: RevenueCat の同期は v2 API を使う(v1 は v2 secret key で 403)
背景: 課金 E2E で「購入成功なのに premium が反映されない」障害が発生。調査の結果、webhook / 即時同期が呼ぶ v1 /subscribers エンドポイントが、ダッシュボードで発行した v2 secret key では 403 になることが判明(現行ダッシュボードで発行できるのは権限スコープ付きの v2 キーのみ)。さらに購入商品が entitlement premium に未アタッチ(アタッチされていたのはオンボーディング自動生成の placeholder 商品)という設定ミスも重なっていた。
決定: 同期は v2 /projects/{id}/customers/{id}/active_entitlements に移行。判定は「有効エンタイトルメントが1つでもあれば premium、expires_at が null なら lifetime」(このプロジェクトのエンタイトルメントは premium 1つだけという前提)。project id はコード内定数。あわせて /premium 表示時にも自己修復の同期を走らせる(購入後にリダイレクト等でクライアントの購入ハンドラを経ずに戻ってきた場合の救済)。
理由: v1 キーの発行手段が実質なく、v2 移行が唯一の恒久策。webhook の TEST イベントは同期をスキップする仕様のため、この種の問題は「本番の初購入」まで顕在化しない — 実カードでの E2E をリリース前にやる価値がまさにここにあった。
2026-07-06: プラン名の表記は「Subnoteプレミアム」(カタカナ)に統一
背景: チェックアウト画面の商品名を見て表記を再検討。一度「Subnote Premium(欧文)維持」と決めたが、ユーザーの意向で翻意。参考アプリの MoneyForward ME もカタカナ「プレミアム」派。
決定: 正式名称「Subnoteプレミアム」、文中の短縮形は「プレミアム」。UI 文言・利用規約・特商法表記・RevenueCat の商品 Display Name を統一(コード識別子 isPremium / premium / premium_monthly 等の内部名は変更しない)。
理由: 日本向けサービスとしての親しみやすさを優先。内部識別子を巻き込まないことで変更コストを表示層だけに閉じた。
2026-07-06: 公開 E2E 検証後にクローズドベータへ再クローズ(正式公開は告知準備が整ってから)
背景: ベータガードを撤去して一般公開し、新規サインアップ E2E(メール着信 → ログイン → 登録)まで検証完了した。が、告知・プロモーションの準備ができていない状態で開けっ放しにする理由もなかった。
決定: ベータガード(ADMIN_EMAILS 以外のログインユーザーを 404)を復元して再クローズ。法務3ページ・ログイン・お問い合わせは公開のまま(ストア審査の提出 URL に使える)。正式公開時は同ブロックを再度削除するだけ。
理由: 「公開できる状態であることの検証」と「公開し続けること」は別の判断。検証は完了済みなので、公開のタイミングを告知戦略(promo 検討)と同期させる方が初速を作れる。再公開のコストは1コミットで、失うものがない。
2026-07-06: 認証メールの Web / アプリ出し分けは Send Email Hook で Phase 2 に実装
背景: マジックリンクのメールが「Web はリンク、アプリはコード」を1テンプレートで兼ねており、文面がぎこちない。Supabase の標準テンプレートは1種類で、ログイン元による出し分けができない。
決定: 当面は両対応の統合テンプレート(Web 用サインインボタンを主、アプリ用6桁コードを従で併記)で運用。出し分けは Supabase の Send Email Hook(自前エンドポイント + Resend 送信、redirect_to の有無で Web/アプリを判別)で Phase 2 に実装する。
理由: 現時点のログインは 100% Web 経由(アプリ未公開)で、統合テンプレートでも実害がない。Hook 化は「ログインメールの単一障害点を自前コードが負う」運用責任を伴うため、アプリ公開と同じタイミングで導入するのが効率も必要性も釣り合う。
2026-07-03: マネタイズはフリーミアム(無料5件)+ 月¥150 / 年¥1,200 / 買い切り¥2,500 の3本立てで有料リリース
背景: ストア公開が視野に入り、長らく保留だったマネタイズ方針を確定する必要があった。当初「完全無料で出してユーザーとレビューを先に集める」案で一度合意したが、You の意向で「サブスク + 買い切りを用意して有料でリリースする」に方針転換。選択肢は完全無料 / 買い切り型 / サブスク型 / 広告。
決定: フリーミアム。無料はアクティブなサブスク登録5件まで、Premium(月¥150 / 年¥1,200 / 買い切り¥2,500)で無制限。価格は「破格にしたい」意向と手数料15%控除後の採算の折衷で「控えめライン」を採用。3プラットフォームで同一価格。
理由: 登録件数制限はサブスク管理アプリの定番で、価値を実感してから自然に課金に到達する導線になり、機能ゲートも1箇所で済む。買い切りを併売することで「サブスク削減アプリがサブスク課金」という心理的抵抗を回避できる。¥100 と ¥300 で購入率はほぼ変わらないため、最安ではなく年額・買い切りをお得にする設計を優先。広告は実装・審査負荷と UX 毀損で不採用。
2026-07-03: 課金基盤は RevenueCat(Web は Web Billing)、Supabase に webhook 経由の entitlement キャッシュを持つ
背景: 月額/年額/買い切りを iOS(StoreKit 2)/ Android(Play Billing)/ Web の3箇所で売り、購入状態を3プラットフォームで同期する必要がある。Web の決済は Stripe 直接実装か RevenueCat Web Billing かの選択もあった。
決定: RevenueCat を採用(App User ID = Supabase の user UUID、Entitlement 識別子 = premium)。Web の決済も RevenueCat Web Billing(自社 Stripe アカウント接続型)。Supabase 側は user_entitlements テーブルを webhook 経由のキャッシュとして持ち、webhook はイベント種別で分岐せず毎回 REST で subscriber を取り直して同期する方式(web/lib/revenuecat.ts)。
理由: レシート検証・購入状態の3プラットフォーム統合を自前実装すると数週間級。RevenueCat は月売上 $2,500 まで無料で、Web Billing も追加の RevenueCat 手数料なし(Stripe 決済手数料のみ)・買い切り対応・エンタイトルメント自動統合を確認済み。Stripe 直接だとチェックアウト実装 + webhook + RevenueCat への送り込みを全部書く割に得るものが少ない。webhook を「REST で取り直す」方式にしたのはイベントの順序入れ替わり・取りこぼしに対して自己修復的だから。premium 判定を保存値からその都度計算する(lifetime or premium_until > now())のも、EXPIRATION の取りこぼしで premium が残留しないため。
2026-07-03: 無料プランの登録上限は Postgres トリガー1箇所で強制する
背景: 無料5件の上限をどこで強制するか。Web はサーバーアクション経由だが、iOS / Android は Supabase に直接書き込むため、クライアント実装だけでは突破できてしまう。
決定: subscriptions への BEFORE INSERT / UPDATE トリガー(migration 20260702000001)で「アクティブ行が増える操作」(新規 insert と解約済み→復活)だけを対象に上限を強制。advisory lock で同時実行のすり抜けも防止。クライアント側の件数チェックはペイウォール表示のための UX に徹する。
理由: 強制ポイントを DB 1箇所に集約すれば、3プラットフォームのどこから書いても必ず効き、集計ロジックが3箇所ある規律(TS/SQL/Swift)のような多重管理を増やさずに済む。上限エラーは 'SUBSCRIPTION_LIMIT_REACHED' を含むメッセージで raise し、クライアントはそれで識別する。Web のサーバーアクションは上限エラーだけ typed error で返す(本番の Server Action は throw のメッセージがマスクされ文言が届かないため)。
2026-07-03: 販売主体は法人(合同会社ラマヌ)、リリース順序は Web(決済込み)→ iOS → Android
背景: リリース形態の棚卸しの中で、販売主体(個人 or 法人)と公開の順序を決める必要があった。Apple Developer Program / Google Play Console はいずれも法人名義で登録済みと判明。
決定: 販売主体は法人(Stripe / 特商法表記 / 各アカウントとも法人名義、メールは info@ramanu.co.jp で統一)。リリースは Web を決済込みのフル構成で先行公開 → iOS → Android。カード明細書表記は会社名ではなく SUBNOTE。Stripe の Managed Payments(+3.5%)と Stripe Tax は不採用・スキップ。
理由: ストアの販売元・決済・法務表記が一つの主体に揃い、ねじれがない。Web 先行はストア審査なしで最速で世に出せるため。Play の「12人×14日クローズドテスト」要件は個人アカウント限定で、組織アカウントのラマヌには適用されないと判明(iOS 先行の理由が一つ減ったが、Sign in with Apple 実装が必要なため順序は維持)。明細書表記をユーザーが認識しているブランド名にするのは、身に覚えのない請求と誤認されてチャージバックになるのを防ぐ定石。日本国内向け・単一税率で Managed Payments / Stripe Tax に追加手数料を払う意味がない。
2026-07-03: bundle ID は現状維持(ramanu を焼き込まない)
背景: RevenueCat への Apps 登録を前に、iOS me.subnote.Subnote / Android me.subnote.android に会社名(ramanu)を入れるべきか迷いが出た。ストア初回公開後は実質変更不可のため、変えるなら最後のタイミングだった。
決定: 現状維持。iOS = me.subnote.Subnote、Android = me.subnote.android。
理由: bundle ID の慣例は「所有ドメインの逆順」であり、subnote.me を所有している以上プロダクトドメイン基準は完全に正当。むしろ「社名変更・事業譲渡」は「Subnote が Subnote でなくなる」より起こりやすく、会社名を焼き込む方が将来の陳腐化リスクがある。法人の存在はストアの販売元表示・特商法表記・Stripe 名義に出るので bundle ID に入れる必要がない。変更作業(Xcode + Android パッケージリネーム)に見合う実益もない。
2026-06-23: keep-alive を為替取得から切り離す(Supabase auto-pause の再発防止)
背景: Supabase 無料プロジェクトが7日 inactivity で auto-pause された。keep-alive は Cloudflare Cron Triggers で日次 /api/exchange-rates/refresh を叩き exchange_rates に upsert する仕組みだったが、旧コードは4通貨のうち1つでも Frankfurter API が !res.ok を返すと upsert 前に return 502 していた。Frankfurter(個人運営の無料 API)が数日落ちた間、Supabase に一切アクセスが発生せず pause を踏んだ。cron は発火していたが「書き込む中身が出てこなかった」だけ。
決定: refresh ルートを「Frankfurter を呼ぶ前に Supabase を必ず1回 read(select fetched_at limit 1)する」設計に変更。これが pause 防止の本体で、為替取得が全滅しても activity が立つ。あわせて1通貨失敗で全体中断せず取れたぶんだけ upsert し、健全性を HTTP ステータスで表現(keep-alive/upsert 失敗=500、為替全滅=502、部分失敗=200)。
理由: keep-alive を第三者 API(しかも SLA のない無料 API)の生死に人質に取らせない。為替 API を exchangerate.host 等に乗り換える案も検討したが、無料枠が狭く(月100リクエスト=日4回 cron だと超過)キー管理も増える上、どの API でも落ちれば同じ事故が起きるため「keep-alive と為替取得を分離する」を本筋とした。Frankfurter は普段は十分機能するので維持。
2026-06-23: cron 失敗時は Resend で管理者にメール通知(専用キーを分離)
背景: 今回の障害は cron が console.error するだけで、誰も Cloudflare ログを見ておらず1週間気づけなかったのが「対策したのに止まった」の隠れた一因。失敗を能動的に検知する手段が必要だった。
決定: lib/notifications/send-email.ts に Resend の HTTP API を fetch で叩く sendAlertEmail() を新設(依存追加なし、env 未設定なら no-op)。worker.ts の両 cron の関所 callInternal で、ルートが HTTP エラー or 例外を返したらメール送信。Resend キーは既存の Supabase SMTP 用(subnote-smtp)とは別に worker 専用キー subnote-worker-cron(Sending access / domain=subnote.me)を新規発行し、Worker env に RESEND_API_KEY/ALERT_EMAIL_FROM(alerts@subnote.me)/ALERT_EMAIL_TO を設定。
理由: 通知先は ntfy も候補だったが、You の要望でメールを採用。送信基盤は既に Resend + subnote.me ドメイン認証が整っていたので最小コストで載せられた。キーを用途別に分けるのは、片方をローテーション/失効させてももう片方が死なないため(=今回の「単一障害点で全部巻き込む」事故と同じ轍を踏まない)。
2026-06-07: Android の Google ログインはブラウザ方式 OAuth で実装(SHA-1 不要・設定は3プラットフォーム共有)
背景: Android に Google ログインを投入するにあたり、Supabase + Android では2方式がある。① ブラウザ(Custom Tab)経由の OAuth(signInWith(Google) → Supabase の /auth/v1/callback → アプリへ subnote://login-callback で復帰)、② ネイティブ Google Sign-In(Credential Manager で ID トークンを取得 → signInWith(IDToken)、ブラウザを開かない)。当初 WIP には「Android は OAuth クライアントに SHA-1 とパッケージ名登録が追加で必要」「Supabase の Google プロバイダ有効化が未設定」と記録されていた。
決定: ① ブラウザ方式を採用(iOS の ASWebAuthenticationSession と対称)。リダイレクトは iOS と同一の subnote://login-callback に揃え、scheme/host 設定 + manifest の intent-filter + handleDeeplinks で実装。
理由: ブラウザ方式では Google から見たリダイレクト先が常に Supabase の https callback なので、Google Cloud 側に必要なのは Web タイプの OAuth クライアント1個だけで、Web/iOS/Android が同じクライアント・同じ callback・同じ subnote://login-callback を共有できる=設定はプロジェクト単位で1回きり、Android 用の追加ポチポチも SHA-1 も不要。実際に GET /auth/v1/settings(apikey=publishable)で external.google: true を確認=プロバイダは既に有効で、WIP の2つの記述(「未設定」「SHA-1 必要」)はいずれも誤り(②の方式と混同していた)と判明。②のネイティブ方式はブラウザを開かない UX 上の利点があるが、Android タイプ OAuth クライアント + SHA-1 登録 + 実装変更が要り、現状の「3プラットフォーム共通・最小設定」という方針と釣り合わないため見送り。将来ネイティブ UX が欲しくなった時のために debug SHA-1(0A:A6:9B:9E:A2:18:60:03:56:64:08:3E:C8:2E:B1:F1:18:18:03:2C)だけ控えておく。
2026-06-07: Android ホームの推移チャートから当月の浮き金額ラベルを廃止
背景: 推移チャートは当月バーの上に金額ラベル(例 ¥32,962)を出していたが、当月が最大値だとバーが満杯になりラベルがカード上端に接近して窮屈に見えた。前セッションでバー領域計算を調整して「切れ」は解消済みだったが、見た目の余白がまだ気になる状態。選択肢は ① 上端の余白をさらに増やす、② ラベルを「推移」見出しの右へ移設、③ ラベル自体を廃止。
決定: ③ 廃止。BarChart に showCurrentLabel 引数を足し、ホームでは false を渡してバー上のラベルを出さない(バー領域も広がる)。
理由: ホームでは月額合計をグラデーションのヒーローカードでチャートのすぐ上に大きく出しており、当月バーのラベルは同じ数字の重複だった。②の見出し右移設も結局ヒーローと重複する。チャートの役割は「推移の形(当月=アクセント色 + 月ラベルで現在地は分かる)」を見せることに絞り、正確な数字はヒーローに任せるのが最もすっきりする。BarChart 自体は引数のデフォルト true でラベル表示も残してあり、ヒーローが無い文脈では従来通り使える。
2026-06-07: Android 版の技術スタックをネイティブ Kotlin + Jetpack Compose に決定
背景: iOS 版が一段落し Android 着手のタイミング。CLAUDE.md では「iOS/Android: 未定(Supabase 公式 SDK あり、認証は Web と共通化可)」のまま技術未選定だった。選択肢は ① ネイティブ Kotlin + Compose、② Kotlin Multiplatform / Compose Multiplatform、③ Flutter / React Native。
決定: ① ネイティブ Kotlin + Jetpack Compose + supabase-kt を採用。iOS の SwiftUI と対称な「薄いネイティブ層が Supabase を叩く」構成にし、認証/DB/RLS/集計の共通層は Supabase 側に寄せる既存方針を踏襲。
理由: 保守性は一般論ではなく「今ある資産との整合」で決まる。iOS を既にネイティブ SwiftUI で作り込み、共通化すべき層は Supabase に寄せてある現状では、コード共有が売りの ②③ の旨味は薄い。② は既存 SwiftUI を流用できず構成が一段重くなるだけ、③ は Web=Next.js / iOS=SwiftUI と技術がバラバラで統一メリットが出ず iOS 作り直し前提になる。①ならパフォーマンス(OS 標準描画で最速)と保守性(iOS と対称・公式 SDK で認証共通化)を両立できる。②③が活きるのは「iOS も含めて作り直す」前提のときだけで今回は割に合わない。
2026-06-07: メール送信基盤を Supabase 共有SMTP → Resend + 独自ドメイン認証に移行
背景: これまで認証メール(マジックリンク/OTP)は Supabase 内蔵の共有SMTP で送っており「1時間4通」程度の厳しい制限があった。CLAUDE.md にも「本番では独自SMTP に切替予定」と記載済みで、ローンチ前の必須項目だった。別プロジェクト review-antenna は Resend を採用していたが送信元が onboarding@resend.dev(共有サンドボックス・宛先は自分限定)で、不特定ユーザーに送る Subnote には流用できなかった。
決定: メール配送を Resend に統一し、subnote.me を独自ドメインとして認証(SPF / DKIM / DMARC を Cloudflare DNS に登録)。Supabase Auth の Custom SMTP に Resend を設定(送信元 noreply@subnote.me)。Supabase Auth の送信 Rate Limit は 200/時 に設定。DMARC は p=none(監視のみ)で開始し、運用安定後に段階強化する方針。
理由: Resend は Cloudflare/Workers と相性が良く、無料枠(日100通/月3000通)でベータ〜数百人を賄える。Cloudflare で DNS を管理しているためドメイン認証も即時に通せた。SPF/DKIM だけでなく DMARC も入れてなりすまし対策と到達率を確保。「メール送信の制御(回数・文面・トークン)は Supabase Auth、実配送は Resend」という役割分担になるため、実効上限は常に「Supabase Rate Limit と Resend 配送枠の小さい方」になる点を運用メモに残す(最初に来る課金判断は Resend Pro=$20/月・月5万通)。MailChannels は無料終了、Amazon SES は初期設定が重いため不採用(review-antenna の選定理由を踏襲)。
2026-06-07: 認証手段に Google OAuth を追加(マジックリンク/OTP と併用)
背景: これまで認証はマジックリンク(Web)/ メール OTP(iOS)のパスワードレス一本だった。メールが届くまでの待ち時間や、メールアプリ往復の手間がログインの摩擦になっており、ワンタップで入れる選択肢が欲しかった。
決定: iOS / Web の両方に Google ログイン(Supabase signInWithOAuth(provider: .google))を追加。既存のマジックリンク/OTP は残し、併用する。iOS は subnote://login-callback の URL scheme、Web は signInWithGoogle サーバーアクションで OAuth URL に redirect し、既存の /auth/callback(exchangeCodeForSession)を流用。
理由: Supabase 公式 SDK が iOS/Web 双方で OAuth を同じ枠組みで提供しており、callback も既存の code フローをそのまま使えて追加コストが小さい。パスワードレスの方針は崩さず、ユーザーの入り口を増やせる。Apple / その他プロバイダは将来必要になったら追加(まず利用者が多い Google から)。実稼働には Supabase の Google プロバイダ有効化 + Google Cloud OAuth クライアント設定が前提となる点は運用メモに残す。
2026-06-05: iOS の起動第一印象とブランド表現の方針
背景: 「機能が単純なアプリほど、起動した瞬間の質感で品質の印象が決まる」という方針が示され、アプリアイコン・起動画面・ブランド表現を優先的に整備した。当初アイコンを indigo 角丸+S で自動生成したが、Web ヘッダーの実ロゴ(スタックカード+セリフS)と別物で「味が消える」と指摘された。
決定:
- アプリのブランドマークは Web の実ロゴ(スタックカード + セリフ S)を使う。 低解像度(128px)しか無い場合の機械的なベクター再現は不可(手描きの味が失われる)→ ユーザー提供の高解像度データ(1254px)を正とする
- スプラッシュ・起動背景は白(ロゴが映え、白フラッシュも回避)。アクセントカラーはブランド indigo (#6366f1)
- 月次推移チャートはデータがある月だけ表示する(空バーは「¥0 使った」と誤解されるため出さない。新規ユーザーは推移セクション自体を非表示)
理由: 単純な UI ほどブランドの一貫性とディテールが効くため、Web と完全一致したマークで起動体験を作る。再現でお茶を濁すより本物の素材を使う。チャートは「データ不足」を空バーで誤魔化さず、貯まってから見せる方が正直で誤解が無い。
2026-06-04: iOS の認証はメール OTP(コード)方式、AGENTS.md/output_logo/ は git 管理する
背景: iOS のログイン方式として、マジックリンク(メールのリンクをタップ)はディープリンク(Universal Links / カスタムスキーム + リダイレクト設定)が必要で初期コストが高い。また長年 untracked だった AGENTS.md(他エージェント用指示書)と output_logo/(カタログのロゴ元素材)の扱いが未決だった。
決定:
- iOS は メール OTP(6桁コード)方式でログイン(
signInWithOTP→verifyOTP)。ディープリンク設定は当面不要。ただし Supabase の Magic Link メールテンプレートに{{ .Token }}を含める前提 AGENTS.mdとoutput_logo/は git 管理する(untracked のままにしない)
理由: OTP コード方式はモバイルで設定が最小(ディープリンク不要)で、コードを手入力するだけ。将来ネイティブのマジックリンク体験が欲しくなったらディープリンク対応を追加すればよい。AGENTS.md は CLAUDE.md と対の指示書で versioning が自然、output_logo/ はカタログ素材でリポに残す価値があるため、片付け TODO はこの方針で解消する。
2026-06-04: iOS は SwiftUI(ネイティブ)で実装、リポは web/ ios/ android/ のモノレポ構成
背景: Web MVP が一段落し iOS の技術選定フェーズに。候補は SwiftUI / React Native / Expo / Flutter。Web が Next.js(React/TS)なので RN/Expo はクロスプラットフォーム+知識地続きの利点があり、当初はその線が有力に見えた。一方ユーザーは「SwiftUI を採用した本格アプリが少ない/見た目のこだわりに不都合では」という懸念と、React Native のデメリット(アップグレード保守・将来のプッシュ/ウィジェットでの native 依存・"write once" の限界)を検討した上で、iOS 体験最優先の方針を選んだ。
決定:
- iOS は SwiftUI(ネイティブ)で実装。Supabase は公式 Swift SDK(supabase-swift)で Web と共通バックエンド(Auth / Postgres / RLS)を参照
- Android は将来別実装(SwiftUI は iOS 専用というトレードオフを受容。着手時に Kotlin/Compose 等で再選定)
- リポジトリは
web/ ios/ android/のモノレポ構成にする。supabase/(migrations)・.devnotes/・.env.*はプラットフォーム共有としてルート据え置き、scripts/は JS 依存のためweb/配下
理由: subnote の UI は SaaS 型(リスト/フォーム/カード/チャート)で、SwiftUI が苦手とする超カスタム表現・重い描画には当たらず、むしろ純正の質感と開発速度の利点が活きる。RN を避ける動機は「見た目の不利」よりも、保守コストと native 依存の総合判断。クロスプラットフォーム性を捨てる代わりに iOS 体験を最大化する方針を優先した。モノレポは将来の android/ も同じリポで対称に扱え、共有バックエンド(supabase/)を1箇所に保てる。
2026-06-04: 更新日リマインダーは「通知」をやめ、アプリ内「もうすぐ更新」表示のみで実現する
背景: MVP のコア機能として「更新日リマインダー」を掲げていたが、実現方法(メール / Web Push / アプリ内表示)が未決だった。メール通知は基盤導入(Resend/SendGrid + 日次送信 cron + ドメイン認証)が必要で残タスク中で最も重く、Web Push は Service Worker/VAPID 等で実装が重い上 iOS の制約も大きい。一方で「そもそもリマインダー要らないのでは」という再検討もあった。
決定:
- 能動的な通知(メール / Web Push)は当面やらない
- 更新日リマインダーはダッシュボードのアプリ内表示で実現する。
next_billing_dateが今日〜3日以内のサブスクを最上部に「もうすぐ更新」サマリとして集約(過去日付・未設定・解約済みは除外)
理由: subnote のコアバリュー(サブスクを一覧可視化して無駄を炙り出す)はリマインダー通知が無くても成立する。アプリ内表示は新規インフラ不要・低コストで体験を前進させられ、各行から編集画面(解約導線)へ繋げられる。メール基盤の重い投資は、必要性が固まってから or アプリ展開時のネイティブプッシュとあわせて再検討する方が筋がよい。ソロ開発のクローズドベータ段階ではスコープを絞るのが健全。
2026-06-01: service_catalog.cancel_url の「正」は本番、本番に直接投入する
背景: cancel_url の投入方法を検討中、ローカル DB は cancel_url が全 291 件ゼロ、本番のみ Netflix が /admin/catalog 経由で設定済み、と判明した。「ローカルを正にして sync で本番反映」か「本番に直接投入」かの選択を迫られた。なお sync-catalog-to-prod.mjs(ローカル→本番)は service_catalog を全カラム upsert するため、再実行すると本番の cancel_url が空で上書きされる。
決定:
- cancel_url の書き込み先・正は 本番とする。
scripts/seed-cancel-urls.mjsで本番に直接投入(PROD_SUPABASE_URL + service role key を環境変数で渡す方式、既存の Netflix と同じ場所) - 第一弾は自信のある消費者向け管理 URL のみ 11 件。SaaS(公開解約 URL 無し)や日本系(要確認)は除外
理由: ユーザーが普段 prod /admin/catalog でカタログを編集しており、Netflix も既に本番に直接入れていた。運用実態に合わせて本番を正とするのが自然。ローカルを正にする案は将来の sync で安全になる利点はあるが、既存の本番データ(Netflix)を backfill する手間と運用変更が必要で、今回は見送り。ただし sync 再実行で cancel_url が消えるリスクは未解決の宿題として残る(ローカルにも入れる or sync を cancel_url 除外にする、で対処予定)。
2026-05-31: テーマ切替は 3 択(システム / ライト / ダーク)、デフォルトは「システム」、保存は localStorage のみ
背景: 当初「ライト / ダークの 2 択」で実装し、マネタイズ対象(Premium 限定機能)に入れる方向で相談した。しかし OS にライト/ダーク自動切替の概念があることがユーザーの認識になく、また「ライト/ダーク自体を有料化」は free ユーザーの夜間体験を壊して stick になる懸念があった。一方ユーザー側からも「面倒だからマネタイズに入れるのやめる」「デフォルトでシステム連動、明示的なライト/ダークも選べるように」と方針が示された。
決定:
- トグルは 3 ボタン(☀️ ライト / 🖥 システム / 🌙 ダーク)、並び順は「ライト / システム / ダーク」(システムを中央)
- デフォルトは「システム」(OS の
prefers-color-schemeに追従、matchMediaの change イベントもリッスンしてリアルタイム反映) - 永続化は
localStorageのみ(DB に保存しない、user_settingsへの列追加もしない) - 描画前の
app/layout.tsxの inline script でhtml.darkを当てて FOUC を防ぐ、<html suppressHydrationWarning> - マネタイズ対象から外す
理由: テーマ切替はマネタイズの「目玉機能」にしてしまうと free ユーザーが「ダークモードがほしいから課金」という stick 型の動機付けになり、Notion / Slack / GitHub など標準的な SaaS の慣習(ダークモードは無料)からも乖離する。DB 永続化(端末横断)は将来 Premium カスタマイズ(アクセントカラー等)を導入する際の付加価値として残しておく方が筋がいい。当面は localStorage で十分。
2026-05-31: タグサジェストは「カテゴリ別の自分の過去タグ chip」+「カテゴリ不明時は 2+ カテゴリの汎用タグだけ」
背景: 当初実装は「自分の全タグから入力済みを除いた候補を chip で出す」というシンプルな v1 だった。しかし v1 では Dropbox(ストレージ)の編集中に「配送」(nosh で使われていた)が候補に出るなど、カテゴリ無関係なノイズが入り混じる問題が判明。ブラウザの form 履歴サジェストも別ブラウザ・別端末で機能しない上に表記揺れを助長するので、まず無効化したい。
決定:
- タグ入力に
autoComplete="off"を付与してブラウザの form 履歴サジェストを抑制 - アプリ側の chip サジェストはカテゴリ別に絞り込む:
- 編集対象のカテゴリが分かるとき(catalog 紐付けあり / 新規追加で catalog 選択後): そのカテゴリで使われたタグだけを表示
- カテゴリ不明(手動入力サブスクの編集等): 「2 つ以上のカテゴリで使われている汎用タグだけ」を表示(1 カテゴリ専用の specific タグは出さない)
- chip クリックで
+ タグ名を入力末尾に追加、入力済みは候補から除外、上限 24 件 - 入力欄を controlled 化(chip クリックで状態同期する必要があるため)
理由: 「Dropbox に配送」「YouTube Music に配送」のような明らかな違和感を消すには、カテゴリ文脈をサジェストに反映するのが最短。カテゴリ不明時に「全部出す」を維持すると同じ問題が再発するので、「カテゴリ専用タグは隠す」方針で fallback を絞る。代償としてカテゴリ専用タグは fallback では出ないが、catalog 紐付けすればちゃんと出るので、ユーザーへのインセンティブとしても妥当。「タグの色分け」はシンプル優先で当面実装しない(ROADMAP のいつかから外す)。
2026-05-31: カタログ default_tags の設計方針は「機能カテゴリ + ベンダー、2〜3 個/サービス、用途タグは入れない」
背景: 291 サービスのカタログに default_tags を一括投入するにあたり、タグの方針を決める必要があった。タグの粒度・命名・包含する次元を決めずに進めると、後で表記揺れや方針ブレで再投入になりかねない。
決定:
- 機能カテゴリタグ(具体的、サービス横断で集計しやすい):
動画/音楽/AI/ストレージ/パスワード/翻訳/会議/通信/食事/宅配等 - ベンダータグ(主要なもの限定):
Apple/Google/Microsoft/Adobe/JAL/Amazon─ 横断集約("Apple 系サブスクの合計" など)に効く - 形式タグ(任意、1 つ):
ストリーミング/クラウド/バンドル等 - 1 サービスにつき 2〜3 個
- 入れないもの:
- 用途タグ(
仕事/プライベート/家族共有等)← ユーザー個別判断、カタログで決めない - プラン依存(
個人/ファミリー/学割)←service_plansで表現済み - カテゴリ名そのままの抽象タグ(
ツール/その他)← 意味なし
- 用途タグ(
理由: ダッシュボードのタグ集計を「機能」「ベンダー」の 2 軸で意味のある単位にまとめたい("動画系" "Apple 系" 等)。用途タグを混ぜると指標が user-specific になって平均化できないし、カタログ側で押し付けるべきでもない。ベンダーは Apple One や Microsoft 365 のような複数サービス所有の典型パターンに刺さるので入れる価値がある。canceled_at-based の今月の解約合計や monthly_snapshots の jsonb tag_totals とも整合する。
2026-05-31: 「解約」と「削除」を別概念として分離し、解約は履歴に残す
背景: これまで編集モーダルの「削除」は文字通り DELETE だけで、「サブスクを辞めた」記録が残らなかった。ロードマップにも「削除→解約ボタン化 + 解約済み履歴 + cancel_url 連携」を一連で挙げていた。一連で見ると「解約」と「削除」は意味が違うのに同じ UI に収まっているのが歪だった。
決定: subscriptions.canceled_at timestamptz NULL 列を追加し、canceled_at IS NULL=アクティブ、非 NULL=解約済み として扱う。編集モーダルの「削除」ボタンを「解約する」(赤枠ボタン、主要操作)と「完全に削除」(小さい text link、誤操作防止のため目立たせない escape hatch)に分離する。集計(月額合計、タグ別、monthly_snapshots)はすべて canceled_at IS NULL でフィルタする。解約済みは /history で見れる(解約日 desc)+ 復活ボタンで canceled_at = NULL に戻せる。カタログ起源で service_catalog.cancel_url があれば「サービスの解約ページを開く ↗」リンクも編集モーダルに出す(カタログのデータ整備後に有効)。
理由: 「解約」と「削除」の混在は (a) 過去契約していたサブスクの記録が失われる、(b) 「最近何を解約したか」が後追いできない、(c) 誤操作で大事なデータが消える、という 3 つの実害がある。分離することで履歴を残しつつ、誤操作も 2 段階で防げる(解約 = ボタン、削除 = 小さいリンク + 強い confirm)。代替案として「Soft delete 一本化」(deleted_at だけにして UI で見せ方を切り替える)も考えたが、「これは間違いだから跡形なく消したい」というケース(テストデータ・重複登録)も普通にあるので、それぞれ別操作にした方が直感的。
2026-05-31: 解約履歴のサマリ指標は「累計節約額」ではなく「今月の解約合計額」を採用
背景: 解約履歴ページに「累計節約額」(解約後の経過月 × 月額換算の総和)を出すか相談したところ、ユーザーから「累計はいらない、今月いくら分解約できたかが見れたら嬉しい」とフィードバック。
決定: /history の上部サマリは「今月の解約 ¥X,XXX / 月相当 (N件)」だけ表示する。各行には契約期間ではなく解約日と月額換算を出すに留め、累計節約額の計算と表示はしない。
理由: 「累計節約額」は実体としては「もし契約し続けていたら払っていた仮定額」で、解約から時間が経つほど際限なく大きくなり、数値の意味が薄まる(モチベ用のいい話ではあるが、運用情報としての価値は低い)。一方「今月の解約合計額」は「この月、いくら分の固定費を削れたか」という単発の意味のある指標で、リアルタイムの行動結果として読める。後者の方が日々のサブスク管理のフィードバックループに寄与する。
2026-05-30: 月次スナップショットは「ユーザーループ」ではなく Postgres の集合演算で全ユーザー一括処理する
背景: 月初に全ユーザー分の monthly_snapshots を記録する仕組みが必要。素朴に「ユーザーごとに 1 回ずつ集計 → API ホップ → upsert」というループにすると、ユーザー 1 人あたり 100ms としても 10 万人で 2.7 時間かかり Cloudflare Worker の scheduled 制限を超えてしまう。途中失敗時のリトライも難しい。
決定: 計算ロジック(通貨換算 / yearly→月割 / タグ別 jsonb 集計)を Postgres の SQL 関数 record_monthly_snapshots(target_month text) に移植し、1 つの WITH 句で全ユーザー一括 upsert する。Cloudflare Worker は cron をトリガしてこの関数を呼ぶだけ。関数は SECURITY DEFINER + service_role のみ EXECUTE 許可で保護。Worker → /api/snapshots/record-all(CRON_SECRET ガード)→ supabase.rpc('record_monthly_snapshots') の経路で叩く。
理由: 集合演算は Postgres が最も得意とする領域で、100 万行スケールでも数秒で完走する。ユーザーループは API ホップ・メモリ・失敗復旧の全観点で破綻する。代替案として Cloudflare Queues でのファンアウト(リトライ自動化が魅力)と pg_cron(Supabase 側で完結)も検討したが、(a) 観察容易性は Worker 経由が一番(Cloudflare の observability で「いつ何件処理したか」が見える)、(b) Queues は単純な定期実行に対しては overengineering、(c) pg_cron 拡張の有効化は将来パターン、で SQL 関数 + Worker cron に落ち着いた。TS の calcTotal を SQL に書き直す初期コストはあるが、一度書けば終わり。
2026-05-30: 為替レート cron を GitHub Actions から Cloudflare Cron Triggers に移行する
背景: GitHub Actions の支払いエラーで 5/22 以降 9 日連続失敗していたことに気付かず、Supabase 無料プランの「7 日 inactivity で auto-pause」を踏んで project が pause された。同じ事故を繰り返さないために、cron インフラを GH Actions に依存させる構成を見直したい。
決定: GH Actions の cron-daily.yml を撤去し、Cloudflare Cron Triggers に移行。OpenNext for Cloudflare が生成する .open-next/worker.js をラップする worker.ts を新設し、scheduled ハンドラで internal fetch(worker.fetch(req, env, ctx))から /api/exchange-rates/refresh を叩く。GH の CRON_SECRET は削除、Cloudflare 側の CRON_SECRET のみ残す。
理由: Cloudflare は subnote の本番ホスト先なので 1 プラットフォームに集約できる。Cloudflare Cron Triggers は Workers の無料枠で動くので追加課金が一切ない(GH Actions は個人アカウントの billing 問題で連鎖障害が起きる)。OpenNext の worker.js は HTTP しかハンドルしないが、main を custom ラッパに差し替える + wrangler.jsonc の triggers.crons に追加する 2 手で scheduled も乗せられる。代替案として「GH Actions billing を直して維持」も検討したが、根本的に「他リポの課金状況に左右される脆さ」が残るのでやめた。
運用上の注意:
pnpm run deployをopennextjs-cloudflare deploy→wrangler deployに変更(OpenNext deploy が custom main を尊重するか不明なため安全側)。Cloudflare Workers Builds の自動デプロイも wrangler 経由なので無問題tsconfig.jsonでworker.tsと.open-next/**を typecheck から除外(worker.tsは.open-next/worker.jsを import するがnext build時には未生成)- 2 つの cron は
event.cronで識別して分岐(0 0 * * *→ 為替、5 0 1 * *→ 月次 snapshot)
2026-05-30: /admin の認可を ENABLE_ADMIN フラグから middleware の ADMIN_EMAILS 専用ガードに切替
背景: 5/22 に入れた ENABLE_ADMIN フラグは「ローカル本番モード(:3001)でも /admin が開けるように」が動機だった。だが本来欲しいのは「ローンチ後も /admin だけ管理者メアドに絞り続ける」モデルで、フラグ方式だと「環境ごとの ON/OFF」と「ユーザーごとの権限」が混ざって筋が悪い。中期的にも /admin を本番運用者だけに公開する方向に進めたい。
決定: app/admin/layout.tsx から ENABLE_ADMIN チェックを撤去し、認可は middleware.ts で一元管理する。middleware に「/admin/* パスは ADMIN_EMAILS(既定 ichirokisanuki@gmail.com)でログイン中のメアドのみ通す、それ以外は 404」というガードを常時(dev/prod 共通)追加する。既存の「本番では ADMIN_EMAILS 以外を全パス 404」というクローズドベータ用ガードはそのまま残し、ローンチ時にそのブロックだけ削除すれば自動で「/admin だけ admin 限定 + 一般は普通にサインアップして使える」状態になる。
理由: 中期的に「/admin の予測可能性を気にしてランダム URL にする?」という相談があったが、middleware の email 許可リストが iron-clad に効くので URL を秘匿する必要はない(漏れる経路の方が多い)。フラグ方式は「環境スイッチ」、許可リストは「ユーザー権限」と性質が違うので、認可ロジックは middleware に寄せる方が役割分担として正しい。代替案として layout 側でも email チェックする二重ガードも検討したが、middleware が必ず先に走る以上意味が薄く、コードが分散するのでやめた。あわせて /admin インデックスページ(カタログ管理 / ロゴ取得自動化 / 外部 Dashboard リンク)も新設した。
2026-05-23: 登録ロゴは 128×128 統一、ネイティブ画像処理ライブラリは導入しない
背景: カタログのロゴ画像が取得元ごとにサイズバラバラだった(App Store 512px、Google Play サイズ可変、ローカルファイルは原寸)。一覧やダッシュボードでの表示サイズに対して過剰な解像度をストレージに保存する状況。揃えたいが、サーバー側で sharp 等のネイティブ画像処理ライブラリを入れると Cloudflare Workers ビルドが壊れるリスクがある。
決定: すべての登録パスで 128×128 統一。実装は追加依存ゼロで:
- mzstatic(App Store アイコン): URL の
/<W>x<H>bb.jpgを/128x128bb.jpgに書き換えてから取得 - googleusercontent(Play アイコン): URL に
=s128を付けて取得 - ローカルファイルアップロード: ブラウザ canvas で中央クロップしてリサイズ → PNG Blob を Server Action に送る
actions.ts の resizeLogoUrl ヘルパが mzstatic と googleusercontent の両方に対応。
理由: sharp 等を依存に入れると Cloudflare Workers のバンドルサイズや互換性リスクが上がる。admin はローカル専用とはいえコードは共通ビルドに含まれるので避けたい。CDN 側のリサイズトリックとブラウザ canvas で同等の結果が得られる。128px はカタログ一覧(h-8 〜 h-10)やダッシュボード(h-10)の表示用途として十分。
2026-05-23: ロゴ取得自動化の取得元は iTunes(名前検索)+ Google Play(URL/ID 指定)のハイブリッド
背景: ロゴ未登録の 118 件を自動で埋めたい。当初「Google Play から取得したい」と方針を決めて google-play-scraper@10.1.2 を導入したが、search()(名前検索)が現状 0 件しか返さないことが判明(instagram / Netflix / メルカリ / Shopify 全部 0 件)。Google が Play 検索ページの構造を変えて scraping をブロックしており、ライブラリの既知の未解決問題。Google には公式の Play 検索 API も無く、Google 画像検索 API も無料枠(Custom Search 100 件/日)で実用性は低い。
決定: 役割を二分する hybrid:
- 名前で候補検索 = iTunes Search API(公式・無料・無認証、レビュー画面の「これで登録 / 別の候補」候補に使う)
- Google Play からの取得 =
gplay.app({ appId })(パッケージ ID 指定なら安定動作)。レビュー画面に「Play ストアで探す ↗」リンクと URL 貼り付け欄を置き、ユーザーが Play ストアのページを開いて URL をコピー → 貼り付け →gplay.app()で本物の Play アイコンを取得、という導線にした - 入力欄は画像 URL でも Play ストア URL でも appId 単体でも受け付ける(
play.google.comを含むか、http(s) で始まらない場合は Play 扱い)
理由: Play search() の安定的な実現手段が現状ない以上、「名前→候補」は iTunes に倒すのが現実解。一方 gplay.app() は安定して動くので、Play のロゴが必要な個別ケースは URL 貼り付けで吸収できる。iOS アプリアイコンと Android アプリアイコンはほぼ同一のことが多く、resizeLogoUrl が両方の URL 形式に対応しているので登録パスは共通化できる。
2026-05-22: 管理画面のローカル限定ガードを NODE_ENV から ENABLE_ADMIN フラグに変更
背景: /admin 配下を本番(subnote.me)で 404 にする目的で、admin layout に if (process.env.NODE_ENV === "production") notFound() を入れていた。だがローカルで next start(:3001、本番ビルドの動作確認用)も NODE_ENV=production を強制するため、:3001 でも /admin が 404 になり、本来速いはずのローカル本番モードで admin が触れなかった。NODE_ENV では「Cloudflare 本番」と「ローカル本番ビルド」を区別できない。
決定: ガード条件を process.env.ENABLE_ADMIN !== "true" に変更(許可リスト方式)。.env.development.local に ENABLE_ADMIN=true を追加して pnpm dev(:3000) と pnpm start(:3001、env を読み込んで起動) では admin を開けるようにする。Cloudflare 側にはこの変数を絶対に設定しない(設定しない=本番では従来どおり 404)。.env.example にも記載してドキュメント化。
理由: ブロックリスト方式(NODE_ENV で本番だけ閉じる)は環境分類が雑で、ローカル本番ビルドが巻き添えになる。許可リスト方式(ENABLE_ADMIN フラグ)は「明示的に開けた環境でだけ admin が動く」というセキュリティ的にもより安全な姿勢に変わる。middleware のメアド許可リストとあわせて二重ガード。.env.development.local は NODE_ENV=production の Cloudflare ビルドでは読み込まれないので、誤って本番に値が混入する経路もない。
2026-05-18: サブスク追加 UI を「カタログ検索 + プラン選択 + フォーム」の3ステップに刷新
背景: 従来の「サービス名を手で打つ」フォームでは、ユーザーがサービス名・料金・タグを正確に入れる手間が大きく、表記揺れも起きやすい。サブスクは多くが既知のサービスなので、運営が用意したマスタから選べる方が UX が良い。
決定: 新規登録モーダルを以下のフローに変更。
- 検索ボックス + カテゴリチップ(動画 / 音楽 / AI / ツール / ストレージ / エンタメ / ビジネス / ゲーム / 開発 / その他) + サービスのグリッド表示
- サービスタップ → そのサービスのプラン一覧(1プランしかなければスキップ)
- プラン選択 → 料金などプリセット入りのフォーム → ユーザーが確認・微調整して保存
「該当なし」のフォールバックとして「手動で入力」リンクをモーダルヘッダー右に常時置く。
理由: 検索 + ビジュアル選択でユーザーの入力負荷を最小化、サービス名の表記揺れを抑える、ロゴ表示でブランド感を出す。データの一貫性向上にも繋がる。
2026-05-18: カタログとプランは service_catalog / service_plans の2テーブル、subscriptions には catalog_id のみ参照
背景: 1サービスに複数プランがある場合(Netflix の広告付き / スタンダード / プレミアム)、料金マスタをどう持つかを検討。
決定: service_catalog(サービス本体)と service_plans(プラン、service_id で1対多)の2テーブル構成。subscriptions には catalog_id だけを持たせ、plan_id は持たない。プラン情報は「フォームに料金を自動入力するための辞書」として使い、ユーザーが確定した amount は subscriptions にスナップショットとして残す。
理由:
- プランが将来変動・追加されてもユーザーの契約値は影響しない
- 「Netflix のスタンダードを選んだ」より「Netflix を 1590円で契約してる」が本質
- DB スキーマもシンプル
2026-05-18: カテゴリはコードに固定リスト、マスタの初期データはマイグレーション外で手動投入
背景: カタログのカテゴリと初期データの管理方法。
決定:
- カテゴリは
lib/service-catalog.tsのCATEGORIES定数(動画 / 音楽 / AI / ツール / ストレージ / エンタメ / ビジネス / ゲーム / 開発 / その他)に固定 - 初期データはマイグレーションには含めず、管理画面 or Supabase Studio で手動投入する
理由: カテゴリは滅多に増減しないのでコード固定で十分。初期データはまだリリース前で頻繁に増減するため、git の版管理対象にしない方が運用が軽い。
2026-05-18: 管理画面 /admin はローカル開発専用、本番では 404
背景: カタログマスタの管理 UI が必要だが、Supabase Studio だけだとロゴアップロード + プラン管理が手間。専用画面が欲しいが、運営者を絞る認証実装は段階的にやりたい。
決定: /admin 配下を「ローカルでだけアクセス可能」にする。app/admin/layout.tsx で process.env.NODE_ENV !== "production" なら notFound() を呼ぶことで、本番デプロイ時には /admin 配下が完全に存在しない URL になる。認証チェックは省略(ローカルなので不要)。ローカルで管理 → 本番に sync する運用。
理由: 認証実装の手間を回避しつつ、運用初期段階では十分。データ件数が増えてユーザー数が増えたタイミングで、ホワイトリスト or admin ロールベースの本番管理画面に昇格する余地を残す。
2026-05-18: ロゴ画像はオリジナルで保存、表示時に CSS で角丸化
背景: カタログのロゴ画像をどの形(角丸加工済み / 矩形)で保存するか。
決定: Storage には オリジナル(加工なし)の正方形画像 を保存。表示時に Tailwind の rounded-lg で角丸にする。推奨サイズは 128×128(最大 64px 表示を Retina 2x で十分)、PNG(透過対応)or SVG。
理由: 後から角丸サイズを変えたい時にも一括で CSS だけで済む。同じ画像を異なるサイズ・形状で再利用しやすい。iOS アプリで使う際の自由度も保てる。
2026-05-18: 為替レート日次更新は GitHub Actions cron で運用
背景: ROADMAP に「Cloudflare Cron Triggers が第一候補」と書いていたが、OpenNext for Cloudflare で scheduled handler を扱うには custom worker entry の調整が必要で、設定コストが高かった。
決定: GitHub Actions の schedule cron で毎日 00:00 UTC (09:00 JST) に https://subnote.me/api/exchange-rates/refresh を Bearer 認証付き POST する。CRON_SECRET は Cloudflare Worker Secret と GitHub Secrets の両方に設定。
理由: Worker のコードを触らず、外部の信頼できるスケジューラから API を叩くだけで完結。GitHub Actions は無料・信頼性高い・workflow_dispatch で手動実行も簡単。後で月初の monthly_snapshots 記録も同じ仕組みで追加できる。
2026-05-18: ファビコンは app/icon.png 単独運用、favicon.ico は使わない
背景: ChatGPT で作った S アイコンを Web に反映する際、.ico に変換すると Chrome タブで白縁が出る(透明背景が崩れる)問題が発生。
決定: app/favicon.ico を削除し、Next.js App Router の規約に従って app/icon.png(512×512、透過 PNG)+ app/apple-icon.png(180×180)のみで運用する。
理由: モダンブラウザは PNG ファビコンに完全対応しており、.ico は不要。PNG なら透過維持が確実で、ImageMagick の .ico 変換時の透過崩れに悩まされない。Next.js App Router が自動で適切な <link> タグを生成してくれる。
2026-05-18: /api/exchange-rates/refresh は CRON_SECRET 必須で cron 専用化
背景: 当初は認証ユーザーがブラウザから叩いて手動更新できる設計だった。cron 化に合わせて、認証要件を見直し。
決定: CRON_SECRET 環境変数が設定されている限り、Bearer 認証が必須(未認証なら 401)。これにより、ユーザーがブラウザから叩く運用は廃止し、cron 専用化する。
理由: Frankfurter API のレート制限を悪意あるアクセスから守れる。為替レートは日次更新で十分(個人サブスク管理用途)。設定画面に手動更新ボタンを置かない方針も継続。
2026-05-18: ロゴにサブタイトル「サブスクをスマートに管理」を常時表示
背景: タブ・サマリー画面・ログイン画面で「Subnote」だけだとサービスの内容が伝わらない。
決定: ロゴコンポーネントを2行レイアウトに変更し、「Subnote」の下に小さく「サブスクをスマートに管理」を表示。
理由: ブランド名だけでは初見でサービス内容が分かりにくい。1行のキャッチコピーを常時添えることで「初めて見る人にも何のツールか伝わる」状態にする。ヘッダーが少し縦に伸びるが、サイズ感の調整(md を h-10 + text-xl)で違和感ない範囲に収めた。
2026-05-17: 独自ドメインは最初から subnote.me を直接(中間ドメインを挟まない)
背景: 「ベータは subnote.ikapps.com で運用し、本格リリース時に subnote.me に切り替える」案を当初検討。しかし、ドメイン移行には Supabase URL / cookie / リダイレクト等の更新作業が発生する。途中で乗り換えるなら最初から本番ドメインで始めた方が手間ゼロ。
決定: Cloudflare Registrar で subnote.me を取得(年額 $13〜18)し、Cloudflare Workers の Custom Domain として直接登録。
理由: ドメイン取得コストは年間 $20 以下、ベータ運用時点から固定 URL を持っておくと共有・ブックマーク・SEO 的に有利。ikapps.com 上のサブドメインを挟むと将来の移行コストが増えるだけ。
2026-05-17: Cloudflare Workers Builds (GitHub 連携) で自動デプロイ運用、ローカル直 deploy は緊急時のみ
背景: ローカル pnpm run deploy だと .env.local 系のローカル値が Worker に紛れ込む事故が多発、wrangler の WARNING で Secret 値が露出する問題もあった。
決定: GitHub 連携で Cloudflare Workers Builds に自動 build/deploy させる。ローカルからの pnpm run deploy は緊急時の保険として残す(wrangler.jsonc の keep_vars: true で安全)。
理由: Cloudflare 上のビルド環境で Build variables を使うので、ローカル env が deploy に介在せず、シークレット漏洩リスクが構造的に下がる。Build cache 有効化で 2回目以降は十分速い。
2026-05-17: サブスク CRUD 後の画面遷移は Server Action の redirect() ではなく Client 側で実行
背景: Cloudflare Workers + Next.js 16 (OpenNext) + experimental-edge middleware の組み合わせで、Server Action 内の redirect() がモーダル閉じ + 一覧戻りまで安定して動かなかった(環境依存)。
決定: Server Action 内では redirect("/") を呼ばず、revalidatePath("/") のみ実行する。Client 側 (SubscriptionForm の wrapper) で mode === "modal" の場合は router.back()、それ以外は router.push("/") + router.refresh() を呼ぶ。
理由: intercepted route のモーダルは router.back() で確実に閉じる。Server Action の redirect 経由より動作が予測可能で、Cloudflare 環境差を気にせず済む。未認証時の redirect("/login") は Server Action 内で維持する(こちらは安定して動く)。
2026-05-17: ログアウトはダッシュボードヘッダーから削除、設定画面のみに集約
背景: ダッシュボードヘッダーに「設定」と「ログアウト」両方のアイコンがあって紛らわしい。設定画面にもログアウトボタンがあり重複。
決定: ダッシュボードヘッダーは「設定アイコン」のみに統一。ログアウトは設定画面側だけに置く。
理由: 誤操作を防ぎつつ、ヘッダーを軽量に保つ。ログアウトは滅多に押さないアクションなので、設定画面まで遷移する手間は許容。
2026-05-17: デプロイ先を Vercel → Cloudflare Workers (OpenNext for Cloudflare) に変更
背景: Vercel に初回デプロイして動作確認はできたものの、Hobby プランの規約が「非商用利用のみ」で、商用は Pro $20/月。個人ツールの初期段階で月固定費を出すのは重い。代替として Lightsail(既存)と Cloudflare Pages を比較。
決定: Cloudflare Workers (OpenNext for Cloudflare) に移行。https://subnote.ichirokisanuki.workers.dev で稼働。
理由: 商用利用が明示的に OK、Free 枠が広い(月10万リクエスト、CPU 時間 10ms は I/O 待ち除外で十分)、エッジ配信で高速、超えても Workers Paid $5/月でスケール、Lightsail の自前運用より楽。Vercel Hobby のグレー運用 / Pro 課金より筋が良い。
2026-05-17: Next.js 16 の middleware は middleware.ts + runtime = "experimental-edge" で運用(Cloudflare 互換のため proxy.ts を捨てる)
背景: Next.js 16 で導入された新しい proxy.ts は Node.js runtime 固定で、Cloudflare Workers にデプロイできない(OpenNext がエラーで止まる)。
決定: proxy.ts を削除して middleware.ts を再作成し、export const runtime = "experimental-edge" を付与。
理由: Cloudflare Workers は Edge Runtime のみ。Next.js 16 では middleware.ts は deprecated 警告が出るが機能としては動作し、唯一 Edge Runtime を指定できる。experimental-edge は Next.js が要求した正確な値(単なる edge だとビルドが拒否される)。Cloudflare をやめるまでは proxy.ts に戻さない。
2026-05-17: ローカル開発用 env file は .env.development.local を使う
背景: .env.local は Next.js のロード規則で dev / build 両方で読まれるため、ローカル Supabase の URL や Service Role Key が pnpm run deploy 時に本番ビルドへ埋め込まれ / Worker の vars として送信される事故が連発した。
決定: ローカル開発用の env は .env.development.local に置く(dev 時のみ読まれる)。本番ビルドで読ませたい値は .env.production.local に置く(commit 対象外)。SUPABASE_SERVICE_ROLE_KEY は env file に書かず、Cloudflare Dashboard で Secret として登録する。
理由: Next.js の env ロード順序の罠を避け、ローカル開発と本番デプロイを env file レベルで完全分離する。Secret は file に書かず Cloudflare の Secret ストアに集約する。
2026-05-17: wrangler.jsonc に keep_vars: true を入れる
背景: pnpm run deploy(= wrangler deploy)が走るたびに Cloudflare Dashboard で設定した Secrets が全削除され、ローカルの設定で上書きされる事故が発生(WARNING も出ていたが見落とした)。
決定: wrangler.jsonc に "keep_vars": true を追加。
理由: Dashboard 側で運用する Secret を保護するため。ローカル deploy で Cloudflare 側の運用設定を勝手に上書きしないのが運用上の正解。本来は GitHub 連携 + Cloudflare Workers Builds に切り替えてローカル deploy 自体を廃止するのが筋(次の改善項目)。
2026-05-17: 前月比は monthly_snapshots テーブル方式 + 月切替は「表示中の月」切替
背景: Claude Design のデザイン案にあった「↓ 4.1%(前月比)」と「期間切替」をどう実装するか検討。直近30日の擬似差分や、subscriptions に active_from/until 列を持たせる案も並べた。
決定: monthly_snapshots(user_id, year_month, total, subscription_count, tag_totals, display_currency, ...) を新設し、月単位で合計をキャッシュ。前月比は「対象月」と「前月」の snapshot を比較。月切替 UI は (a)「表示中の月を切り替える」方式(?month=YYYY-MM)。今月(live)は subscriptions から計算、過去月は snapshot から取得。
理由: 履歴を持たないと前月比は不正確、ライブ計算では「過去の状態」を再現できない。snapshot を持てば期間切替・棒グラフも一緒に乗る。スキーマも小さく、月初 cron で記録するだけのシンプル運用。範囲指定や年額換算期間切替は MVP では複雑すぎるので採用しない。
2026-05-17: サブスクのプラン名は memo フィールドで代用
背景: Claude Design では「Adobe Creative Cloud · コンプリートプラン」「Notion · Plus (年払い)」のようにプラン名が併記されていた。専用カラムを足すか検討。
決定: 専用カラムは追加せず、既存の memo テキストにユーザーが書く運用とする。
理由: MVP の DB スキーマを安定させたい。プラン名を持つことで表示要件が増える割に、本質的に集計や検索に効かない(memo で十分検索もかかる)。本当に必要なら後から plan_name 列を追加する。
2026-05-17: ブランドカラーは @theme の brand-50..900(indigo 相当)
背景: Claude Design 由来の紫系(ロゴ・ボタン・選択中タグ・棒グラフ・focus ring・通知系のリンク)が必要だった。indigo-500 直書きを散らかすか、ブランドトークンを切るか。
決定: app/globals.css の @theme に --color-brand-50 〜 --color-brand-900 を定義(値は Tailwind indigo 相当)。アプリ内のブランド表現は bg-brand-500 text-brand-700 focus:ring-brand-500 のように brand-* 経由で参照する。
理由: Tailwind v4 のカスタムカラー手段に揃えることで、後でブランド色を青→緑などに振り替えたいときに一箇所で済む。indigo-* を直書きするとプロジェクト固有の意味が薄くなる。
2026-05-17: サブスクのイニシャルアイコン色はサービス名ハッシュで自動決定
背景: Claude Design ではサブスクの行頭アイコンがブランドカラー(Netflix 赤、Figma オレンジ等)になっていた。サービス名→色のマッピング表を作るか、自動生成にするか。
決定: Tailwind の bg-{red|orange|amber|lime|green|emerald|teal|cyan|sky|blue|violet|purple|fuchsia|pink|rose}-500 の固定パレットから、サービス名の単純ハッシュで決定。
理由: 個人 MVP でサービスごとのブランド色を手動メンテするのは現実的でない。ハッシュ方式なら同じサービス名は常に同じ色になり、サービスの追加にも自動対応。完全にブランドに合わせる必要が出てきたら個別マッピングを追加する。
2026-05-17: 棒グラフ(過去12ヶ月の推移)は外部ライブラリを使わず CSS divs で自作
背景: サマリーカードに過去 12 ヶ月の棒グラフを入れるにあたり、Recharts / Chart.js / visx などを検討。
決定: 棒は flex の中の div で表現し、高さは style={{ height: ... }} で動的に設定。今月のバーだけ bg-brand-500、他は灰、データなしは薄バー。
理由: 単純な縦棒チャートに数百KBのバンドルを入れたくない。ホバー時のツールチップは title 属性で十分。複雑なチャート(折れ線・複数系列・カーソル等)が必要になったら導入を再検討する。
2026-05-17: アイコンライブラリは lucide-react を採用
背景: ヘッダー・モーダルの × ・戻る矢印・ログアウトなどでアイコンが必要になった。Heroicons / Lucide / Tabler / 自前 SVG などを検討。
決定: lucide-react を採用。
理由: Tree-shake が効くインポート方式、サイズも軽量、デザインが Subnote のミニマルなトーンに合う。Heroicons でも良かったが、Lucide のほうがアイコンの線が均一でモダン。
2026-05-17: 為替レート API は Frankfurter(exchangerate-host から変更)
背景: 設計段階では exchangerate-host を第一候補にしていたが、同 API が登録/access_key 必須に方針転換した経緯があるため、要件「無料・登録不要」を満たす代替が必要だった。
決定: Frankfurter API(https://api.frankfurter.dev/v1/latest)を採用。base / symbols 指定で複数通貨の相互レートを取得し、exchange_rates テーブルに upsert。
理由: 無料・登録不要・CORS OK・ECB レートベースで信頼性も十分。サブスク管理用途では十分な精度。
2026-05-17: 設定の保存は「即時保存方式」(保存ボタン廃止)
背景: 設定画面の表示通貨切替で「保存ボタンを押しても何も反応がなく分かりにくい」という UX 課題があった。トースト / pending ボタン / 即時保存 / インライン通知の4案を検討。
決定: プルダウン変更で即時保存し、横に「保存中... → 保存しました ✓」(2秒で消える)を表示。
理由: 表示通貨は「選んだら即反映してほしい」性質の設定で、Apple / Google の設定アプリも同じ UX。フォーム + 保存ボタンより操作が1ステップ少なく、フィードバックも明確。
2026-05-17: 為替レート「今すぐ更新」ボタンは設定画面には置かない
背景: Route Handler /api/exchange-rates/refresh を作った際、設定画面に「今すぐ更新」ボタンも仮置きしていたが、最終ユーザー目線で必要か再検討した。
決定: ボタンは廃止。為替レートの更新は本番 cron(Vercel Cron 等)に任せ、開発時は curl / ブラウザで直接 endpoint を叩く運用にする。
理由: 個人サブスク管理で為替を秒単位で見る人はいないため 24h 更新で十分。設定画面に「触らなくていい技術的なボタン」を置くと UX を散らかすだけ。Route Handler 自体は残しているので開発・初期デプロイ時の手動更新は可能。
2026-05-17: 削除ボタンは formAction 経由で呼ぶ
背景: 削除ボタンを <button onClick={async () => await deleteAction()}> で実装したところ、削除後に 404 が表示されるバグが発生(Server Action の redirect("/") がうまく navigate されなかった)。
決定: 削除ボタンは <button type="submit" formAction={deleteAction} formNoValidate> 形式で呼ぶ。確認ダイアログは onClick で e.preventDefault() 制御。
理由: Server Action は <form action={...}> または <button formAction={...}> から呼ぶのが正規ルートで、Next.js のランタイムが redirect を含むレスポンスを正しく解釈する。手動 await action() だと navigate が安定しない。これは Subnote 内で Server Action を呼ぶ際の標準パターンとする。
2026-05-16: Web の技術スタックは Next.js + Supabase
背景: 認証・クラウド同期・iOS/Android との API 共通化を含めた基盤選定が必要だった。
決定: Next.js (App Router) + Supabase (Auth / Postgres / RLS) を採用。
理由: マジックリンク認証が標準対応、Postgres で SQL ベースの集計が素直に書ける、行レベル権限 (RLS) で認可を DB 側に寄せられる、無料枠で MVP 期間は十分、Pro $25/月までスケール可能。代替案の「自前 API on Lightsail」は基盤構築コスト高、「Firebase」は Firestore でタグ別集計が苦手・ロックイン強で見送り。
2026-05-16: 認証方式はマジックリンク / OTP
背景: パスワード方式、OAuth、マジックリンクの3案から選ぶ必要があった。
決定: マジックリンク / OTP(パスワードレス)を採用。
理由: Supabase Auth で標準対応、ユーザーにとってもパスワード管理が不要で UX 良好、実装コストも最小。OAuth (Google/Apple) は iOS アプリリリース時に Apple ログイン必須になる可能性があるが、その時点で追加検討すればよい。
2026-05-16: タグは text[] 配列で持つ(正規化しない)
背景: タグの DB 設計として、正規化(tags + subscription_tags の3テーブル)と非正規化(subscriptions.tags を text[])の2案を検討。
決定: subscriptions.tags を text[] 型で持つ。GIN インデックスで検索・集計に対応。
理由: MVP として実装コストが軽い、Postgres の配列型と GIN インデックスでタグ別集計や WHERE tags && ARRAY[...] 検索も問題なくこなせる。将来正規化が必要になった場合の移行も難しくない。タグの色分けや属性付けが必要になった時点で正規化を再検討する。
2026-05-16: 通貨は多通貨対応、為替レートは外部 API で自動換算
背景: USD/EUR 建てのサブスク(海外サービス)が混ざる可能性があるため、JPY のみか多通貨かの判断が必要だった。
決定: 多通貨対応とする。各サブスクは入力通貨で保存し、集計時に user_settings.display_currency に換算。為替レートは外部 API(exchangerate-host が第一候補)から 1日1回 exchange_rates テーブルにキャッシュ。
理由: 「JPY のみ」だと海外サービスの厳密管理ができない、「ユーザー手動入力」だとレート変動への追従が手間。自動化することで UX 良好。代償として為替 API の選定・キャッシュ・スケジューラ実装が必要だが、許容範囲。
2026-05-16: 月額合計は年額÷12 で換算して合算
背景: 月額サブスクと年額サブスクをどう統合表示するかの選択が必要だった。
決定: 年額サブスクは ÷ 12 で月額換算してから合計に含める。
理由: 「ひと月いくら使っているか」が一目でわかるのが個人向けサブスク管理ツールの常道。年額の内訳は詳細表示や年額換算で別途見せる余地はある。
2026-05-16: 登録/編集 UI は intercepted route でモーダル中心
背景: サブスク登録/編集を専用ページにするかモーダルにするかの判断。
決定: Next.js intercepted route を使い「一覧からはモーダル、URL 直アクセスはフルページ」のハイブリッド。
理由: SPA 体験(一覧画面を維持したまま編集)とブックマーク可能な URL(深リンク)を両立できる。実装はやや凝るが Next.js App Router の標準機能なので過剰ではない。
2026-05-16: iOS / Android / リマインダー実装 / マネタイズは MVP 後回し
背景: スコープを膨らませず Web MVP を最短で形にするための取捨選択。
決定: 以下を MVP スコープ外とする。
- iOS / Android の技術選定(Web の本実装が落ち着いてから検討)
- 更新日リマインダーの配信方法(データは持つが、通知実装は後回し)
- マネタイズ方針
理由: Web で全機能を実装してドッグフーディングするのが先決。リマインダー通知はメール/Push/アプリ内のどれも基盤構築コストがあり、MVP のクリティカルパスから外しても価値検証は十分できる。
DEVLOG(作業ログ)
開発日誌
このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。
2026-07-23
14:49 - Codex 実装レビュー→コミット + RevenueCat Play 連携一式(RTDN 疎通まで)+ iOS「配信地域未設定」事故の検出・修正
やったこと:
- Codex の Android パリティ実装をレビュー → 出荷品質と判断してコミット(
d90c3cf、24ファイル +2069行):- 適合確認: appUserID 小文字正規化(configure/logIn 両方)/ entitlement
premium/ 商品ID ハードコード無し / アカウント削除(502=購読残存の案内)/ SiwA ブラウザ OAuth / UTC 日付ズレは epoch day 方式の正攻法で修正(Material DatePicker の millis は UTC 基準の日付インデックス)/ 金額上限 /subscription_added・paywall_shownは iOS と同名同パラメータ / anti-steering 遵守(Web への導線は規約・プライバシー・問い合わせ・削除 API のみ)/ google-services.json 無しでもビルド可 / 独立ビルド検証 assembleDebug 成功 - 合わせて versionName 0.1.0→1.0.0、前セッション残骸の
seed_kimi.sqlは削除(You 指示) - 軽微メモ:
launchModesingleTask→singleTop(課金フロー両立のため。OAuth 復帰 E2E は未検証のまま)
- 適合確認: appUserID 小文字正規化(configure/logIn 両方)/ entitlement
- RevenueCat Play 連携一式:
- RC に「Subnote (Play Store)」アプリ作成(
appa1d98c25d3/me.subnote.android)→ 公開キーgoog_qyoLEgWvXvnPUyQhkXVvMquffIyを Config.kt に反映(6a4e880) - GCP(subnote-ab213): SA
revenuecat-play@subnote-ab213.iam.gserviceaccount.com作成 + Google Play Android Developer API / Play Developer Reporting API 有効化 - Play Console: SA を招待(アプリ情報閲覧・売上データ閲覧・注文と定期購入の管理の3権限)→ 有効
- You: JSON キー発行 → RC へアップロード。Pub/Sub 権限エラー → IAM で SA に「Pub/Sub 管理者」付与(Claude の IAM 操作は分類器ブロックのため You 実施。Pub/Sub Lite や サービスエージェントと間違えない)
- RTDN 疎通完了: RC でトピック
projects/subnote-ab213/topics/Play-Store-Notifications作成 → Connected to Google → Play Console 収益化のセットアップにトピック名を登録(通知の内容は買い切り含む全種類、lifetime があるため)→ テスト通知が RC に到達(Last received 2026-07-23 5:25 UTC) - RC の「Credentials need attention」は権限伝播待ち(最大36h、実検証は初回テスト購入時)で放置可
- RC に「Subnote (Play Store)」アプリ作成(
- iOS「App Store から削除済み」事故の検出→修正(You が ASC 画面で気づき):
- 症状: リリース済みなのにアプリ一覧で「App Store から削除済み」/ バナー「価格および配信状況に移動して再度追加してください」
- 原因特定: API で
appStoreVersions=READY_FOR_SALE(正常)だがappAvailabilityV2が 404 =配信地域レコードが存在しない。7/20 の API 提出フローで価格・配信権は設定したが availability を作っていなかった。今朝からの「ストア反映待ち」はラグではなくこれが原因 - 修正: ASC UI「配信状況の設定」→ すべての国または地域(175)→ 確認。「配信可能を処理中」に遷移(反映は最大24時間以内)。API で
availableInNewTerritories: trueを裏取り
教訓(重要):
- ASC の API 提出フローでは appAvailability(配信地域)を明示的に作らないと、審査もリリース操作も止まらないまま「公開時に配信先ゼロ」になる。価格(appPriceSchedule)・配信権・著作権を API で埋めても availability は別物。提出前チェックリストに「
GET /v1/apps/{id}/appAvailabilityV2が 200 を返すか」を追加すべき - リリース後にストアに出ない時は「反映ラグ」と即断せず、まず appAvailabilityV2 を GET する
次回やること:
- 数時間後に iOS のストア公開を確認(
apps.apple.com/app/id6790385156) - Android: upload 署名キー作成 → release AAB ビルド → Play 内部テストへアップロード(→ Play Billing 商品作成が解放)
- Play Billing 商品3種(想定: 定期購入
premium+ ベースプラン monthly/annual、買い切りpremium_lifetime。価格は iOS と同じ 月¥150/年¥1,200/買い切り¥2,500)→ RC の offering に紐付け → 内部テストで購入 E2E - ストア掲載整備(説明文は iOS 逆パターンで「iOS への言及を入れない」/ スクショ流用 / Data safety / コンテンツレーティング)
- You: Play お受け取り方法(銀行口座)追加
13:14 - Android パリティ始動: Codex 依頼書 → Codex 実装完了(レビュー待ち)+ Play Console リリース土台一式
やったこと:
- Android パリティ依頼書を作成 docs/codex-task-android-parity.md: iOS↔Android の機能差分を全量調査し、優先度3層で整理
- A=ストア提出必須: RevenueCat 課金 + ペイウォール / 無料5件上限到達時の導線 / アカウント削除 / 法務リンク / SiwA(ブラウザ OAuth)
- B=計測・バグ: Firebase Analytics/Crashlytics(イベント名は iOS と完全一致指定)/ 次回課金日が UTC 固定で JST だと前日にズレるバグ疑い / 金額上限バリデーション欠落 / ログアウト投げっぱなし
- C=UX パリティ: カタログ新導線追随(検索上部固定・手入力降格)/ 前月比チップ + 年額換算 / スワイプ解約・削除 / プルリフレッシュ / チャート月送り / テーマ切替 / ハプティクス拡充 / 縦固定
- Codex が実装完了(並行作業。未コミット・レビュー待ちでワーキングツリーに残存): 全項目実装 + エミュレータ検証済み(assembleDebug/Release 成功・google-services.json 無しでもビルド可・UTC 日付ズレ修正を JST で確認・テーマ永続化・RC キー未設定でもペイウォールが壊れない等)。詳細は docs/codex-task-android-parity-REPORT.md。未検証=Play 購入 E2E / OAuth 完走 / アカウント削除 E2E / 新規無料アカウントでの6件目エラー(REPORT に列挙)
- Firebase に Android アプリ登録(You がコンソール操作)→
google-services.jsonをandroid/app/に配置(クライアント公開キーのみ、iOS の plist と同じ扱い) - Play Console リリース土台一式(Claude が実 Chrome を操作、規約同意等は都度 You 承認):
- アプリ作成: Ramanu 組織アカウント(info@ramanu.co.jp)に「Subnote - サブスク管理をスマートに」(
me.subnote.android/ ja-JP / アプリ / 無料) - お支払いプロファイル紐付け: 既存の RAMANU LLC プロファイル(Play/AdSense 共用)を選択。業種=コンピュータ ソフトウェア / サポートメール=info@ramanu.co.jp / サイト=ramanu.co.jp / カード明細表記=RAMANU(DECISIONS)
- アカウントグループ「RAMANU LLC」作成 + 関連デベロッパーアカウント申告=該当なし(DECISIONS)
- 15% サービス手数料に登録完了(グループ合算の年間売上 $1M まで 30%→15%。Apple SBP の Google 版が先に片付いた)
- アプリ作成: Ramanu 組織アカウント(info@ramanu.co.jp)に「Subnote - サブスク管理をスマートに」(
気づき:
- ASC の実アプリ名は「Subnote - サブスク管理をスマートに」。DECISIONS 2026-07-13 の「Subnote - サブスク管理」はその後変更されており記録が古かった → Play も実名称に合わせて作成(DECISIONS)
- ichirokisanuki 個人の Play アカウントは 2025-01-31 に Google Play から削除済み(本人確認が期限までに未完了のため。コンソールにアカウント自体は残存、お支払いプロファイル無し)
- Play Billing の商品作成は「課金ライブラリ入り AAB を一度アップロードするまで解放されない」→ 商品3種の作成は内部テストアップロード後
- Play の関連アカウント申告フォームは、お支払いプロファイルを持たないデベロッパーアカウントをグループに追加できない(合算対象になり得ないアカウントは申告対象外という整理)
次回やること:
- Codex 実装のレビュー → コミット(REPORT の「要検討」も確認: versionName 0.1.0→1.0.0 判断など)
- RevenueCat に Play Store アプリ + サービスアカウント設定 →
goog_公開キー取得 →Config.ktのRevenueCatConfig.API_KEY差し替え - AAB を内部テストへアップロード → Play Billing 商品3種(月額/年額/買い切り)作成 → RevenueCat 紐付け
- You: Play お受け取り方法(銀行口座)追加(収益 ¥100 到達まででOK)
- iOS 継続タスク(ストア反映確認 / スクショ差し替え / 告知 / Apple SBP / GA4)
10:57 - iOS 1.0.0 審査通過 → 公開ボタン押下(正式リリース)+ Crashlytics 疎通確認
やったこと:
- 審査通過: 2回のリジェクト(1回目=Guideline 3.1.2 規約リンク欠落 / 2回目=Guideline 2.3.10 説明文の Android 言及、いずれもメタデータ修正で対応済み)を潰した再提出分が承認された。バイナリはビルド3 のまま
- 公開ボタン押下(正式リリース): App Store Connect で手動リリースを実行。1.0.0 初回リリースは段階的リリース(phased release)の対象外=全ユーザー即時公開。ステータスは
処理中→販売準備完了と遷移し、ストア反映(検索・DL 可)まで数十分〜数時間のラグ想定 - Crashlytics 疎通確認(リリース前の確認作業):
- コードの配線を裏取り:
FirebaseCrashlytics(SPM) リンク済み /SubnoteApp.swift起動時にAppAnalytics.configure()→ plist があればFirebaseApp.configure()(AppAnalytics.swift)→ Crashlytics は Firebase コンポーネントとして自己登録=明示 import 不要 - bundle ID 3箇所一致を検証: Xcode
PRODUCT_BUNDLE_IDENTIFIER/GoogleService-Info.plistのBUNDLE_ID/ Firebase コンソール表示 すべてme.subnote.app。GOOGLE_APP_IDも...:ios:...で iOS として登録済み - Firebase コンソールの「アプリが検出されクラッシュを待機しています」はエラーではなく健全な初期状態(SDK 疎通済み・実クラッシュ待ち)。テストクラッシュは必須ではないと判断
- dSYM アップロードは Release ビルドのみの Run Script=本番配信ビルドのクラッシュはシンボル化される
- コードの配線を裏取り:
気づき:
- Crashlytics の疎通は「アプリが検出され…」表示で確認できる。end-to-end(クラッシュ→コンソール表示)まで見たい場合は、デバッガを外して強制クラッシュ→再起動でレポート送信(送信はクラッシュ時でなく次回起動時)。Debug ビルドだと記録されるが未シンボル化
次回やること:
- ストア反映を確認(アプリページ
apps.apple.com/app/id6790385156/ 検索)→ 実機で本番配信ビルドを DL→起動して動作確認 - 公開後タスク: カタログ画面スクショ差し替え / 告知(ASO)/ Small Business Program 申請 / GA4 定点観測立ち上げ / 数日運用して問題なければアプリ内レビュー導線
2026-07-22
09:49 - iOS 1.0.0 審査 2回目リジェクト対応(Guideline 2.3.10 = 説明文の Android 言及)→ 削除して再提出
やったこと:
- 再提出(7/22 05:05 JST 分)が再びリジェクト:
appStoreState=REJECTED/ reviewSubmissionUNRESOLVED_ISSUES。 ただし前回の EULA リンク(3.1.2)は解消され、今回は別の指摘 - 理由の特定: Apple 2通目メール(同一スレッドに追記された新メッセージ)に Guideline 2.3.10 - Performance - Accurate Metadata「メタデータに他プラットフォームの情報が含まれる → 説明文から Android 言及を削除せよ」。 前回の自動チェックと違い人間レビュアーの指摘
- 該当箇所: 説明文「■ Web版とクラウド同期」末尾の
Android版も準備中です。(Android 言及は説明文のこの1箇所のみ。 キーワード・プロモ文・アプリ名には無し) - 修正(メタデータのみ・再ビルド不要): 前回と同じ P8 ES256 署名 PATCH スクリプトで、現行説明文を GET → 該当1文を削除 → PATCH(200)。Android 出現数 1→0 を検証。前回追記した規約ブロックは維持。 Web版の言及は残した(レビュアー指摘は Android のみ、クロスプラットフォーム同期は訴求上重要)
- 再提出(You の ASC UI 操作): 同一 reviewSubmission 再利用で5項目束ね直し →
WAITING_FOR_REVIEW(再提出 2026-07-22 00:49 UTC / JST 09:49)
気づき:
- リジェクトは1件ずつ潰す構造: 1回目=3.1.2(自動チェック)→ 通ると人間レビューに回り、2回目=2.3.10(人間レビュアー)で 別観点の指摘。1つ直すと次が出る前提で構える
- App説明文で Android / 他モバイルプラットフォームへの言及は 2.3.10 でNG(Web版の言及は概ね許容)。 次アプリでは説明文作成時に他プラットフォーム言及を最初から入れない
次回やること:
- 審査結果待ち(再提出済み)。人間レビュー分は前回(自動チェック)より時間がかかる可能性。承認 → 自動公開 / 再リジェクト → 対応
05:05 - iOS 1.0.0 審査リジェクト対応(Guideline 3.1.2 = 規約リンク欠落)→ メタデータ修正して再提出
やったこと:
- 審査結果を確認 → リジェクト判明:
appStoreState=REJECTED/ reviewSubmissionUNRESOLVED_ISSUES。 提出5項目のうちアプリ本体が REJECTED、サブスク商品類は巻き添えでREADY_FOR_REVIEWのまま保留 - リジェクト理由の特定: 公開 API には具体理由が出ない(状態までしか取れない)。 Apple の2通目メール「There's an issue with your … submission」本文(info@ramanu.co.jp 宛)に実文言があった。 指摘は自動チェックの1点=Guideline 3.1.2「自動更新サブスクを提供しているが、利用規約(EULA)への機能リンクがメタデータに無い」
- 原因確認(API): App説明文(ja)に規約リンク無し・カスタム EULA 未設定(
endUserLicenseAgreement= null) - 修正(メタデータのみ・再ビルド不要): App説明文の末尾に「■ 規約 / 利用規約: https://subnote.me/terms /
プライバシーポリシー: https://subnote.me/privacy」を追記。asc-subnote MCP は GET 専用のため、
~/.config/asc/AuthKey_8M2SWG78S6.p8で ES256 署名した JWT を作りPATCH /v1/appStoreVersionLocalizations/{id}を 直接実行(200)。冪等スクリプトで現行説明文を取得→追記する形にして取りこぼし防止 - 再提出(You の ASC UI 操作): 同一 reviewSubmission を再利用して5項目束ね直しで再提出 →
WAITING_FOR_REVIEW(再提出 2026-07-21 20:05 UTC / JST 07-22 05:05)。公開方法は AFTER_APPROVAL のまま
気づき:
- ASC の審査リジェクト具体理由は公開 API に出ない。実文言は Apple 2通目メール or Resolution Center を見る (1通目「Your App Review Feedback」は定型文で理由なし)
- asc-subnote MCP は GET 専用 → メタデータ編集(PATCH)は P8 で自前署名して api.appstoreconnect.apple.com を直接叩く
(
crypto.sign('sha256', input, {key, dsaEncoding:'ieee-p1363'})) - 規約ページ https://subnote.me/terms・/privacy はいずれも 200 で稼働(二次リジェクト防止に事前確認)
次回やること:
- 審査結果待ち(再提出済み)。承認 → 自動公開 / 再リジェクト → 内容確認して対応。ステータスは asc-subnote MCP で監視可能
- 公開後: カタログ画面スクショの差し替え / 告知(promo-expert)/ Small Business Program 申請 / GA4 定点観測
2026-07-20
03:57 - iOS 1.0.0 審査提出(GA4/Firebase・メール出し分け・UI改善・Web正式公開まで一気通貫)
やったこと:
- iOS 1.0.0 を審査提出(7/20 未明): 提出物モデル(Review Submission)で5項目
(アプリ 1.0.0 (3) + サブスクグループ + 月額/年額/買い切り)を提出 →
WAITING_FOR_REVIEW。 公開方法は AFTER_APPROVAL。提出直前に未設定だった 著作権(2026 Ramanu LLC)/ コンテンツ配信権(USES_THIRD_PARTY_CONTENT)/ 価格(無料・JPN基準) を API で補完 - 行動分析導入(GA4 + Firebase): Web は
@next/third-partiesの gtag (本番のみ、測定ID埋め込み)、iOS は FirebaseAnalytics(SPM) +AppAnalyticsラッパー- イベント2種(subscription_added / paywall_shown)。Firebase プロジェクト
subnote-ab213(You 作成)、plist 同梱で有効化。プライバシーポリシー改定
- イベント2種(subscription_added / paywall_shown)。Firebase プロジェクト
- Crashlytics 導入(ビルド3): SDK + dSYM 自動アップロード Run Script(Release のみ)
- サンドボックス無効化。ASC App プライバシーに「クラッシュデータ」追加(計8データタイプ、 製品の操作・デバイスID もこのセッションでブラウザ操作追加)
- 認証メールの出し分けが本番稼働: Send Email Hook 受け口
POST /api/auth/send-email(standardwebhooks 署名検証・256KiB 上限・Resend deadline 3.8s・rawBody ベース Idempotency-Key)+planAuthEmails()に種別/宛先/文面を集約 (email_change 2通対応・通知7種・未知種別 fail-closed)。Codex レビュー3ラウンド (High1/Mid3/Low3 全対応)で「出荷可」。本番 E2E: Web=リンクのみ / アプリ=コードのみ - ASC スクショに Web 同期訴求を追加: iPhone 7枚目「Webでも、iPhoneでも。」 (2デバイス構図、Codex 分業)を4枚目に配置、iPad 版04 を3枚目に配置。 Web 側 raw はローカル Supabase にデモデータ再現 + headless Chrome/CDP + Cookie 注入で 撮影(本番はベータガードで撮影不可のため)。プロモテキストにも同期訴求を追記(API PATCH)
- iOS UI 改善: アイコンのマーク20px下げ(余白均等化)/ カタログ登録画面の導線再設計 (検索を上部固定のアクセント枠付き自前フィールドに・手入力は見出し右リンク + 空状態 3段構えに降格、検索語プリフィル)/ ホームのブランドヘッダーをスクロール追従化 (上端ソフトフェード、タイトル〜カード間隔 46→23pt、上余白 55→27pt)/ iPhone 縦固定
- Pull to Refresh のカスタム演出(カード円環)を実装 → ユーザー判断で標準に戻した (見た目は好評だったが最終的に不採用。revert 済み)
- Web 正式公開: middleware のクローズドベータガードを撤去して push = 全開放
- QA データ掃除: RC の大文字 customer(正規化バグ残骸)と SiwA テストユーザー
0dc0a587…(RC + auth.users)を削除。goda のテストサブスクと lifetime entitlement は QA 用に残す判断
詰まったこと / 気づき:
- ASC の新提出フローは「提出物(Review Submission)」に項目を積む方式。サブスクは グループだけでなく**配下の商品を個別に「審査用に追加」**する必要がある
- ASC の縦並び確認ダイアログ(次へ/キャンセル/戻る)はボタン位置が画面ごとに変わる。 ブラインド連打でキャンセルを踏み提出フローが1回巻き戻った → 毎回 JS で座標特定が確実
.searchableは iOS 26 で下部配置になり、この画面構成では気づかれない (実機での You の違和感が決め手)。visionOS ではなく iPhone 実機での体感確認が正義- SwiftUI の
strokeはパス中央線のため上下の太さが非対称に見えることがある →strokeBorderで解消 withAnimationでリスト差し替えをアニメーションすると.refreshableのスピナー収納と 干渉してスクロール位置が狂う(+34pt 残留)→ リフレッシュのデータ反映は無アニメが正解- ローカル Supabase の Send Email Hook 検証は config.toml に一時ブロック追記 → supabase 再起動 → dev サーバーのログでメール内容確認、のループが有効だった
- GoTrue の PKCE マジックリンクは「リンクを開くブラウザに code_verifier がある」ことが 前提。別ブラウザ(headless)で開くと exchange に失敗する → password grant で セッションを API 発行し Cookie を自作して CDP 注入する方式が確実
- Xcode の「Upload Symbols Failed」(Firebase 系 dSYM)は既知の無害警告
次回やること:
- 審査結果待ち(最大48時間表示)。承認 → 自動公開 / リジェクト → 対応
- 公開後: カタログ画面スクショの差し替え(新導線版。審査中はメタデータロック)
- 告知(ASO・告知文面)/ Small Business Program 申請
2026-07-17
03:36 - ASC スクショ v2 完成(1枚ずつ × Codex 分業)→ 全9枚アップロード完了
やったこと:
- スクショ v2 を完成・アップロード: iPhone 6.9 ×6(①ホーム合計ヒーロー ②カタログ ③一覧$/¥混在 ④月次チャート ⑤プラン選択 ⑥ダーク)+ iPad 13 ×3(ホーム/カタログ/ダーク)。ASC API で全9枚 COMPLETE(iPad 13 枠はセット新規作成
ccb1b77c…)。v1 の素のスクショ4枚は API で削除済み - 制作体制: raw 撮影・検品・ASC アップロード = Claude Code / 仕上げ加工 = Codex(docs/asc-screenshots/generator/ の template.html + shots.json + generate.mjs、headless Chrome + ImageMagick)。依頼書 docs/codex-task-asc-screenshots.md に「今回のタスク」を書いて1枚ずつ往復
- スタイル探索の経緯: インディゴ背景(初案)→ You 指定でモノクロ → 白背景+黒文字(ラウンド2)→ ラウンド1のグレーグラデ × ラウンド2の構図のハイブリッドで確定(+余白調整: コピー94px・端末幅1140px)
- 構成の再設計: 当初案「01ホーム/02一覧/…/04もうすぐ更新」はホーム由来3枚で画変わりが弱いと You 指摘 → 「もうすぐ更新」独立枠を廃止し(訴求は01のバッジで代替)、カタログ/チャート/プラン選択を挟む現構成に
- デモデータ整備: ChatGPT を $20 USD に変更(多通貨訴求。OTP ログイン + REST PATCH)/ チャート用に
monthly_snapshots11ヶ月分をバックフィル(SQL。email 照合版は 0行 INSERT で空振り → UUID 直指定版で解決)。副産物でヒーローカードに前月比 ↑3.4% バッジが出るように - アプリ改修: プラン選択画面にサービスアイコン(76pt)をセンター表示 +
ServiceIconにサイズ指定追加(プラン画面が寂しい問題の恒久対策、26f1406) - 仕様確認: iPhone スクショの必須サイズは 6.9インチ(6.5 は 6.9 が無い場合のみの条件付き必須)。ASC の空状態 UI が 6.5 枠を出すのは罠
- コミット
26f1406(スクショ一式 + アプリ改修 + v1 削除 + README v2 化)
決めたこと:
- スクショ v2 の構成・スタイル確定(DECISIONS 2026-07-17)
- プラン選択画面にサービスアイコンを表示(DECISIONS 2026-07-17)
詰まったこと / 気づき:
- status_bar override は cellular 指定必須:
--cellularMode active --cellularBars 4を省くと電波が「••••」の圏外風になる(v2 で1枚撮り直しが発生) - AXe の超低速・短距離スワイプは認識されない(30pt/1.5s・45pt/2.5s は不動、60〜70pt/0.5〜1.5s は慣性で大きく動く)→ スクロールの中間位置を精密に狙うのは困難。構図はスナップする2状態(最上部/最下部近辺)を前提に組む方が確実
- ImageMagick 出力の PNG はメタデータ(タイムスタンプ)で md5 が毎回変わる → 「再生成で内容不変」の検収はファイルサイズ + shots.json/template の差分で判断
- 月次チャート(推移セクション)は
chartPoints.count > 1で初めて表示される(スナップショット0件だとセクションごと消える) - Codex とのファイル分業は「スタイル確定後は shots.json 追記だけ」「テンプレは1pxも変えない」を検収条件に明文化すると安定した
- 縮小プレビューでは存在しないアーティファクトが見えることがある(2回発生、いずれも原寸クロップで潔白確認)
次回やること:
- 審査提出(You の ASC UI 操作): バージョン 1.0 ページの「アプリ内課金とサブスクリプション」に READY_TO_SUBMIT の3商品を追加 → 「審査用に追加」で提出(初回 IAP はアプリ本体と同時審査必須)。公開方法は AFTER_APPROVAL のまま=承認と同時公開、手動にするなら提出前に変更
- QA データ掃除(継続。デモアカ
+ascは撮り直し用に保持、ChatGPT=$20・スナップショット入りの状態も保持)
2026-07-16
22:43 - ASC 提出用スクショ製作・アップロード + IAP メタデータ完成(審査提出は保留)
やったこと:
- スクショ v1 を製作: デモアカウント
ichirokisanuki+asc@gmail.com(プラスアドレス、本番 Supabase)を新規作成し、カタログから5件登録(Netflix / Spotify / YouTube Premium / ChatGPT Plus / Amazon Prime 年額。もうすぐ更新・タグ集計・年額表示が映る構成)。iPhone 17 Pro Max(6.9インチ、1320×2868)で6枚 + iPad Pro 13(2064×2752)で3枚 + IAP 用ペイウォール1枚を撮影 → docs/asc-screenshots/ に README 付きで整理。ステータスバーは 9:41・電池100% に整形 - ASC へ API アップロード: Node 自作スクリプト(ES256 JWT → 予約 POST → バイナリ PUT → md5 コミット → COMPLETE ポーリング)で、iPhone 6.9 枠に4枚(01ホーム/02一覧/03カタログ/06ダーク)、iPad 13 枠に3枚を投入。全件 COMPLETE 確認済み(04 プラン選択・05 設定はローカル保留)
- IAP 3商品を READY_TO_SUBMIT に: 3商品(月額
6790955757/ 年額6790958820/ 買い切り6790959717)にレビュー用スクショを添付。添付後もサブスク2本が MISSING_METADATA のままだった残件を特定 → グループ表示名 ja「Subnoteプレミアム」新規作成 + 販売地域(全175 territory)設定で解消 - ビルド 1.0 (1) に輸出コンプライアンス申告:
usesNonExemptEncryption=falseを PATCH(Info.plist には宣言済みだったが ASC 側は null だった。次ビルドからは自動) - App Review 情報を API で作成: 連絡先(氏名・メール・電話)+ デモアカウント不要 + 「SiwA / メール OTP でアプリ内から即時登録できる」旨の英文メモ
- 確認済み事項: 年齢制限指定は設定済み(全 NONE = 4+)/ App プライバシーは公開済み(5データタイプ、3日前に You が設定済みだった)/ 有料 App 契約はセッション中に合意 → 有効化(WIP の「未着手 = ブロッカー」記載は陳腐化していた)
- 審査提出は保留: スクショをもっと仕上げたいため(別セッション)。初回 IAP はアプリ本体と同時審査必須のため、提出時にバージョン 1.0 へ3商品を追加してから提出する
詰まったこと / 気づき:
- シミュレータの日本語ハードウェア IME は
AppleKeyboardsから ja を消しても生き残る →simctl pbcopy+ 長押し「ペースト」で入力を迂回するのが確実(メールアドレス・OTP 入力ともこれで自動化できた) - ワードマークの文字ジャンプアニメで画面ハッシュが変わり続け、XcodeBuildMCP の snapshot が期限切れ頻発 → bundled の AXe CLI(
~/.npm/_npx/…/xcodebuildmcp/bundled/axe)による座標 tap / swipe に切り替えると安定。スクショはアニメ静止フレームを確認してから撮る(iPad ダーク版で「n」がジャンプ中に写り撮り直した) - ASC API の要点:
appScreenshotSetsへのfilter[screenshotDisplayType]は効かない(全件取得して attributes で判定する)/ 6.9インチ枠の displayType はAPP_IPHONE_67(1320×2868 を受理)/ iPad 13 はAPP_IPAD_PRO_3GEN_129/ スクショ差し替えはDELETE /v1/appScreenshots/{id}→ 再アップロード - Sandbox は有料 App 契約が有効なら実 ASC 商品を返す。未サインインのシミュレータはストアフロントが US になるため USD 表示(ペイウォールに $0.99/$6.99/$14.99 が出た。¥ で撮るなら Xcode ▶ Run のローカル StoreKit 設定)
- 現行ビルドは
TARGETED_DEVICE_FAMILY=1,2,7(iPad 対応)のため iPad 13 スクショが必須 → iPad レイアウトは良好だったため3枚提出して iPad 対応のまま進行。iPhone 専用化するなら device family=1 で 1.0 (2) 再アップロードが必要
次回やること:
- スクショ仕上げ(別セッション): 04 プラン選択 / 05 設定(デモメール写り込み)の扱い・マーケ加工(デバイスフレーム + コピー)の検討 → 差し替えアップロード
- 仕上げ後に ASC UI で審査提出: バージョン 1.0 に IAP 3商品を追加 → 提出(公開方法は現在 AFTER_APPROVAL = 承認と同時公開。手動公開にするなら提出前に変更)
- QA データ掃除(継続。デモアカ
+ascは撮り直し用に保持)
19:20 - ヘッダーワードマークに文字ジャンプアニメーション(Codex 分業実装)
やったこと:
- ホームヘッダーの「Subnote」ワードマークに「時々文字が動く」遊び心を追加。指示書
codex-task-wordmark.md(案出し6種 + 技術制約)を作成して Codex に実装委任 → Claude Code がレビュー・シミュレータ検証・コミット(f837fb4。指示書は役目を終えて削除) - 採用案: ランダム1文字ジャンプ(跳ねて着地時に squash & stretch、0.78秒)+ タップで即発動。発動頻度は表示4秒後に1回 + 以降20〜40秒のランダム間隔
- 実装は iOS 18 の
TextRenderer/TextAttribute(ios/Subnote/Subnote/AnimatedWordmark.swiftに分離、HomeView はText("Subnote")の差し替えのみ)。Reduce Motion 尊重 / シート表示・バックグラウンド中は.task(id:)で停止 /displayPaddingでクリッピング回避 / レイアウトシフトなし - レビュー修正1件:
Textの+連結が iOS 26 で非推奨警告 →Text補間("\(a)\(b)")に変更。customAttributeが補間を通っても保持されることを実機挙動で確認 - 検証:
simctl io recordVideoで録画 → AVFoundation(AVAssetImageGenerator)の自作 Swift スクリプトでワードマーク領域をクロップし基準フレームとの平均差分で発動時刻を検出 → フレーム目視。修正後ビルドで4回発動(起動直後 + タップ2 + 自動1)、毎回別の文字(n/o/S)が跳ねることを確認
詰まったこと / 気づき:
- XcodeBuildMCP の
record_sim_videoは停止時に保存パス取得に失敗することがある →xcrun simctl io recordVideoをバックグラウンド起動して SIGINT で止めるのが確実 - Homebrew の ffmpeg が libass 欠落で起動不能だった → AVAssetImageGenerator の Swift スクリプトでフレーム抽出・差分解析を代替。「録画 → 領域クロップ → 基準フレーム差分 → 閾値超え時刻を目視」はアニメーション検証の定量化レシピとして再利用できる
- 検証中にシミュレータへ別アプリの通知バナーが出て座標タップを奪い、別アプリが前面化した。座標タップ前は前面アプリの確認が要る
次回やること:
- (変わらず)審査提出準備: PremiumView スクショ → ASC の各 IAP 商品にレビュー用スクショ添付 → IAP 込みで v1 審査提出 / QA データ掃除
15:35 - Sign in with Apple ネイティブ修復(新 App ID を旧 primary に group)+ Web/iOS 統合実証
やったこと:
- SiwA ネイティブ「登録を完了できませんでした」の真因を三者議論(ChatGPT)+ 実測で確定。議論ログ: .devnotes/discussions/2026-07-16-siwa-signup-not-completed.md
- 切り分け: エラーは Apple 純正シート側(Subnote コード無関係、credential 到達前の失敗)と特定 → 別端末 × 別 Apple ID でも同一失敗を再現 → アカウント要因・端末要因・レート制限を全て棄却
- 構成の実測: ASC API(
/v1/bundleIds+ bundleIdCapabilities)でme.subnote.app=独立 primary(PRIMARY_APP_CONSENT)/me.subnote.Subnote=独立 primary /me.subnote.web(Services ID) の Primary App ID=旧me.subnote.Subnoteを確認。実機ビルドの embedded profile / 署名バイナリの識別子・SiwA entitlement は全整合(署名要因を除外) - 修復(三者議論の系統Y): Portal で
me.subnote.appを「単独 primary」→「旧me.subnote.Subnoteに group」に変更(ASC API でPRIMARY_APP_CONSENT→RELATED_APP_CONSENTを確認)→ cloud signing(ASC API キー8M2SWG78S6)で実機再ビルド(新プロファイル生成)→ iPhone 14 に再インストール → ネイティブ SiwA 成功 - Web/iOS 統合を実証: 同一 Apple ID(Hide My Email のリレー
8dv8scfz4h@privaterelay.appleid.com)で iOS ネイティブ + Web ログイン → Supabase Auth の該当ユーザーは 1件のみ(0dc0a587…、nativeaud=me.subnote.appと webaud=me.subnote.webが同一sub)→ Web ダッシュボードに iOS 登録サブスクが表示。「Web/iOS で同じデータ」コア要件を目視で確認 - 副次: Supabase Apple provider の Authorized Client IDs は
me.subnote.appを含む設定済み(native ログイン成立で確認) - Web ベータガード検証で一時的に ADMIN_EMAILS を書き換え → 検証後
ichirokisanuki@gmail.comのみに復元(テスト用リレーメールは本番許可リストから除去)
詰まったこと / 気づき:
- 新規 primary App ID は Portal/ASC でフラグが立っても Apple 認証バックエンドで機能しないことがある(全アカウント失敗)。稼働実績のある primary に group するのが確実(原因不問で迂回でき、Apple 推奨構成でもある)
- SiwA capability の OFF→ON は非破壊ではない(保存済み構成リセット + プロファイル無効化)。ChatGPT の指摘で回避し、group 変更を選択
- App ID 設定変更でプロファイルが無効化 → 再ビルド・再署名・再インストールが必須
subは developer team スコープ(primary 単位ではない)。同一 Team + 同一 primary への集約で Web/iOS の identity が揃う- Hide My Email 時の実メールは
@privaterelay.appleid.com。ADMIN_EMAILS 検証で当初 Apple アカウントのメール(yahoo)を足して 404 が消えなかったのは、実メールがリレーだったため - 本番の SUPABASE_SERVICE_ROLE_KEY は Worker secret のみ(ローカルに無い)=本番 DB 直叩き不可。統合確認は Supabase ダッシュボードの Auth Users で実施
次回やること:
- 審査提出準備: ペイウォール(PremiumView)スクショ → ASC の各 IAP 商品(MISSING_METADATA)にレビュー用スクショ添付 → IAP 込みで v1 審査提出
- QA データ掃除: 本番 DB の今日のテストユーザー
0dc0a587…(+ 登録サブスク)/ 過去のテストユーザー群 / goda の本番テストサブスク / Xcode テスト購入由来の lifetime entitlement
03:05 - iOS 実機テスト(IAP 実購入フルパイプライン検証 + ASC 初ビルドアップロード)
やったこと:
- 実機テスト環境構築(7/15 夜): iPhone 14(iOS 26.5)に development ビルドをインストール。ASC API キーで cloud signing(
xcodebuild -allowProvisioningUpdates -authenticationKeyPath ...)→ デバイス登録・プロファイル生成まで CLI 完結。pbxproj にDEVELOPMENT_TEAM = 9HZW92277Nを追加 - スキームの StoreKit 参照パス切れを発見・修正: Codex 手書きの
../Subnote/Subnote.storekitは解決不能で、Xcode Run でもローカル設定が無効 = Sandbox 接続になっていた(「InApp 購入を行う権限がありません [Environment: Sandbox]」。シミュレータ含めこれまで一度も有効になっていなかった)。Edit Scheme の GUI で選び直して正規パス../../Subnote/Subnote.storekitに確定 - RC app_user_id の大文字バグを発見・修正(重大): iOS が
UUID.uuidString(大文字)を渡し、Web(小文字)と RC customer が分裂。webhook 同期は小文字で引き直すため 404 → iOS で課金しても Supabase に premium が反映されず6件目が弾かれ続ける状態だった。PurchaseManager(configure / handleSignedIn)で.lowercased()正規化(e438719、DECISIONS 2026-07-16) - IAP フルパイプラインを実機検証: [Environment: Xcode] 購入成功 → RC に entitlement(Xcode 環境の購入も RC サーバーが受理)→ webhook 200(wrangler tail で観測)→ Supabase 同期 → 無料5件ゲート解放(6件目登録成功)。実 ¥ 価格表示(¥150/¥1,200/¥2,500)も確認。加速時計による失効 → premium 剥奪同期も観測
- ASC に初ビルド 1.0 (1) をアップロード(当初目的「ASC にアイコン登録」= アイコンはビルド同梱でしか登録できないため): App Manager 権限の API キーでは distribution の cloud signing 不可(
Cloud signing permission error)→ Xcode サインイン済みアカウントのセッション経由(-exportArchivedestination=upload、API キー引数なし)で成功。処理完了(VALID)・アイコン登録済み - Info.plist に
ITSAppUsesNonExemptEncryption=false追加(次回ビルドから輸出コンプライアンス質問スキップ。今回のビルド 1 だけ TestFlight 利用時に ASC で一度回答が必要) - SiwA ネイティブ E2E は Apple 側エラーで持ち越し: 「登録を完了できませんでした」(Apple シートの Sign-Up not completed)。切り分け済み = App ID capability 正常(旧 App ID と設定同一の PRIMARY_APP_CONSENT)/ profile に entitlement あり / Apple 障害なし / メール共有・非公開とも失敗 / 同一 Apple ID の Web SiwA は成功 → 新 App ID
me.subnote.appの SiwA 有効化(当日の自動プロビジョニング付与とみられる)の反映待ちが最有力。時間を置いて再試行
詰まったこと / 気づき:
- StoreKit ローカル設定は Xcode の ▶ Run でしか注入されない(simctl / devicectl / ホーム画面起動は Sandbox 接続)。有効化の確認手段は購入シートの
[Environment: Xcode]表示 - スキームの
StoreKitConfigurationFileReferenceidentifier は xcschemes ディレクトリからの相対パス(../../<srcフォルダ>/<file>.storekit)。手書きせず Xcode GUI で選ぶのが安全 - RC は Xcode 環境の購入も受理して webhook まで飛ばす = 有料 App 契約・Sandbox なしで課金パイプラインのフル検証が可能(想定外の収穫)
- Xcode 環境の月額サブスクは加速時計で約30秒ごとに更新 → webhook 連打。DB の premium も30秒周期で有効/失効を往復するので、安定検証は Debug ▸ StoreKit ▸ Manage Transactions でトランザクション削除 → 買い切りを購入して行う
- Sandbox 側は「Sandbox テスター sandtarou3@gmail.com が端末設定済み・ASC 商品定義から実価格取得も動作」まで確認(購入完了は有料 App 契約待ち)
- 実機 development ビルドは ASC API キー(App Manager)で cloud signing 可能だが、distribution は Admin 相当の権限が必要。アップロードは Xcode アカウントセッションで代替できる
次回やること:
- SiwA ネイティブ再試行 → 成功したら premium 分離確認 → アカウント削除 → 端末設定の「Apple でサインイン」一覧から Subnote が消えるか(失効 API の挙動は wrangler tail で観測)
- 審査提出準備: ペイウォールスクショ → ASC の各 IAP 商品(MISSING_METADATA)にレビュー用スクショ添付
- QA データ掃除: goda の本番テストサブスク6件 + lifetime entitlement(Xcode テスト購入由来で本番 DB に残存)+ RC の大文字 customer
923CE947-...削除
2026-07-15
15:57 - RevenueCat ③設定を API で完了 + Codex の IAP 実装をコミット + StoreKit ローカル QA
やったこと:
- RevenueCat ③(残り設定)を v2 API で完了・検証(ブラウザ操作なし。
project_configuration権限つき一時 secret keysubnote-config-tempを You が発行 → API 実行 → 使用後 revoke):premium(entlbf6187e2a6)に App Store の monthly(prod965d7f2f07)/ annual(prodf04d30f11d)を紐付け(lifetime は既存)。attach_products のボディはproduct_ids(配列)- default offering(
ofrng5f5b483d83)の3パッケージ$rc_monthly/$rc_annual/$rc_lifetimeに対応する App Store 商品を追加(package の attach はproducts:[{product_id, eligibility_criteria:"all"}]) - iOS Public SDK キーを API で取得(
GET /apps/{id}/public_api_keys→appl_SSHoBZAPUkHdynsZSYGmsdcAkSu)→ Config.swift のRevenueCatConfig.apiKeyに反映(placeholder 削除)
- Codex の iOS IAP 実装一式をコミット(
10d342e): RevenueCat SDK(PurchaseManager)/ ペイウォール PremiumView / 各画面結線 /Subnote.storekit/ 共有スキーム / REPORT。appl_公開キー・storekit ダミー価格のみで secret なし .storekitのローカルテスト価格を本番価格に修正(5740da7): 暫定(月480/年4,800/買い切り9,800円)→ ¥150 / ¥1,200 / ¥2,500- StoreKit ローカル QA(XcodeBuildMCP、iPhone 17 / iOS 26.5): ビルド起動 → QA アカウント goda.takeshichan2 でメール OTP ログイン → ペイウォール到達。購入完了を伴わない項目を全検証:
- ペイウォール構成(3プラン/復元/自動更新開示文)・anti-steering(Web誘導なし)・購入結線(タップ→StoreKit起動)・復元(未購入時「復元できる購入がありません」)
- 無料5件上限→ペイウォール導線: カタログから5件登録 → 6件目保存で
SUBSCRIPTION_LIMIT_REACHEDを検出し日本語案内(生エラーなし)+「Subnoteプレミアムを見る」→ ペイウォール遷移まで確認(前回 Codex QA が2件しか登録せず未検出だった箇所)
詰まったこと / 気づき:
- CLI(simctl)起動ではスキームの StoreKit ローカル設定が非活性: 購入タップで実 StoreKit の「Apple Account サインイン」が出た(ローカル設定なら不要)。このためペイウォール価格が RevenueCat フォールバックの $0.99/$6.99/$14.99(USD) で表示され、修正した .storekit の ¥ 価格も反映されない。コードのバグではなくテスト環境由来。実 ¥ 価格の確認と購入完了は Xcode の Product▸Run(ローカル設定有効)か Sandbox が必要
- RevenueCat 公開 SDK キー(
appl_)は v2 API の/apps/{id}/public_api_keysで取得可能(ダッシュボード必須ではなかった) - QA アカウント goda.takeshichan2 の本番 DB にテスト用サブスク5件が残存(Netflix/ChatGPT/Claude/Amazon Prime/YouTube Premium)。掃除は任意
次回やること:
- 実購入検証(Xcode 実行 or Sandbox Apple ID): 購入完了 → entitlement 有効化 → UI 反映 → webhook → Supabase 同期 / アカウント切替の premium 分離
- ペイウォール(PremiumView)のスクショ → 各 IAP 商品にレビュー用スクショ添付 → アプリ審査と同時提出
- 一時キー
subnote-config-tempの revoke は完了済み
06:57 - Codex QA ループ1周(F-001〜F-010 全対応)+ Apple トークン失効 + RevenueCat キー移行 + IAP 実装指示書
やったこと:
- Codex QA ループを iOS ラウンドへ初期化: 6月の Web 版総合レビューを
.codex-review/archive/に退避し、AGENTS.mdの Codex 契約を iOS シミュレータ QA 向けに書き換え(App Store 提出前の重点観点6つ + 所感セクション許可 + 本番 Supabase なのでテストアカウント運用を明記) - Codex findings F-001〜F-009 対応(
3ea4dee):- F-001(高): アカウント削除導線。iOS 設定に削除 UI + 新 API
POST /api/account/delete(Bearer 認証のみ = CSRF 構造的防止。RevenueCat customer 削除 → auth ユーザー削除、データは FK cascade) - F-002/F-007: ログアウト時の OTP 状態リセット / 認証エラーの日本語化 + メール形式事前チェック
- F-003: 金額検証を web の
MAX_AMOUNT(9,999,999,999.99)と統一 - F-004/F-005/F-006: 特大 Dynamic Type で縦積みレイアウト切替、一覧末尾に 88pt の contentMargins(固定「+」ボタン重なり解消)、サービス名2行制限
- F-008/F-009: ログイン画面・設定に利用規約/プライバシーポリシーリンク / カタログ検索0件時の空状態表示
- F-001(高): アカウント削除導線。iOS 設定に削除 UI + 新 API
- アカウント削除時の Apple トークン失効を実装(
b9c1dd8、審査 5.1.1(v) 付帯要件): iOS が SiwA サインイン成功時に authorizationCode をPOST /api/apple/tokenへ送信 → サーバが refresh token に交換しapple_refresh_tokensに保管(id_token の sub と Apple identity の一致検証付き)→ 削除時に/auth/revoke。client secret はAPPLE_SIGNIN_PRIVATE_KEY(Cloudflare secret 登録済み)から都度 ES256 生成。migration20260715000001をsupabase db pushで本番適用 - F-010(高)対応(
d6258ae+f09303b): 本番で削除 API が全ユーザー 502 → 原因は RevenueCat キーが読み取り専用で customer DELETE が 403(DELETE=403 / GET=404 のプローブで切り分け)。fail-closed を「premium_until が未来のユーザー」だけに限定し、無課金・lifetime は続行に変更。本番 E2E(admin generate_link でセッション作成 → 削除 200 → admin 照会 404)で解消確認 - RevenueCat キー移行完了: write 権限(Customers Read & Write)つき新キーを発行(You)→ Worker secret 差し替え(You)→
wrangler tail監視下の削除 E2E で「RC 失敗ログなし」を確認 → ローカル env もクリップボード経由(値非表示・sk_ 形式検証つき)で差し替え → 旧キー「Subnote Cloudflare」Revoke 済み - iOS v1 の提出方針を決定: IAP を組み込んでから提出(②)。実装は Codex に委任することにし、指示書 docs/codex-task-ios-iap.md を作成(商品 ID 案 3 つ / app_user_id=Supabase UUID 厳守 / StoreKit Configuration によるローカル検証 / 復元ボタン・法務リンク等の審査要件 / 受け入れ条件 / REPORT 形式)。依頼はまだ出していない
決めたこと:
- SiwA を v1 に残し、Apple トークン失効を自前実装(メール OTP のみ案は却下)→ DECISIONS 2026-07-15
- アカウント削除の fail-closed は課金アクティブ(premium_until 未来)のみ → DECISIONS 2026-07-15
- iOS v1 は IAP 組み込み後に提出、実装は Codex 委任 → DECISIONS 2026-07-15
詰まったこと / 気づき:
- RevenueCat v2 の権限切り分け: 存在しない customer への DELETE が 403 なら権限不足、404 なら権限OK。読み取り専用キーとの区別がこれで即判定できる
- Supabase の Apple refresh token は外部に出ない: Web OAuth(Services ID
me.subnote.web)経由のトークンは Supabase 内部保持で失効不可。iOS ネイティブ分だけ authorizationCode を自前で交換・保管する設計が必要(今回の実装) - Apple client secret は「静的 JWT を半年ごとに貼り替え」ではなく「.p8 を secret に置いて都度署名」にすれば期限管理が不要(Supabase ダッシュボード用の Web 分は従来通り半年更新)
- 本番 E2E の小技:
supabase projects api-keysで鍵取得 → admingenerate_link→/auth/v1/verifyで任意ユーザーのセッションを作れる(削除 E2E に活用)。デプロイ反映待ちは削除 API を 20 秒間隔で poll - Codex QA の見落としカバー: QA は 2 件しか登録しなかったため無料5件上限の生エラー(
SUBSCRIPTION_LIMIT_REACHEDプレフィックス付き)は未検出。IAP 実装の指示書に上限導線の整備を含めた
次回やること:
- Codex に IAP 実装を依頼(依頼文は準備済み)→ REPORT レビュー → コミット
- 並行で You 側: 有料 App 契約 / ASC の IAP 商品3つ / RevenueCat に App Store アプリ追加 +
appl_キー取得 - Codex の QA 再検証(F-010 削除成功確認。502 で残った Codex の QA アカウントが使える)
2026-07-13
19:47 - App Store Connect リリース準備: API キー発行・Bundle ID 統一・アプリレコード「Subnote - サブスク管理」作成
やったこと:
- ramanu アカウント(info@ramanu.co.jp)の App Store Connect API を有効化 → チームキー発行: アクセス権リクエスト(規約同意)は You が実施、以降は Claude がアプリ内ブラウザを操作。キー名
subnote-mcp(ロール App Manager、Key ID8M2SWG78S6)。.p8 は~/.config/asc/AuthKey_8M2SWG78S6.p8に配置(chmod 600)。Issuer ID 等は .mcp.json 参照 - asc-subnote MCP を新設: プロジェクトルートに
.mcp.jsonを作成し、既存の自作 ASC MCP サーバー(~/.config/asc/mcp-server/server.js)を env 差し替えで流用。JWT 手署名の検証スクリプトでGET /v1/apps200 を確認(発行直後・アプリ0件)。次セッション起動時に承認すれば使える - Bundle ID
me.subnote.appを API で登録(POST /v1/bundleIds、In-App Purchase capability 自動付与) - アプリレコード作成(ブラウザ操作): Apple ID
6790385156、名前「Subnote - サブスク管理」、プライマリ言語=日本語、SKU=subnote、ユーザアクセス制限なし。「会社名」(App Store 開発元表示・以後変更不可)は「Ramanu LLC」で設定 - Xcode プロジェクトの Bundle ID 統一:
ios/SubnoteのPRODUCT_BUNDLE_IDENTIFIERをme.subnote.Subnote→me.subnote.appに変更(Debug/Release 両方)
決めたこと:
- iOS の Bundle ID を
me.subnote.appに変更(旧確定me.subnote.Subnoteを覆す)→ DECISIONS 2026-07-13 - App Store の会社名=「Ramanu LLC」/ アプリ名=「Subnote - サブスク管理」→ DECISIONS 2026-07-13
詰まったこと / 気づき:
- 公式 ASC API では「アプリレコードの新規作成」だけができない(Web UI 手作業)。それ以外(メタデータ・スクショ・TestFlight・審査提出・IAP/サブスク商品・価格)はほぼ API 化可能。ビルドアップロードは REST ではなく Xcode/Transporter(認証は同じ API キー可)
- アプリ名は App Store 全体でユニーク必須。「Subnote」単体は他開発者が使用済みで検証エラー → タグライン付きで回避(名前はバージョン更新時に変更可。会社名と違い永続ではない)
- 「無料アプリ契約」が検証中(2026-07-12〜、Apple が法人情報確認中、完了連絡は info@ramanu.co.jp 宛)。TestFlight 配信・審査提出はこれが終わるまでブロックされる可能性。サブスク課金には別途「有料 App 契約」+ 銀行口座・税務情報が必要(未着手)
- ASC MCP が 2 本立てに:
app-store-connect(ユーザーレベル、旧 Thomsons 別アカウント)とasc-subnote(プロジェクトレベル、ramanu)。混同注意(エージェント記憶にも記録済み) - ~/Downloads に SiwA 鍵
AuthKey_9MN99J7VFW.p8が残置されていた(パスワードマネージャ保管済みのはず)→ 削除する
次回やること:
- Xcode で新 Bundle ID のままビルド → アーカイブ → ASC へアップロード(契約検証と並行して準備可)
- アプリレコードのメタデータ整備(説明文・キーワード・スクショ。fastlane deliver でコード管理化を検討)
- Phase 2 残り: アカウント削除機能 / RevenueCat SDK + IAP 商品登録(有料 App 契約が前提)/ SiwA ネイティブ(App ID
me.subnote.appに capability 付与から)
18:00 - Apple Developer 審査通過 → SiwA 本番稼働・relay ドメイン登録・インフラ名義の法人統一(Supabase/Cloudflare)
やったこと:
- Sign in with Apple の設定一式 → 本番稼働(Phase 2 ブロッカー解消): Apple Developer 法人審査の通過を受け、WIP の手順書どおりに実施。App ID
me.subnote.Subnote(SiwA capability)→ Services IDme.subnote.web(Return URL = Supabase callback)→ 鍵 .p8 作成(Key ID9MN99J7VFW/ Team ID9HZW92277N、.p8 はパスワードマネージャ保管)→ gen-apple-secret.mjs で client secret 生成(有効期限 2027-01-09)→ Supabase Apple プロバイダ設定(Client IDs=me.subnote.web)→ Cloudflare Build variableNEXT_PUBLIC_ENABLE_APPLE_LOGIN=1→ デプロイ後、本番 /login からの Apple ログイン E2E 成功(ichirokisanuki.apple@gmail.com、「メールを共有」)。再生成コマンド:node scripts/gen-apple-secret.mjs --key <p8> --kid 9MN99J7VFW --team 9HZW92277N --client-id me.subnote.web - ADMIN_EMAILS を Worker secret で上書き: テストのため
ichirokisanuki@gmail.com,ichirokisanuki.apple@gmail.comの2件をwrangler secret putで設定(コード側の既定値を上書き、即時反映・リビルド不要)。正式公開時に Apple 用アドレスを残すか見直す - relay メールドメイン登録(Phase 2 の宿題を前倒し消化): Apple の Email Communication に
subnote.meを登録。SPF チェックが登録ドメインの apex を見るため初回は ✗ →(Resend の SPF はsend.subnote.me側にしかない)→ apex に TXTv=spf1 include:amazonses.com ~allを追加して緑チェック。「メールを非公開」ユーザーへの認証メール到達性を確保 - 保留していた為替 cron 変更を commit & デプロイ(
26d7d92): 実行時刻を 00:00 UTC → 01:47 UTC(00:00 ちょうどは Frankfurter が日次バッチ集中で 521 全滅する日が多いため時報から外す)+ 全滅時の通知(502)は既存レートが72時間超のときのみ(単発全滅は通知ノイズ扱い、failures はレスポンス/ログに残る) - Supabase を法人 org へ移管: info@ramanu.co.jp で新アカウント + org「RAMANU LLC」作成 → 個人アカウントを Developer で招待 → 個人アカウント側(移管元 Owner)から Settings → General → Transfer project 実行。ref / URL / API キー / DB / Auth 設定は無変更・ダウンタイムなし(Cloudflare Secrets・ローカル CLI も変更不要)
- Cloudflare アカウントを法人化(A方式): Google ログイン運用でパスワード未設定だったため forgot-password でパスワード設定 → ログインメールを info@ramanu.co.jp に変更 → 2FA 設定(リカバリーコードはパスワードマネージャへ)→ アカウント名を「RAMANU LLC」にリネーム。wrangler のローカルセッションは無影響(whoami で新メール表示を確認)
- dev-timer リマインダー登録: #32(2026-12-22 9:00)secret 再生成 / #33(2027-1-5 9:00)最終確認(期限 1/9)
決めたこと:
- Supabase の法人移管は「新法人アカウント + org 間 transfer」方式 → DECISIONS 2026-07-13
- Cloudflare は「アカウント自体を法人化」方式(完全移行はしない)→ DECISIONS 2026-07-13
詰まったこと / 気づき:
- App ID と Services ID は識別子の名前空間を共有する。App ID 作成時に誤って
me.subnote.webを入れかけた(先に使うと Services ID で同じ識別子が作れなくなる) - Apple の Email Sources の SPF チェックは apex を見る。Resend のドメイン認証は SPF を
send.サブドメインに置くため、apex には別途 SPF が必要(今回追加した apex SPF はスプーフィング対策としても本来あるべきレコード) NEXT_PUBLIC_*は Cloudflare の Build variables に入れる(Settings 直下の Variables and secrets はランタイム用で効かない。「Deploy」ボタンと wrangler config の注意書きが出るのがランタイム側)- Supabase Free org はメンバー招待無制限(Owner/Admin/Developer)・招待リンク24h失効・Owner/Admin メンバーの Free 枠は org 横断で合算判定(→ 個人アカウントは Developer 招待で巻き込み回避)。移管は「移管元 Owner かつ移管先メンバー」を一人で兼ねる必要があり、Supabase の GitHub 連携が残っていると移管がブロックされる
- subnote.me は Cloudflare Registrar 登録のため、アカウント間のゾーン移動はレジストラ転送を伴い高コスト(Cloudflare 法人化で A方式を選んだ決め手の一つ)
- 現 Cloudflare アカウントの中身: ゾーン=crosstechlab.com / entameen.com / ramanu.co.jp / subnote.me、Worker=entameen / subnote(会社ドメインが既に同居しており実態が会社インフラだった)
次回やること:
- 正式公開の判断(ベータガード削除 + 告知。ASO・文面は promo-expert 壁打ち)
- Phase 2(iOS)着手: アカウント削除機能 / RevenueCat SDK + IAP / SiwA ネイティブ(relay ドメイン登録は今回済み)/ 認証メール出し分け
- 手入力トリアージの本番目視 + ダイジェストメール着信確認(7/9 分の宿題、未確認なら)
- Google ログイン用 OAuth クライアント(Google Cloud)の法人名義化はいつか枠
- リカバリーコード txt(~/Downloads)をパスワードマネージャ保存後に削除
2026-07-09
20:14 - 手入力サービス名のトリアージ機能と日次メール通知(実装 → 本番反映まで完了)
やったこと:
- 課題: ユーザーがカタログ検索でヒットせず手入力したサービス名を、運営が発見してカタログに追加する仕組みがなかった。調査の結果、素材はほぼ揃っていた(
subscriptions.catalog_id IS NULL= 手入力の暗黙シグナル、/admin/catalog の追加フローはデプロイ不要)ので、「発見・判断」のレイヤーだけを追加 - トリアージ画面 /admin/catalog/requests: 手入力を
normalizeServiceName()で名寄せし「未対応 / カタログ一致 / 却下済み」に3分類。各グループにユーザー数・登録件数・金額例(プラン設定の参考)・タグを表示。「カタログに追加」→ 新規フォームに名前/通貨/タグをプリフィル、「追加しない」→ 却下(取り消し可) - 却下記録テーブル
catalog_review_dismissals(migration20260709000001、本番適用済み): 正規化名 PK の最小構成。カタログ追加された名前は normalized_name 一致で自動的に一覧から消えるため、記録するのは却下だけでよい - 確認付きバックフィル: サービス編集画面に「未紐付けの登録 N 件」セクションを追加。確認ダイアログを挟んで既存ユーザーの
catalog_idを一括設定(ロゴ・解約 URL が反映される)。対象はサーバー側で毎回再計算しクライアントから受け取らない - 日次ダイジェストメール: Cron Trigger 3本目(毎日 09:15 JST)→ /api/catalog-requests/digest。直近25時間(1時間重ねて取りこぼし防止)に新しい手入力があった日のみ、既存 Resend 基盤(cron 失敗通知と同じ
ALERT_EMAIL_TO)でトリアージ画面リンク付きメールを送る。新しい secret 不要 - admin トップに「手入力サービス(未対応 N 件)」カードを追加
- 検証:
pnpm build通過 + ローカル DB でランタイム検証(テストデータ投入 → digest ルート実行: 認証なし401 / カタログ一致除外 / 却下除外 / 25h 窓の新着判定すべて期待通り → データ掃除)。ローカルの既存手入力3件のうち U-NEXT・AWS はカタログ一致、「ほげ」が未対応と正しく分類された - 本番反映:
supabase db push(migration 1件)→ commit0e09a8dpush → Workers Builds 自動デプロイ
決めたこと:
- トリアージ運用の設計(検知は暗黙シグナル / 却下テーブル / 確認付きバックフィル / 日次メール)→ DECISIONS 2026-07-09
次回やること:
- デプロイ完了後に本番の /admin/catalog/requests を目視確認(本番にも手入力データがあれば未対応に出るはず)
- 明朝 09:15 JST 以降、新しい手入力があった日にダイジェストメールが届くことを確認
2026-07-06
20:10 - Phase 1 完了: ペイウォール実装・課金 E2E(障害2件を修正)・公開検証 → 再クローズ、Apple ログイン Web 実装
やったこと:
- Stripe 審査通過 → 本番接続: 本番 DB に migration 20260702000001 を適用(
supabase db push、無料5件ゲート稼働開始)+ 管理者に lifetime 付与。RevenueCat の残り設定(Web Billing 接続 / 商品3つ / Offering は未作成が判明し作成(パッケージは標準識別子$rc_monthly等を使用)/ webhook / Cloudflare Secrets 2つ) - Phase 1 実装(
cea68f0): 法務3ページ(/terms / /privacy / /tokushoho、未ログイン閲覧可)+ /premium ペイウォール(purchases-js、価格は Offering から動的取得、購入成功後はsyncMyEntitlementで即時同期)+ 設定画面のプランセクション + 5件上限エラーからの導線。チェックアウトはselectedLocale: "ja"で日本語化 +customerEmail事前入力(0ddef2e) - 課金 E2E(実カード・本番): 失敗経路(残高ゼロのデビット → チェックアウト内に日本語エラー、副作用なし)→ 成功経路(月額¥150 購入)→ premium が反映されない障害 → REST 直叩きで調査して2つの原因を特定・修正:
- v1 API は v2 secret key で 403(ダッシュボードで発行できるのは v2 キーのみ)→ 同期を v2
/customers/{id}/active_entitlementsに移行(9b8a310)。判定は「有効 entitlement が1つでもあれば premium、expires_atnull は lifetime」。/premium 表示時の自己修復同期も追加 - 購入商品が entitlement
premiumに未アタッチ(アタッチされていたのはオンボーディング時の placeholder 商品3つ)→ ダッシュボードで実商品3つをアタッチ(遡及適用で解決)
- v1 API は v2 secret key で 403(ダッシュボードで発行できるのは v2 キーのみ)→ 同期を v2
- 解約 E2E: RevenueCat 顧客ポータル経由で解約 →
willRenewを取得して「自動更新は停止済み」表示を出し分け(6747b9824559af)。返金は見送り(¥150 はテスト費用。実質コストは Stripe 手数料 ¥5 程度) - 表記統一: プラン名を「Subnoteプレミアム」(カタカナ)に統一(
1f3eca1、UI・規約・特商法。RevenueCat の商品 Display Name も You が変更) - 認証メールテンプレート改善: Web 用サインインボタン(インライン CSS)+ アプリ用6桁コード併記の両対応デザインに刷新、「beta」残骸を削除(Supabase ダッシュボードで適用)
- 公開検証 → 再クローズ: ベータガード撤去で一般公開(
20c04b5)→ 新規サインアップ E2E 成功(メール着信 / ログイン / user_settings 自動作成)→ 告知準備が整うまでガードを復元(48e5b73) - Sign in with Apple の Web 実装(
811f4f3):signInWithAppleアクション + Apple ガイドライン準拠ボタン。NEXT_PUBLIC_ENABLE_APPLE_LOGIN=1のフィーチャーフラグ制(Apple Developer 審査待ちのため本番未点灯)。client secret 生成スクリプト gen-apple-secret.mjs(依存ゼロ・ES256、使い捨て鍵で検証済み)。webhook 側は TEST イベントの早期 200 化 + 診断ログ強化(5f2e85a)
詰まったこと / 気づき:
- RevenueCat の落とし穴2つ(上記)はどちらも「webhook の TEST イベントが同期をスキップするため本番初購入まで顕在化しない」タイプだった。RevenueCat の v2 キーは権限スコープ制で、書き込み系(entitlement 設定・返金)には別途権限が要る
- Apple Developer は法人の加入審査がまだ通っておらず、App ID / Services ID / 鍵の作成がブロック中。SiwA の残作業(ポータル設定 → secret 生成 → Supabase 設定 → フラグ点灯)は審査通過後
- 手動付与した lifetime は webhook 同期で上書きされて消える(今回2回踏んだ)。手動付与を増やすなら別カラム分離が必要(ROADMAP いつか行き)
- 教訓: RevenueCat/Stripe の初期設定はダッシュボード手作業が本体(KYC・OAuth 接続)だが、設定系は v2 API で操作・検査できる。今回の「Offering 未作成」「未アタッチ」は API 経由でやっていれば防げた類のミス
次回やること:
- Apple Developer 審査通過 → SiwA 設定一式(手順書は 7/6 チャット参照・DEVLOG 下記)→ フラグ点灯
- 正式公開の判断(ベータガード再削除 + 告知。promo-expert に壁打ち可)
- Phase 2: iOS(RevenueCat SDK / アカウント削除機能 / SiwA ネイティブ / メール出し分け Send Email Hook)
2026-07-03
15:18 - リリース戦略の確定 + 課金基盤 Phase 0 + RevenueCat/Stripe 法人アカウント開設
やったこと:
- リリース戦略の棚卸し(7/2 開始): 「どういう形でリリースすべきか」を一から整理。マネタイズはフリーミアム(無料はアクティブ5件まで / Premium は月¥150・年¥1,200・買い切り¥2,500 の3本立て)、課金基盤は RevenueCat(Web は Web Billing、追加手数料なし・買い切り対応・エンタイトルメント自動統合を確認して Stripe 直接実装は不採用)、リリース順序は Web(決済込みフル構成)→ iOS → Android に確定。販売主体は法人(合同会社ラマヌ)
- 課金基盤 Phase 0 を実装(コミット
6f194c1):user_entitlementsテーブル +has_premium()+ 無料5件ゲートの DB トリガー(migration 20260702000001)。iOS/Android は Supabase 直接書き込みのため、上限強制はクライアント3箇所ではなく DB 1箇所に集約する設計。RevenueCat webhook 受け口 /api/revenuecat/webhook(fail-closed 認可、イベント種別で分岐せず REST で subscriber を取り直して同期=順序入れ替わり・取りこぼしに強い)。登録/復活アクションは上限エラーを typed error で返し(本番 Server Action の throw はメッセージがマスクされるため)、フォームにインライン表示 - ローカル検証: ゲートの SQL テスト8項目(6件目拒否 / 解約済み insert 通過 / 復活拒否 / RLS 下発動 / premium 通過 / 期限切れ premium 失効 / 通常 update 無影響)+ webhook の curl 4パターン(401/400/匿名スキップ/500リトライ)+ tsc / lint すべて通過
- RevenueCat アカウント開設(info@ramanu.co.jp): プロジェクト Subnote、Entitlement、Default Offering(Monthly / Yearly / Lifetime)まで作成
- Stripe 法人アカウント開設 → 審査提出まで完了: 法人番号検索で事業情報入力、明細書表記 SUBNOTE、セキュリティ対策措置状況申告書を提出(管理画面=メール許可リスト制、OTP のレート制限、カード情報は Stripe ホスト型で自社を通らない、という実装事実で回答)。Managed Payments(+3.5%)は不採用、Stripe Tax / Climate はスキップ
詰まったこと / 気づき:
- Google Play の「12人×14日テスター要件」は個人アカウント限定=組織アカウントのラマヌには適用されない(Android リリースが想定より軽くなる)
- iOS は Google ログインを載せている以上 Sign in with Apple が審査必須(ガイドライン 4.8)= iOS 側の唯一の実装ブロッカー
- ramanu.co.jp は Cloudflare のボット保護で機械アクセスが 403 になる。Stripe の自動サイト確認に弾かれたら一時的に緩める
次回やること:
- Stripe 審査通過確認 → RevenueCat に Web Billing 接続
- RevenueCat: Apps 登録(
me.subnote.Subnote/me.subnote.android)・商品3つ・webhook 設定、Cloudflare に Secret 2つ登録 - 本番 migration 適用 + 管理者アカウントに lifetime 付与
- Phase 1: 法務3ページ(プラポリ / 利用規約 / 特商法)→ ペイウォール UI + Web Billing 組み込み → ベータガード解除 → Web 公開
2026-06-23
09:52 - Supabase auto-pause からの復旧 + keep-alive 堅牢化 + cron 失敗メール通知
発端:
- Supabase から「subnote プロジェクトが7日 inactivity で auto-pause された」メールが届いた。「対策したのになぜ?」を起点に原因を追った
やったこと:
- 原因特定: keep-alive(Cloudflare Cron Triggers が毎日
/api/exchange-rates/refreshを叩いてexchange_ratesに upsert する)は確かにデプロイ済み・発火もしていた。が、Supabase への書き込みが Frankfurter API の成功に完全依存していた。旧コードは4通貨のうち1つでも!res.okなら upsert 前に return 502 する構造で、Frankfurter が数日落ちた間は Supabase に一切アクセスが発生せず inactivity 判定 → pause。cron は無実で、書き込む中身が出てこなかっただけ - Supabase を手動 unpause(You がダッシュボードで実施)
- keep-alive を Frankfurter から切り離す(refresh/route.ts): Frankfurter を呼ぶ前に Supabase を必ず1回 read(
select fetched_at limit 1)。為替が全滅しても pause リスクが消える。さらに1通貨失敗で全体中断せず取れたぶんだけ upsert。健全性を HTTP ステータスに反映(keep-alive/upsert 失敗=500、為替全滅=502、部分失敗=200) - cron 失敗時のメール通知を追加(Resend): lib/notifications/send-email.ts に
sendAlertEmail()(fetch で Resend API を叩くだけ、依存追加なし、env 未設定なら no-op)。worker.ts の両 cron の関所callInternalで、HTTP エラー or 例外時に送信。Resend 専用キーsubnote-worker-cron(Sending access / domain=subnote.me)を発行し、Worker env にRESEND_API_KEY/ALERT_EMAIL_FROM(alerts@subnote.me)/ALERT_EMAIL_TOを設定 - 本番疎通確認: 使い捨てのテストエンドポイントを一時デプロイ → 実際に Gmail へテストメール着信を確認 → エンドポイント削除
詰まったこと / 気づき:
- メール送信は worker.ts の cron 経路(callInternal)の中だけにあるため、refresh ルートを直接 curl しても通知経路は通らない。テストには別途エンドポイントが要った
- Next.js App Router の
_始まりフォルダは private folder 扱いでルーティング除外=404。一時エンドポイントを_alert-testで作って踏み抜き、alert-selftestに改名して解消 - 「subnote-smtp」という既存 Resend キーは Supabase Auth の Custom SMTP 用(6/7 に発行)。用途を分けるため worker 用は新規キーにした
- cron が
console.errorするだけで1週間誰も気づけなかったのが「対策したのに止まった」の隠れた一因 → メール通知で可視化
次回やること:
- 数日 Supabase が再 pause されないことを確認(keep-alive の実効検証)
- 従来の継続タスク(Android Google ログイン E2E / iOS ウィジェット / メール送信量の監視)は変わらず
2026-06-08
18:06 - iOS 月送りのビルド検証 + ホームにブランドヘッダー追加
やったこと:
- iOS 月次チャート「月送り」のビルド検証(前セッションで未検証コミット a9a861b として保存していた分):
- Xcode 26.5 / iPhone 17 Pro シミュレータで
xcodebuildビルド成功 → コミットメッセージの「ビルド未検証」を解消 - 起動して初期描画を目視: 推移セクション見出しに
6月 ¥32,962 [今月]、当月バー強調。既定で当月がfocusedPointとして見出しに出る挙動OK - タップ実挙動のスクショ自動化は断念(idb-companion は Meta がアーカイブ済みで facebook/fb tap から削除されており導入不可、simctl 単体はタップ非対応)。代わりにコード精査で結線を確認=
.chartXSelection(value:)(Swift Charts 公式 API)でバー選択 →selectedLabel→focusedPoint再計算 → 見出し更新 +barColorでハイライトの流れが妥当。ビルド+初期描画+結線の3点で実用上検証済みと判断
- Xcode 26.5 / iPhone 17 Pro シミュレータで
- iOS ホーム上部にブランドヘッダーを追加(HomeView.swift)= 高級感目的:
- システム大タイトル(
.navigationTitle("Subnote"))は左にアイコンをインライン挿入できないため、.navigationBarTitleDisplayMode(.inline)+safeAreaInset(edge: .top)でブランドマーク「S」+ Subnote ワードマークの左寄せロックアップを全状態(読み込み/空/データ)共通で表示。並び替え/歯車は上の細バーに残す - 素材は既存
BrandMarkimageset。マーク 40pt・角丸9・極薄枠、ワードマークはlargeTitle.bold()をscaleEffect(0.85, anchor: .leading)で縮小、offset(x: -2, y: -1)で光学位置を微調整(You とスクショ往復で詰めた最終値) - シミュレータでビルド→インストール→スクショを繰り返して目視で追い込み
- システム大タイトル(
気づき:
- iOS シミュレータのタップ自動化は手段が痩せた(idb 廃止、simctl 非対応)。操作を伴う検証は You の手タップかコード精査に頼る前提に戻った
- ブランドのアイコン+ワードマークは「マーク主役・文字は一段控えめ(0.85倍)」がバランス良いという結論
16:23 - Android ホーム画面ウィジェット(月額合計)を仕上げ + 実機 E2E 検証
やったこと:
- 前セッションで作りかけだった Android ホーム画面ウィジェット(月額合計) を仕上げ、エミュレータで End-to-End まで動作確認
- 構成は既にコードが揃っていた状態だったので、参照整合性とビルド・実機描画を検証して確定:
- MonthlyTotalWidget.kt(新規): Glance
GlanceAppWidget+GlanceAppWidgetReceiver。ウィジェット側で Supabase は叩かず、アプリが計算済みの表示テキストを Glance Preferences ステート(KEY_TOTAL/KEY_SUB)に書き込み、ウィジェットはそれを描画。updateMonthlyTotalWidget(context, total, sub)で配置済み全ウィジェットへ反映(未配置なら no-op)。インディゴ背景・角丸24dp・タップでMainActivity起動 - res/xml/monthly_total_widget_info.xml(新規): appwidget-provider。3×2 セル /
updatePeriodMillis=0(更新はアプリ主導)/ Glance 既定 loading layout - AndroidManifest.xml:
MonthlyTotalWidgetReceiverを APPWIDGET_UPDATE で登録 - HomeScreen.kt:
LaunchedEffect(subscriptions, displayCurrency)でサブスク/表示通貨が変わるたびにformatMoney(viewModel.monthlyTotal(), displayCurrency)と「N 件・通貨 換算」をウィジェットへ反映 - build.gradle.kts / libs.versions.toml:
androidx.glance:glance-appwidget 1.1.1追加 - strings.xml: ウィジェット説明文
- MonthlyTotalWidget.kt(新規): Glance
- 参照シンボル(
formatMoney/viewModel.monthlyTotal()/BrandIndigo)の実在を確認 →assembleDebugビルド成功 - エミュレータ(kowai16k)で実データ検証: アプリホーム = ¥32,962 / 13件、ホーム画面ウィジェット = ¥32,962 / 13 件・JPY 換算 で完全一致。
dumpsys appwidgetでプロバイダ登録 + ウィジェットインスタンス配置済みを確認。ウィジェットタップでMainActivity起動も確認
気づき:
- ウィジェットは別プロセスで動くため Supabase を直接叩かせず「アプリが計算 → Glance Preferences に表示テキストを置く → ウィジェットは描画専念」の責務分離。ホーム読み込み時の
LaunchedEffectで自然に最新化される - iOS の月次チャート「月送り」対応(バータップで過去月へ)の未コミット変更が作業ツリーに残っていた(本セッション着手前から)。失わないよう別コミットで保存(iOS は本セッションでビルド未検証)
2026-06-07
23:26 - 未目視デザインの実機確認 + チップ/アイコン質感 + Android Google ログイン投入
やったこと:
- 前セッションの未目視4点を実機スクショで目視確認(新セッションで画像 API 制約がリセットされ目視復活)→ ① 推移チャート「6月」金額ラベル切れ=修正OK、② 編集フォーム grouped の余白感=良好、③ FAB 円形=OK、④ ダークモード色味=light/dark ともホーム/編集で破綻なし
- タグチップ/イニシャルアイコンの質感調整(android/.../Components.kt):
TagChip/CategoryChip: 未選択を surfaceVariant(薄グレーで背景に沈む)→ 白ピル + outlineVariant のハイライン枠に。パディング 12/6→14/8 に微増ServiceIcon: 外周に 1dp ハイライン枠(白背景ロゴが白カードに溶けないように、border→padding(1dp)→clip→background順で枠を残す)InitialLetter: SemiBold 化
- 推移チャート当月ラベルの微調整(Components.kt / HomeScreen.kt):
BarChartにshowCurrentLabel引数追加。ホームはfalse(月額はヒーローで既出=重複排除)→ バー上の浮き金額ラベルを廃止し、バー領域を広げて上端の窮屈さも解消。barAreaHeight = maxHeight - 38dp - (ラベル表示時のみ24dp) - Android に Google ログインを投入(ブラウザ=Custom Tab 経由 OAuth、iOS の ASWebAuthenticationSession 方式と対称):
- SupabaseClient.kt:
install(Auth){ scheme="subnote"; host="login-callback" }(既定リダイレクトsubnote://login-callback) - AndroidManifest.xml: VIEW/BROWSABLE intent-filter(
subnote://login-callback)+launchMode="singleTask" - MainActivity.kt:
supabase.handleDeeplinks(intent)を onCreate / onNewIntent で処理 - AuthViewModel.kt:
signInWithGoogle()=supabase.auth.signInWith(Google) - LoginScreen.kt: 「または」区切り + 「Google でログイン」OutlinedButton
- SupabaseClient.kt:
- ビルド成功(
assembleDebug)、emulator にインストール → 全変更を実機目視。ログアウト→OTP 再ログイン(Gmail MCP でコード取得)まで通して確認。Google ボタンはタップで Chrome Custom Tab が起動することまで確認
気づき / 重要な確認:
- supabase-kt 3.1.1 の Android Google ログインはブラウザ方式で済む → gradle キャッシュの aar を javap で確認:
Googleプロバイダ /signInWith/ Auth 設定のscheme/host/handleDeeplinks/ androidx.startup で Context 自動取得、すべて存在。SHA-1・Android タイプ OAuth クライアントは不要(SHA-1 が要るのはネイティブ Google Sign-In=ID トークン方式だけ)。WIP の「Android は SHA-1 登録が追加で必要」は方式の混同で、今回の実装では当たらない - Supabase の Google プロバイダは既に有効だった: 公開エンドポイント
GET /auth/v1/settings(apikey=publishable)でexternal.google: trueを確認。WIP の「Google プロバイダ有効化が未設定」は古い記述。iOS/Web/Android は同じ Supabase callback・同じ Web OAuth クライアント・同じsubnote://login-callbackを共有するため、設定はプロジェクト単位で1回きり。Android 用の追加ポチポチは不要で、そのまま動く想定(実機での End-to-End は未検証) - debug キーストア SHA-1 =
0A:A6:9B:9E:A2:18:60:03:56:64:08:3E:C8:2E:B1:F1:18:18:03:2C(将来ネイティブ方式に切替える場合のみ使用)
次回やること:
- Android Google ログインの End-to-End 確認(emulator は Chrome 初回設定/Google アカウント未ログインで詰まりやすい → 実機で
app-debug.apk推奨) - Android のウィジェット相当(ホームに月額合計)/ メール送信量の監視 / cancel_url 残り投入
- iOS の追加磨き(月送り / ウィジェット)
やったこと:
- 編集フォームを grouped 白カード化: iOS の grouped フォーム風に再構成(グレー背景 + セクション見出し「サービス/料金/次回課金日/タグ/危険操作」 + 白カード内の枠線なしフィールド)。解約・削除も白カード内の行スタイル(primary / error 色)、課金日は行タップで DatePicker・選択日を primary 色表示。uiautomator で構成を確認
- FAB をまんまるに:
shape = CircleShapeを指定(Material3 デフォルトは角丸正方形) - menuAnchor() の deprecation 解消:
MenuAnchorType.PrimaryNotEditableの新 API へ → ビルド警告ゼロ - 未使用 import 整理(HomeScreen の CircularProgressIndicator / LoginScreen の Spacer)
- 推移チャートの当月金額ラベルが切れる問題を修正: 当月が最大値だとバーが満杯になり上の金額ラベルの余白が無く切れていた → チャート高さ 160→176dp、バー領域計算で上ラベル分(約24dp)も差し引く(
barAreaHeight = maxHeight - 62dp)、ラベルを softWrap=false / overflow=Visible + 太字化 - ダークモード起動をクラッシュなしで確認(配色適用、色味は未目視)
詰まったこと / 重要な制約:
- 画像 API の制約でスクショ目視が途中から不可に。エラー文は
max allowed size for many-image requests: 2000 pixels。画像を 900/760/640px に縮小しても弾かれた=1枚のサイズではなく、この会話に蓄積したスクショの枚数/総量が効いていると推測。新セッションに切り替えれば画像蓄積がリセットされ目視は復活する見込み(ただし新セッションでも撮りすぎれば再発するので、節目だけ小サイズ + uiautomator テキスト確認中心を継続) - このため 22:14 以降のデザイン変更(編集フォーム grouped・FAB 円形・チャート切れ修正・ダークモード色味)は構成/ビルド/クラッシュ無しは確認済みだが、ピクセル単位の見た目は未目視。次セッション冒頭で実機目視確認するのが安全
次回やること(新セッション・目視前提):
- 未目視項目を実機スクショで確認: チャート「6月」切れ修正の結果 / 編集フォーム grouped の余白感 / FAB が円形か / ダークモードの色味
- 気になれば微調整(チャートの当月金額はバー上ではなく「推移」見出し右への移設も選択肢)
- タグチップ/イニシャルアイコンの質感調整(目視しながら)
- Google ログイン本番化(Supabase 設定が前提で保留)/ ウィジェット相当
22:14 - Android の見た目を iOS 品質に刷新(デザイン全面見直し)
やったこと:
- 「機能は同等だが見た目が iOS と比べてダサい」という指摘を受け、実機スクショで診断 → 主因は ① Material3 デフォルトが primary から紫がかった surface を自動生成して背景が薄紫になる、② 区切り線だけのベタ置き一覧、③ 全行の ⋮ メニュー、④ ベタ塗り合計カード・スカスカのチャート、と特定
- 配色(Theme.kt)を全面刷新: Material3 デフォルトを上書きし、iOS の systemGroupedBackground 風に「薄グレー背景(#F2F2F7) + 白カード(#FFFFFF) + 黒/グレー階層」へ。light/dark 両方を明示定義して紫汚染を排除
- ホームを inset grouped 風に再構成: グラデーション(indigo→violet)の月額合計ヒーローカード / 白カード内の推移チャート(当月に金額ラベル + 太バー) / 丸型の白い検索バー / 一覧から ⋮ を廃止しタップで編集(解約・削除は編集画面に集約)
- 設定・解約履歴を grouped スタイル化: 白角丸カード + セクション見出し + 行区切り。設定は iOS のグループ化設定そのものの質感に
- ログイン画面を刷新: グラデーションのブランドマーク(白い S) + 太字ロゴ + サブタイトル + 丸型入力 + primary ボタン
- 共通パーツを追加:
GroupCard(薄い影 1dp 付き白カード) /SectionLabel/SearchField/BrandMark - カタログ/プラン/編集の背景・TopAppBar もテーマに統一
- 実機検証: 配色・ホーム・設定・ログイン(ログアウト→新ログイン→再ログイン)を emulator で確認。デフォルト Material 感が消え iOS 品質に到達
気づき:
Icons.Default.Search等のアイコンはプロパティの個別 import が必要(androidx.compose.material.icons.filled.Search)- 画面ごとに private な
SectionLabelを持つと共通版と衝突するので共通化(Components.kt に集約) - emulator スクショは解像度が大きいと画像 API に弾かれることがある →
sips -Zで縮小して読む
次回やること:
- 編集フォームの grouped 白カード化(今は白背景 + 枠付きフィールドで実用十分)
- タグチップ/イニシャルアイコンの微調整、ダークモードの実機確認
- Google ログイン本番化(Supabase 設定が前提で保留中)/ ウィジェット相当
21:52 - Android 版に着手 → iOS と機能同等まで一気に到達
やったこと:
- 技術選定: Android はネイティブ Kotlin + Jetpack Compose + supabase-kt に決定(iOS の SwiftUI と対称、性能と保守性の両立。KMP/Flutter は既存 iOS 資産を活かせず割に合わないため不採用)
- 環境確認: Android Studio / SDK(platform 33・35、build-tools 34・35、system-image android-35 arm64)/ adb / sdkmanager 揃い。AVD
kowai16k既存。javaは Android Studio 同梱 JBR(OpenJDK 21)、emulatorは SDK 内。gradle CLI は無いので wrapper を GitHub から取得(Gradle 8.11.1 の jar/gradlew を curl) - プロジェクト雛形: モノレポに
android/新設。Gradle 8.11.1 / AGP 8.7.3 / Kotlin 2.1.0、version catalog、compileSdk 35 / minSdk 26、namespaceme.subnote.android。アダプティブアイコン(ベクター "S"、indigo#4F46E5) - 認証: iOS と対称のメール OTP(
signInWith(OTP)→verifyEmailOtp)、sessionStatus購読でセッション復元 - iOS 機能をフル移植:
- ホーム: ロゴ画像(Coil)/ 検索 / 並び替え / タグ別集計チップ / もうすぐ更新 / 月次推移チャート(Compose Canvas で自作)/ 月額合計カード / 行ごとの操作メニュー(⋮)
- 新規登録: カタログ選択フロー(検索 + カテゴリチップ → プラン選択 → フォーム自動補完、手入力も可)
- 編集: 金額バリデーション / 通貨・周期・次回課金日(DatePicker)・タグ / 解約・完全削除
- 設定: 表示通貨切替(user_settings upsert)/ 解約履歴 / ログアウト
- 解約履歴: 復活(確認ダイアログ)/ 今月解約件数 / 解約日表示
- 集計・通貨換算ロジックは Web/iOS と同一仕様で移植(
convertCurrency/ 年額÷12 / タグ別) - Repository(
SubnoteRepo)に Supabase 読み書きを集約、Navigation Compose で画面結線(VM は Activity スコープ共有、編集対象/プリセットは VM 経由で受け渡し)
- 第一印象の磨き込み: ネイティブ起動スプラッシュ(core-splashscreen、indigo 背景 + 白い S)/ ホーム読み込み中のスケルトン行 / FAB ハプティクス
- 実機検証(emulator kowai16k): 本番 Supabase に接続し、OTP ログイン(Gmail からコード取得して adb で投入)→ 月額合計¥32,962/13件・USD→JPY 換算込みの実データ表示を確認。ホーム/編集/設定/解約履歴/カタログ選択を全て目視確認。セッション永続(force-stop 後もログイン維持)も確認
詰まったこと / 気づき:
- supabase-kt の
is not nullフィルタ API が不確実だったため、解約済み判定はクライアント側フィルタに統一(canceledAt != null) ExposedDropdownMenuはスコープのメンバなので非修飾呼び出しが必要(修飾でビルド失敗)- Android は adb で install/launch/screenshot に加え
adb shell input tap/textで操作まで自動化可能(iOS の simctl と違いタップできる)。OTP は Gmail MCP からコード取得して投入できた - FAB 等のタップ座標は画面スケール差で不安定(プラン選択画面のみ目視スクショ未取得、コードは他画面と同型で結線済み)
次回やること:
- Google ログイン(iOS 同様コードは入れられるが、Supabase の Google プロバイダ有効化が前提で動作確認できないため保留)
- ウィジェット相当(ホームに月額合計)
- menuAnchor() の deprecation 解消(警告のみ)
21:00 - メール基盤を Resend + 独自ドメイン認証に移行(環境おさらい起点)
やったこと:
- セッション冒頭で「サービス環境のおさらい + スケーリング観点」を整理。現状アーキ(Cloudflare Workers/OpenNext + Supabase + Cron)を確認し、人数増で効く順のボトルネックを洗い出し(① Supabase 同時接続/Pooler、② 認証メールのレート、③ 月次 snapshot cron の単発実行、④ Workers 課金、⑤ Supabase 容量)
- ②の「認証メール 1h/4通制限」を解消するため、別プロジェクト review-antenna のメール実装(Resend REST 直叩き、送信元
onboarding@resend.devでドメイン未認証)を参照 → Subnote は不特定ユーザーに送るので独自ドメイン認証が必須と判断 - Resend に
subnote.meをドメイン登録(API 経由、domain idecdbf27c...)→ DKIM/SPF(MX)/SPF(TXT) の3レコードを取得 - Cloudflare DNS に3レコードを自動登録(CF API トークン Zone DNS Edit を発行してもらい、API で投入)→ Resend で検証 → 全レコード verified
- ドメイン疎通テスト:
noreply@subnote.me→ Gmail INBOX 着信を確認(SPF/DKIM パス) - Supabase Auth の Custom SMTP を Resend に設定(host
smtp.resend.com/ port 465 / userresend/ pass=送信専用 Resend キー、sendernoreply@subnote.me)→ anon キーで OTP 送信を叩き、マジックリンクが Resend 経由で Gmail INBOX 着信することを確認=本番フロー成功 - DMARC レコード追加(
_dmarcTXT =v=DMARC1; p=none;、手動登録 → 1.1.1.1/8.8.8.8 両方で反映確認) - Supabase の Auth Rate Limit「sending emails」を 200/時 に引き上げ
- 作業用の一時キー(フルアクセス Resend キー / CF トークン)はローカル削除 + ユーザー側で revoke 済み
決めたこと:(詳細は DECISIONS.md 参照)
- メール送信基盤を Supabase 共有SMTP → Resend + 独自ドメイン認証(SPF/DKIM/DMARC) に移行
詰まったこと / 気づき:
- 「1h/4通制限」には2種類あった: ① Supabase 内蔵共有SMTP の貧弱さ(Resend 移行で解消)、② Supabase Auth 自体の送信回数 Rate Limit(SMTP を変えても残る、カスタムSMTP 時のみ自分で引き上げ可能)。今回②を 200/時 に設定
- 実効上限は常に「Supabase Rate Limit と Resend 配送枠の小さい方」。Resend 無料枠は 日100通/月3000通 なので、当面の実効上限は日100通。ユーザー増で最初に来る課金判断は Resend Pro($20/月・月5万通)
- 検証監視ループは最終チェック時 pending で timeout 扱いになったが、直後の手動確認で verified(DNS 伝播のタイミング差)
次回やること:
- Supabase の Google プロバイダ有効化(別件・継続)
- メール量が日100通/月3000通に近づいたら Resend Pro へ。DMARC は運用安定後に
p=none→quarantine→rejectに段階強化
18:48 - iOS の Google ログイン/スワイプ操作を検証確定 + Web に Google ログインを実装
やったこと:
- 別セッションが途中で不調になったため引き継ぎ。未コミットだった iOS の2機能を検証して確定:
- Google ログイン:
signInWithOAuth(provider: .google, redirectTo: subnote://login-callback)、Info.plist に URL schemesubnote登録、GoogleLogoアセット、ログイン画面に「または」区切り + ボタン - 一覧スワイプ操作: 行の左スワイプで「解約」(オレンジ)/「削除」、それぞれ confirmationDialog。解約は cancel_url があれば「解約ページを開く」も提示。
SubscriptionStore.cancel()/delete()に集約
- Google ログイン:
- iOS をシミュレータ(iPhone 17 Pro / iOS 26.5)でビルド→起動検証: BUILD SUCCEEDED、ログイン画面の Google ボタン描画 OK、Google OAuth が Supabase 同意プロンプトまで到達することを確認(ログイン後のスワイプ実操作は simctl にタップ無し=You が手動確認)
- Web に Google ログインを実装(iOS とのパリティ):
signInWithGoogle()サーバーアクション(signInWithOAuthで得た URL にredirect()、PKCE verifier クッキーはアクション内で発行)components/login/google-button.tsx(公式4色 G ロゴを SVG 埋め込み、useFormStatusで pending 表示)- ログイン画面に「または」区切り + Google ボタン追加(ネスト form を避けてフォーム外に配置)
- 既存の
/auth/callback(exchangeCodeForSession)をそのまま流用 - 検証:
tsc --noEmit/eslintパス、dev サーバーで/login200 + ボタン描画をスクショ確認
決めたこと:(詳細は DECISIONS.md 参照)
- 認証手段に Google OAuth を追加(マジックリンク/OTP と併用、iOS/Web 共通)
詰まったこと / 気づき:
- シミュレータの OS(26.3.1)がアプリの最低 OS(26.5)より低くインストール不可 → iOS 26.5 ランタイムのデバイスを選び直して解決
- Web で Google ボタンを
<form>として作ったため、loginフォームの中に入れるとネスト form で不正 → メールフォームの外(同一カード内)に配置して解決 - Google ログインの実動作には Supabase 側の Google プロバイダ有効化 + Google Cloud OAuth クライアントのリダイレクト URI 設定が前提(コードでは完結しない、未設定)
次回やること:
- Supabase の Google プロバイダ設定(iOS/Web 共通で有効化) + 本番での Google ログイン動作確認
- iOS の残り磨き(月送り対応 / ウィジェット)
- Android 着手
2026-06-05
20:55 - iOS: 第一印象パック + ブランドマーク統一 + カタログ選択/設定/解約履歴/チャート/質感/バリデーション
やったこと:
- 第一印象パック(起動品質): 「単純なアプリほど起動時の品質で印象が決まる」という方針のもと一気に整備
- アプリアイコン(コミット
4944cf8→fcba5b7→a73c4a9): 当初 indigo+S を自動生成 → Web ヘッダーの本物マーク(スタックカード + セリフ S)と違うと指摘 → 機械的再現は「味が死ぬ」ため破棄 → **ユーザー提供の高解像度ロゴ(1254px)**を採用。焼き込まれた透明チェック柄を ImageMagick の角フラッドフィルで除去(カード白地は黒線で囲まれてるので保持)、白背景合成で 1024px アイコン化 - 起動演出(
a7d31e7→b240512): SwiftUI スプラッシュ(ロゴをふわっと、0.7s でフェード)+ ネイティブ Launch Screen 背景。最初 indigo→後にユーザー要望で白背景に(起動背景も白で白フラッシュ回避) - アクセントカラーをブランド indigo (#6366f1) に統一(FAB/ボタン/チップ)、ログイン画面にブランドマーク
- アプリアイコン(コミット
- カタログ選択フロー(コミット
0823c99): 新規登録で サービス検索 + カテゴリチップ絞り込み → プラン選択 → フォーム自動補完(名前/金額/通貨/周期/タグ/catalog_id)。「手入力で登録」併設。CatalogModels/CatalogStore/CatalogBrowseView、SubscriptionEditViewを preset 対応 + insert に catalog_id - 設定画面(
d37b933): 歯車→設定シート、表示通貨ピッカー(upsert user_settings → 合計再換算)、メール表示、ログアウト集約 - 解約履歴(
8ff4821): 設定→解約履歴。canceled_at NOT NULL を新しい順、今月解約件数、復活ボタン + 確認ダイアログ - 月次推移チャート(
e6dd85a): Swift Charts で過去12ヶ月の月額合計(monthly_snapshots + 当月ライブ)。当月=アクセント色。最大12本のローリング表示、過去スナップショットが無い新規ユーザーは非表示 - 質感アップ(
e6dd85a): 読込中スケルトン(redacted)/ ハプティクス(FAB=light、保存・解約・復活=success、削除=warning)/ 空状態文言を「右下の+から登録」に - フォームバリデーション(
a419414): 名前+金額(0以上の数値)を満たすまで保存ボタン非活性、不正時は赤ヒント
決めたこと:
- iOS のブランドマークは Web の実ロゴ(スタックカード+セリフS)を使う。低解像度しか無い場合の機械的再現は不可(味が失われる)→ 高解像度の元データを使う
- スプラッシュ・起動背景は白
- チャートはデータがある月だけ表示(空バーは「¥0使った」と誤解されるため出さない)
詰まったこと / 気づき:
- 同期グループ方式のため Info.plist をソースフォルダ内に置くと Copy Bundle Resources に二重登録される →
.xcodeprojと同階層(ソースフォルダ外)に置いて回避、INFOPLIST_FILE = Info.plist - 色アセット名から
Color.brandIndigo等が自動生成されるので、手書き extension と衝突する simctl io screenshotでスプラッシュ/アイコン/チャートを撮って Claude 自身が目視確認できた(操作系は引き続き要 You タップ)
次回やること:
- iOS: スワイプ削除/解約・月送り対応・ウィジェット のいずれか
- cancel_url の残り投入(niコニコ / DMM TV / 楽天系 等)
18:49 - iOS: 閲覧系(検索/ソート/タグ別/もうすぐ更新)+ 書き込み系(登録/編集/解約/削除)を実装
やったこと:
- 検索 + 並び替え(コミット
4efe0f9):.searchableでサービス名・タグのインクリメンタル検索、ソートメニュー(金額高/安・次回課金が近い・名前順、金額は換算月額で比較) - 「もうすぐ更新」セクション(コミット
b8817ed):next_billing_dateが今日〜3日以内のサブスクを月額合計の下に集約(今日/明日/N日後チップ)。Subscription.daysUntilBilling/billingRelativeLabelを追加 - タグ別集計 + フィルタ(コミット
350f49e):SubscriptionStore.tagTotals(換算金額+件数)、横スクロールのタグチップ、タップで一覧フィルタ - 編集画面(同
350f49e): 行タップ→SubscriptionEditView(名前/金額+通貨/周期/次回課金日/タグ)→update→リロード。memo はモデル未対応のため触らない、next_billing_date は OFF で null 明示送信 - 新規登録 + 解約 + 削除(コミット
1e3856b): 右下 FAB「+」→ シートで登録(user_id付きinsert)。編集画面に解約(canceled_atセット、cancel_urlあれば「解約ページを開く」)/ 完全削除(delete)。SubscriptionにcatalogCancelURL追加、ログアウトは ⋯ メニューに集約、解約/削除ボタンを中央寄せ - Run 自動化の確立:
xcodebuild(DEVELOPER_DIRで Xcode 26.5 指定)でビルド →xcrun simctl terminate/install/launchで「Stop→Run」相当 →simctl io screenshotで画面を撮って Claude 自身が目視確認、までを CLI で実行できることを確認
詰まったこと / 気づき:
- iOS のサービスアイコンは Web の
object-cover rounded-lgに合わせscaledToFill+ 角丸クリップ(scaledToFit+padding だと色付き四角ロゴが中央に小さく浮いて角丸が効かない) simctlには UI タップ機能が無いため、「見た目の確認」は Claude が巻き取れるが「操作を伴う検証」(保存・削除フロー等)は You のタップが必要(idb 等を入れれば自動化可)
次回やること:
- 新規登録のカタログ選択フロー(サービス検索→プラン→フォーム、ロゴ/標準料金の自動補完)
- iOS 設定画面(表示通貨切替・ログアウト集約)/ 解約済み履歴ページ / アプリアイコン・起動画面
2026-06-04
21:50 - iOS: ログイン(メール OTP)+ サブスク一覧(換算合計 + アイコン)を実装
やったこと:
- Xcode で SwiftUI プロジェクト作成(
ios/Subnote/Subnote.xcodeproj、ProductSubnote/ Bundleme.subnote.Subnote/ iOS 17+)。PBXFileSystemSynchronizedRootGroup(同期グループ)方式なので、フォルダに置いた.swiftは自動でビルド対象 - supabase-swift(SPM)を導入。初回は製品
Supabaseをターゲットにリンクし忘れていてビルド失敗→ General > Frameworks に追加で解消 - メール OTP ログインを実装(コミット
4830c11)Config(本番 URL + publishable key)/SupabaseManager(共有クライアント)AuthViewModel:signInWithOTPでコード送信 →verifyOTP(type: .email)で検証、authStateChanges購読でセッション復元LoginView(メール→コードの2ステップ)/HomeView(仮)/ContentView(認証で切替)- ⚠️ ハマり: デフォルトの Magic Link メールテンプレートはリンクのみでコードが無い。Supabase ダッシュボードの Magic Link テンプレートに
{{ .Token }}を追加して解消(テンプレ反映に数分のラグあり)
- ホームをサブスク一覧に(コミット
1eedefc)Subscription(amount は number/string 両対応で decode)/ExchangeRate/UserSettingsモデル +SubscriptionStoreexchange_ratesとuser_settings.display_currencyを取得し、表示通貨に換算した月額合計(Web のconvertCurrency/calcTotalを移植)→ Web と一致を確認service_catalog.logo_urlを埋め込み取得(select("*, catalog:service_catalog(logo_url)"))しAsyncImageで角丸アイコン表示(scaledToFill+ clip、未紐付けは頭文字フォールバック)- pull-to-refresh / 空・エラー表示
- ⚠️ Swift の MemberImportVisibility が有効で、
@Published/ObservableObjectにimport Combine、Session.user.emailにimport Auth、PostgREST のクエリメソッドにimport Supabase/import PostgRESTが必要だった
AGENTS.mdとoutput_logo/を tracked 化(iOS コミットに巻き込まれて混入したが、残す方針で確定=長年の untracked 片付け TODO が解決)
決めたこと:
AGENTS.md(他エージェント用指示書)とoutput_logo/(カタログのロゴ元素材)は git 管理する(untracked のままにしない)
詰まったこと / 気づき:
- iOS の Supabase は MemberImportVisibility のせいで、型は使えてもメンバーアクセスに定義元モジュール(Auth/PostgREST 等)の明示 import が要る
xcodebuildが CommandLineTools を向いていて失敗 →DEVELOPER_DIR="/Applications/Xcode 26.5.app/Contents/Developer"で回避(sudo 不要)- 為替レートが 6/1 以降に更新され合計額が動いた(換算ロジックは Web と一致)
次回やること:
- iOS の閲覧系を厚く(タグ別集計 / 検索・並び替え / もうすぐ更新)or 書き込み系(登録・編集・解約、カタログ選択フロー)へ
- iOS 設定画面(表示通貨切替・ログアウト)
20:40 - iOS 技術選定(SwiftUI)+ モノレポ化(Web を web/ へ移動、ios/ 新設)
やったこと:
- iOS の技術選定を SwiftUI に決定(詳細は下の「決めたこと」/ DECISIONS)
- モノレポ化を実施(コミット
fe7c5a6):web/ ios/ android/の対称構成を目指し、リポ直下にあった Next.js 一式をweb/へ移動git mvで 71 ファイルをweb/へ rename(app/components/lib/public/types/scripts + 各 config + worker.ts + wrangler.jsonc)。rename 追跡で履歴維持- ルート据え置き:
supabase/(migrations、JS 依存なし=共有)/.devnotes//.env.*/README.md/CLAUDE.md scripts/は@supabase/supabase-js(node_modules)依存のためweb/scripts/へ(Node は実行ファイル位置から node_modules を解決するため、root 据え置きだと解決不能)web/.env.*は ルートへの symlink で単一ソース維持(.env.*は gitignore 済みなのでリンクも非追跡、各環境で張り直す運用).gitignoreのパスをweb/向けに修正(/.next→/web/.next等)+ Xcode/Swift 無視設定を追加ios/README.mdに SwiftUI プロジェクトの構成方針・作成手順・Supabase Swift SDK 導入手順を記載- Cloudflare Workers Builds の Path を
/→/webに変更(ダッシュボードの Build 設定。これを忘れるとビルドが package.json を見つけられず本番デプロイが壊れる)。本番ビルド成功・subnote.me 稼働を確認
決めたこと:
- iOS は SwiftUI(ネイティブ)で実装する。Android は将来別実装になるトレードオフを受容。DECISIONS.md に転記
- リポは
web/ ios/ android/のモノレポ構成にする
詰まったこと / 気づき:
- Web を
web/に移す際、Cloudflare のビルド設定(Path)はダッシュボード側にあり、コードだけでは完結しない。移行 push の前に Path を/webに変える順番が重要 scripts/は JS 依存があるため「共有だからルート」とはいかずweb/配下が正解だった- ⚠️
.devnotes等の既存ドキュメント内に残るscripts/...やapp/...のリンクは web/ 移動でパスが古くなる(履歴記録なので未修正のまま)
次回やること:
- Xcode で
ios/配下に SwiftUI プロジェクト作成(ProductSubnote/ Orgme.subnote/ iOS 17.0+)→ Supabase Swift SDK 導入 → ログイン画面から実装
16:56 - sync 上書きガード + 更新日リマインダー方針確定(アプリ内表示)+ cancel_url 第二〜三弾
(6/1 の wrap-up 以降、6/3〜6/4 にかけての継続セッション分)
やったこと:
sync-catalog-to-prod.mjsに本番上書き防止ガードを追加(コミットf41bda9)- cancel_url の正が本番に移行したのを受け、ローカル→本番の一方向 sync を不用意に流すと本番の cancel_url 等が空上書きされる問題に対処
- 冒頭に「原則使用しない・本番が正」警告バナー +
--i-know-this-overwrites-prodフラグ無しでは即中断する実行ガード(exit 1 を確認)
- ダッシュボードに「もうすぐ更新」サマリを実装(コミット
a8deec9)- components/dashboard/dashboard-board.tsx:
next_billing_dateが今日〜3日以内のサブスクを最上部の琥珀色バナーに集約 - 過去日付・未設定・解約済みは除外。相対ラベルは既存
formatRelativeDateを再利用、各行クリックで編集画面(解約導線)へ。0件ならバナー非表示 - これが「更新日リマインダー」の実装にあたる(後述の決定どおり、通知ではなくアプリ内表示で実現)
- components/dashboard/dashboard-board.tsx:
- service_catalog.cancel_url を追加投入(第二弾7件 + 第三弾5件 = 計12件)
- 各サービスの解約/管理 URL を Web 検索で確認してから
scripts/seed-cancel-urls.mjsに追記し本番直接投入 - 第二弾(コミット
43ffb7e): unext / hulu / dアニメストア / dazn / abemaプレミアム / appletv / twitch - 第三弾(未コミット → この wrap-up で commit): kindleunlimited / lemino / audible / amazonmusicunlimited / crunchyroll
- 直リンクが無いもの(ABEMA / Amazon Music / Crunchyroll 等)は公式の解約案内ページを指す方針
- 本番の cancel_url は計 24 件(Netflix 含む、主要な動画/音楽/電子書籍をほぼ網羅)
- 各サービスの解約/管理 URL を Web 検索で確認してから
決めたこと:
- 更新日リマインダーは「通知(メール/Web Push)」を廃止し、アプリ内「もうすぐ更新」表示のみで実現する。DECISIONS.md に転記
- 「もうすぐ更新」の窓は 3日以内、過去日付は含めない
詰まったこと / 気づき:
- cancel_url の追加投入は、niコニコ / DMM TV / 楽天系 / dマガジン / FOD 等はクリーンな URL が見つからず見送り(次ラウンドで要追加調査)
next_billing_dateは単発日付のまま(billing_cycle で次回へ自動繰り越さない)。過去日付は「過ぎてます」表示のまま。周期繰り越し対応は将来課題
次回やること:
- cancel_url の残り投入(要 URL 確認のもの)
- iOS 技術選定 /
AGENTS.md・output_logo/の片付け など
2026-06-01
18:28 - monthly_snapshots cron 初発火確認 + 月次チャートのホバー + 解約中間ダイアログ + cancel_url 投入
やったこと:
- monthly_snapshots cron の初発火を確認
- cron
5 0 1 * *(= 00:05 UTC = 09:05 JST)は前月分を記録する仕様。6/1 発火なら 2026-05 分が入るはず - 本番 service role key で確認するスクリプト scripts/check-snapshots.mjs を用意したが、最終的にダッシュボードで 2026年5月 に月送りして ¥31,308 が表示される(「データなし」でない)ことで初発火成功を確認
- 5 月は過去月扱いになり、ダッシュボードは monthly_snapshots テーブルから読む。値が出る = cron が前月分を記録した証拠
- cron
- 月次推移チャートのバーをホバーで料金ツールチップ表示(コミット
b3cdc3e)- components/dashboard/monthly-chart.tsx を
"use client"化、useStateでホバー中の index を保持 - 遅延のあるネイティブ
title属性を廃止し、onMouseEnterで即時表示するスタイル付きツールチップ(料金 + 年月)に変更。ホバー中のバーは色を一段濃く
- components/dashboard/monthly-chart.tsx を
- 解約ボタンに「解約はお済みですか?」中間ダイアログを追加(コミット
88dffc3→21eb1cf)- components/subscription-form.tsx: 解約ページ URL が登録済みのサブスクは、解約ボタン押下で中間ダイアログを挟む
- 2 択「解約ページを開く ↗」「解約は済んでいる」+「やめる / 背景クリック」
- 「解約ページを開く」は新タブで開くだけでダイアログは閉じない(サービス側で手続きを済ませて戻ってから「解約は済んでいる」を押せるように)。最終確認 (window.confirm) に進むのは「解約は済んでいる」経由のみ
- URL 未登録のサブスクは従来通り即確認。
CancelSubButton(submit ベース)を廃し、useTransitionで programmatic に解約実行する方式に変更 - native confirm とカスタム modal の重なりを防ぐため
setTimeout(0)でダイアログを閉じてから confirm
- service_catalog.cancel_url を主要 11 サービスに本番直接投入
- 調査でローカル DB は cancel_url が全 291 件ゼロ(Netflix も)、本番のみ Netflix が設定済みと判明 → cancel_url の正は本番側
- scripts/seed-cancel-urls.mjs(normalized_name → URL マッピング内蔵、
--dry対応)を本番に直接実行 - 投入: spotify / youtubepremium / youtubemusic / amazonprime / applemusic / appleone / disney / adobecreativecloud / microsoft365 / googleone / dropbox(自信のある消費者向け管理 URL のみ)
- SaaS(ChatGPT / Notion / Slack 等、公開解約 URL 無し)と日本系(U-NEXT / ABEMA / Hulu JP 等、要確認)は第一弾から除外
決めたこと:
- cancel_url の書き込み先は 本番直接投入(ローカルを正にして sync ではなく)。DECISIONS.md に転記
詰まったこと / 気づき:
- ⚠️
scripts/sync-catalog-to-prod.mjs(ローカル→本番)は service_catalog を全カラム upsert するため、再実行すると本番の cancel_url が空で上書きされて消える。対策(ローカルにも cancel_url を入れる or sync を cancel_url 除外にする)は未対応 - 本番 service role key をチャットに貼ってもらって実行した。念のためローテーション推奨(必須ではない)
次回やること:
- sync 上書きリスクの対策(cancel_url を守る)
- cancel_url の追加投入(日本系サービス、要 URL 確認)
- 未コミットの
scripts/seed-cancel-urls.mjsの commit 判断
2026-05-31
20:13 - ダークモード切替 UI + タグサジェスト + カタログ default_tags 一括投入
やったこと:
- ダークモード切替 UI を実装(コミット
36fd926→8dfabe8→d52d056)- Tailwind v4 を class ベースに切替(
@custom-variant dark (&:where(.dark, .dark *)))、@media (prefers-color-scheme: dark)を.darkセレクタに変更 app/layout.tsxに FOUC 防止の inline script を入れて、描画前にlocalStorage.themeを読んでhtml.darkを当てる +suppressHydrationWarningcomponents/settings/theme-toggle.tsx: 「☀️ ライト / 🖥 システム / 🌙 ダーク」の 3 ボタントグル、デフォルトはシステム(OS の prefers-color-scheme に追従、matchMediaの change イベントもリッスンしてリアルタイム反映)- 当初「ライト/ダーク 2 択」で実装したが、相談の結果「システム連動」を真ん中の既定として 3 択に拡張、並び順も「ライト / システム / ダーク」に整理
- Tailwind v4 を class ベースに切替(
- タグサジェスト機能を実装(コミット
049c1c3→24ab901→8da865f)lib/tags.ts:extractUniqueTags/extractTagsByCategory/tagsForCategoryヘルパ追加(使用件数 + 日本語ソート)components/subscription-form.tsx: タグ入力を controlled 化、chip クリックで+ タグ名を末尾に追加。入力済みタグは候補から除外、上限 24 件- 入力に
autoComplete="off"を付与してブラウザの form 履歴サジェストを抑制 - 4 つの親ページ(
/subscriptions/newページ・モーダル、/subscriptions/[id]ページ・モーダル)のクエリにsubscriptions.tags + catalog:service_catalog(category)の join を追加し、編集対象のカテゴリで pre-filter NewSubscriptionFlowはtagsByCategoryを受け取り、選択中サービスのcategoryで動的にフィルタ- カテゴリ不明時のフォールバックを「2 つ以上のカテゴリで使われている汎用タグだけ」に絞り、無関係カテゴリのタグ混入を防止(例: YouTube Music の編集で「配送」が出ない)
service_catalog.default_tagsを 291 件分一括投入(コミット0667f4f、マイグレ20260531105030_seed_catalog_default_tags.sql)- 方針: 機能カテゴリ + 必要に応じてベンダー、2〜3 個/サービス
- 動画 24 / 音楽 12 / AI 31 / ストレージ 6 / エンタメ 12 / ゲーム 6 / 開発 31 / デザイン 16 / ツール 35 / ビジネス 33 / その他 85 = 291 件
- 本番 Supabase Dashboard SQL Editor で適用済み
- Cloudflare Error 1102(Worker exceeded resource limits) が一度トップページで観測されたが、デプロイ反映タイミングの cold start で一時的なものとして自然復旧
- カテゴリ別フィルタの実装で
extractTagsByCategoryの型が PostgREST の embedded 配列推論と合わず ビルドエラー →pickCategoryヘルパで配列/単体オブジェクト両対応
決めたこと:
- テーマ切替は 3 択(システム / ライト / ダーク)、デフォルトはシステム、保存は localStorage のみ(DB には置かない、マネタイズ対象外)(DECISIONS.md)
- タグサジェストは「自分の過去タグから chip 提示」「カテゴリで絞り込み」「カテゴリ不明時は 2+ カテゴリの汎用タグだけ」(DECISIONS.md)
- カタログ
default_tagsは「機能カテゴリ + ベンダー、2〜3 個」「用途タグ(仕事/プライベート 等)は入れない」(DECISIONS.md) - 「タグの色分け」は実装しない(ユーザーが "シンプルにしたい" と判断、ROADMAP のいつかから外す方向)
詰まったこと / 気づき:
- OS にライト/ダーク連動の概念があることがユーザー認識になく、当初 2 択で実装 → 改めて方針を相談して 3 択 + システム既定に修正。「設定 UI の OS 連動デフォルト」は普段の習慣(Notion / Slack 等の挙動)に揃えるのがやはり自然
- タグサジェストは v1 で「全カテゴリ横断」にしていて違和感が出た(Dropbox に「配送」、YouTube Music に「配送」)→ カテゴリ別フィルタ + 汎用タグ判定の 2 段に進化させた
- PostgREST の embedded リソースは TS 型推論では配列として返るので、結合先のヘルパは配列/単体オブジェクト両対応にする必要あり
次回やること:
- 月初 6/1 09:05 JST に monthly_snapshots cron が初発火するか確認
- 6/1 09:00 JST の為替レート cron も継続確認
- 次の機能候補:
- ドッグフーディング再開(カタログから自分のサブスクを実データで登録)
- 今月/今年の支出履歴ビュー(
monthly_snapshotsを使った時系列強化、6/1 後にデータ揃う) - 更新日リマインダー方針決定(メール or Web Push)
- iOS 技術選定
- カタログの
cancel_urlデータ投入(解約リンク UI は仕組み済み)
18:29 - Cloudflare cron 動作確認 + 解約まわり一連を実装
やったこと:
- Cloudflare cron 初発火を確認: 朝 09:00:53 JST に為替レート 12 通貨ペアが本番
exchange_ratesテーブルに upsert された → GH Actions から Cloudflare Cron Triggers への移行が本番で実証された。Supabase 無料プランの inactivity auto-pause も今後踏まないはず - 解約まわり一連を実装(コミット
35bca6c)— 「解約」と「削除」を別概念にして分離- マイグレ
20260531090249_add_canceled_at_to_subscriptions.sql:subscriptions.canceled_at timestamptz NULL列追加- 部分インデックス 2 種(active 用 / canceled 用)追加
record_monthly_snapshots関数を再定義(where s.canceled_at is nullをsub_monthlyCTE に追加)
- 既存の
subscriptionsSELECT にcanceled_at IS NULLフィルタを追加: app/page.tsx(ダッシュボード)、app/api/snapshots/record/route.ts - Server Action 追加:
cancelSubscription(id)(now() をセット)、restoreSubscription(id)(NULL に戻す) - 編集モーダル UI 刷新(components/subscription-form.tsx): 旧「削除」を「解約する」(赤枠ボタン、主要操作)と「完全に削除」(小さい text link、escape hatch)に分離
- カタログ起源で
service_catalog.cancel_urlがあれば「サービスの解約ページを開く ↗」リンクを編集モーダルに表示(現状 0/291 設定なので仕組み先行・データ後追い) Subscription型にcatalog_cancel_url?: string | nullを追加し、ダッシュボード / 編集ページ / モーダル の SELECT にcatalog:service_catalog(logo_url, cancel_url)join を追加- 新規
/historyページ: 解約済み一覧(解約日 desc)+ 「今月の解約 ¥X,XXX (N件)」サマリ + 復活ボタン - ダッシュボード下部に「解約履歴を見る →」リンク
- 復活ボタン用
components/history-restore-button.tsx(Client Component,restoreSubscription呼び出し)
- マイグレ
- 本番 Supabase Dashboard SQL Editor で
canceled_at追加 + 関数再定義のマイグレを適用、subnote.me で動作確認 OK
決めたこと:
- 「解約」と「削除」を別概念に分離(DECISIONS.md)
- 履歴ページのサマリ指標は「累計節約額」ではなく「今月の解約合計額」を採用(DECISIONS.md)
気づき:
record_monthly_snapshotsは SQL 関数なのでアプリ側の集計フィルタ変更だけだと不整合になる。集計ロジックを 2 箇所(TS と SQL)に持つコストとして、canceled_at追加のたびに両方更新する規律が必要service_catalog.cancel_urlの枠は前から作ってあったが、データは 0 件のまま。cancel_urlを活かす UI が今回入ったので、カタログ整備時に投入する動機ができた
次回やること:
- 6/1 09:05 JST に monthly_snapshots cron が初発火するか確認
- 次の機能候補(どれかは未決):
- ドッグフーディング(実データ投入 + UX 改善洗い出し)
- ダークモード切替 UI
- タグまわり(サジェスト + 色分け)
- 今月/今年の支出履歴ビュー
- 更新日リマインダー方針決定(メール or Web Push)
2026-05-30
16:36 - /admin 認可リファクタ + 管理ハブ新設 + Cron を Cloudflare へ移行 + monthly_snapshots 自動記録
やったこと:
/admin認可モデルを刷新(コミット11db2fc)- middleware に「
/admin/*はADMIN_EMAILS(既定ichirokisanuki@gmail.com)のみ通す」ガードを追加(dev/prod 共通、常時) - 既存のクローズドベータ全パスガード(
NODE_ENV=production用)は残置、ローンチ時にこのブロックを削除すれば自動で「/adminだけ admin 限定 + 一般は普通に使える」状態になる app/admin/layout.tsxのENABLE_ADMINフラグチェックを撤去(認可は middleware で一元管理).env.development.local/.env.exampleからENABLE_ADMINを削除、ADMIN_EMAILSを記載app/admin/page.tsxを新設(管理ハブ: カタログ管理 / ロゴ取得自動化 / 外部 Dashboard リンク)
- middleware に「
- 為替レート cron を GH Actions → Cloudflare Cron Triggers に移行(コミット
881552e)- 発端: GH Actions の支払いエラーで 5/22 以降 9 日連続失敗 → Supabase 無料プラン inactivity auto-pause を踏んで止まっていた
worker.tsを新設(OpenNext が生成する.open-next/worker.jsを fetch ハンドラとしてラップし、scheduledハンドラで internal fetch)wrangler.jsonc:mainをworker.tsに変更、triggers.cronsに0 0 * * *を追加tsconfig.jsonでworker.tsと.open-next/**を typecheck 対象から除外package.json: deploy / preview を wrangler 直接呼び出しに変更(opennextjs-cloudflare deploy/previewが custom main を尊重するか不明なため安全側)- GH
CRON_SECRET削除、ワークフローファイル.github/workflows/cron-daily.ymlも削除
- 月次スナップショット自動記録を追加(コミット
c3dc429)- 10 万人規模を想定し、ループではなく Postgres の集合演算で全ユーザー一括 upsert する方針
- マイグレ
20260530072226_add_record_monthly_snapshots_function.sql:record_monthly_snapshots(target_month text)を追加(SECURITY DEFINER + service_role のみ EXECUTE 許可)。TS のcalcTotal/calcTagTotalsの通貨換算・yearly→月割・タグ jsonb 集計を 1 つの WITH 句に移植 app/api/snapshots/record-all/route.ts: CRON_SECRET ガード付き POST endpoint。admin.rpc('record_monthly_snapshots')を叩くwrangler.jsonc: cron に5 0 1 * *追加(毎月 1 日 00:05 UTC = 09:05 JST)worker.ts:event.cronで 2 つの cron を分岐- ローカル DB で関数実行テスト OK(1 ユーザーで
total: 12613.67 / 4 件 / タグ別 jsonbを確認) - 本番 Supabase Dashboard SQL Editor で関数登録完了
詰まったこと / 気づき:
- PKCE 認証で「認証に失敗しました」: マジックリンクをリクエストしたブラウザと、メールクリックするブラウザが違うと
code_verifierクッキーが届かず失敗する。同じブラウザで完結 + メール preview で長押ししないでクリック、で解消 - Supabase 無料プラン inactivity auto-pause: 7 日 DB アクセス無しで project pause される(90 日で完全削除)。為替レート cron が止まっていたのが原因。Cloudflare Cron に移したことで二度と起きないはず
- GH Actions の課金エラー: 個人アカウントの支払い周りで他リポも影響。GH の billing 設定要確認(subnote としては Cloudflare 移行で離脱済み、subnote としての支障なし)
次回やること:
- 明日朝 09:00 JST に為替レート cron が Cloudflare から正常発火するか動作確認
- monthly_snapshots cron は 6/1 09:05 JST に初発火予定(待つだけ)
- 次の機能候補(どれかは未決):
- 解約まわり一連(削除 → 「解約」ボタン化 +
cancel_url連携 + 解約済み履歴 + 累計節約額) - ダークモード切替 UI
- タグサジェスト / タグ色分け
- 今月/今年の支出履歴ビュー
- 更新日リマインダー方針決定(メール or Web Push)
- 解約まわり一連(削除 → 「解約」ボタン化 +
2026-05-23
12:23 - ロゴ取得自動化機能の実装 + カタログ 291 件投入 + 管理画面ガード方式の変更
やったこと:
- カタログのサービス + プランを大量投入: 動画 24 / 音楽 12 / AI 31 / ツール 35 / ストレージ 6 / エンタメ 12 / ゲーム 6 / 開発 31 / デザイン 16 / ビジネス 33 / その他 85、合計 291 サービス / 324 プラン(ローカル DB)
- CATEGORIES に「デザイン」を追加(lib/service-catalog.ts)
- 管理画面ガードを
NODE_ENV === "production"からENABLE_ADMIN !== "true"フラグ方式に変更(コミット1323e2d)- 動機:
next startローカル本番モード(:3001)がNODE_ENV=productionを強制するため、admin layout の従来ガードだと :3001 でも /admin が 404 になっていた .env.development.localにENABLE_ADMIN=trueを追加、Cloudflare には設定しない → ローカルでだけ admin が開ける
- 動機:
- カタログ一覧のロゴをクリックで直接アップロードできる UI(
LogoCell)を追加(コミットea46fa9) - ロゴ取得自動化機能を新規実装(
/admin/catalog/logo-auto):- マイグレーション
20260521054505_add_logo_auto_target.sqlでservice_catalog.logo_auto_targetフラグ列を追加(boolean、default false) - 現状ロゴ未登録の 118 件にフラグを立て、一覧画面に「ロゴ取得自動化」フィルタチップ + 「▶ 1件ずつロゴを取得する」起動ボタンを追加
- 専用レビュー画面で 1 件ずつ確認 →「これで登録 / 別の候補 / スキップ / URL 貼り付け / ローカルからアップロード」
- 名前検索 = iTunes Search API(公式・無料・安定)
- Google Play は URL/ID 指定で
gplay.app()経由(google-play-scraper@10.1.2追加) - 「Play ストアで探す ↗」リンクで現在のサービスの Play 検索ページを新タブで開ける
- 登録ロゴは 128×128 統一: mzstatic / googleusercontent は URL リサイズトリック(
128x128bb.jpg/=s128)、ローカルファイルはブラウザ canvas で中央クロップ - レビュー画面のロゴ枠は角丸を外して四角表示
- マイグレーション
- パッケージ依存(
google-play-scraper→es5-ext)でビルドが落ちる症状をpnpm-workspace.yamlのallowBuilds.es5-ext: falseで解消 - 型スタブ
types/google-play-scraper.d.tsを追加 - ロゴ登録を 289/291 まで実施。残り 2 件(
note Pro/EDIST. CLOSET)はis_published=falseだったのでlogo_auto_targetフラグを下ろして自動化対象から除外 - Docker Desktop が落ちて
/admin/catalogの応答が 7 秒かかる症状を経験 → Docker 再起動 +npx supabase startで復旧(Postgres データは Docker ボリュームに永続化されていた)
決めたこと:
- 管理画面ガードを
ENABLE_ADMINフラグ方式に変更(既コミット、DECISIONS.md にも記録) - ロゴ取得自動化の取得元は iTunes(名前検索)+ Google Play(URL/ID 指定)の hybrid(DECISIONS.md)
- 登録ロゴは 128×128 統一、画像処理ライブラリは導入しない(URL リサイズトリック + canvas)(DECISIONS.md)
詰まったこと / 気づき:
google-play-scraperのsearch()は Google が Play 検索ページの構造を変えてから動作不能(テストした instagram / Netflix / メルカリ / Shopify すべて 0 件)。app()は安定。Play の名前検索を自動化する手段は現状なし- pnpm 11 は
pnpm-workspace.yamlのallowBuildsに未解決プレースホルダ(set this to true or false)があるとpnpm installが exit 1 →next build内部の deps check で連鎖失敗する。明示でfalseに設定して解消 - ローカル本番モード(:3001)で
/admin/catalogが 7 秒かかった原因は Docker 落ち。catalog 作業前に Docker Desktop の起動確認を運用ルーティンに
次回やること:
- 未コミット変更(このセッション分)を整理してコミット
- ローカル → 本番 sync スクリプト(カタログ 291 件 + Storage の 289 ロゴを本番に転送)
- monthly_snapshots の月初自動記録(GitHub Actions cron + 全ユーザーループの管理 endpoint)
- リマインダー実装方針の決定
- ドッグフーディング再開
- iOS 技術選定
2026-05-18
17:05 - サブスクカタログ機能(service_catalog / service_plans)導入、管理画面実装、カタログベースの追加 UI 改造
やったこと:
- データモデル追加(マイグレーション 2件):
service_catalog(運営マスタ、name / normalized_name / description / default_currency / default_tags / service_url / cancel_url / logo_url / popularity / is_published / category)service_plans(service_catalog への 1対多、name / amount / currency / billing_cycle / popularity / is_published)subscriptions.catalog_id追加(カタログ参照、任意。plan_idは 持たない)- Storage に
service-logosbucket(public)+ select 用 RLS service_catalog.categoryカラムを追加(カテゴリは固定リスト、コード側で管理)
- サブスク追加 UI を カタログベース に刷新(
NewSubscriptionFlow):- 検索ボックス + カテゴリチップ(動画/音楽/AI/ツール/ストレージ/エンタメ/ビジネス/ゲーム/開発/その他)+ サービスのグリッド表示
- サービスタップ → プラン選択(1プランしかない場合はスキップ) → フォーム(プリセット値入り)
- 「該当なし → 手動で入力」フォールバック、リンクは モーダルヘッダー右(タイトル横、step="browse" 時のみ)
- logo_url があれば画像、無ければイニシャル + 色付きの背景
- ロゴ表示(Phase D): ダッシュボード一覧の各行で
subscriptions.catalog_id経由でservice_catalog.logo_urlを引いて画像表示 - 管理画面
/admin/catalog(ローカル限定、process.env.NODE_ENV !== "production"なら 404):- 一覧、新規追加、編集(サービス情報 + プラン管理 + ロゴアップロード)
- ロゴ画像は Server Action で Storage にアップロード →
logo_urlを自動更新(cache-buster クエリ付き)
- SubscriptionForm を拡張:
presetValuesプロップで初期値受け取り、catalog_idを hidden input で送信、onBackで戻る動線 subscriptions.actionsのparseFormDataでcatalog_idを受け取り保存- モーダル幅を選べるように
ModalShellにwidthプロップ追加(md/lg/xl/2xl)、サブスク追加モーダルは2xlで広く - ローカル DB に Netflix / Spotify / Amazon Prime / iCloud+ / GitHub Copilot を 5 件投入してテスト
決めたこと:(詳細は DECISIONS.md 参照)
- サブスク追加 UI を カタログ検索 + プラン選択 + フォーム の 3 ステップに刷新
- カタログとプランは
service_catalog/service_plansの 2 テーブルで持つ。subscriptionsにはcatalog_idだけ参照させ、plan_idは持たない - カテゴリは コードに固定リスト、初期データはマイグレーションではなく 手動投入
- 管理画面
/adminは ローカル開発専用(本番では 404)、認証なし - 1サービス1プランの時は プラン選択画面をスキップ
- 「手動で入力」リンクは モーダルヘッダー右(タイトル横)、step="browse" 時のみ表示
- ロゴ画像は オリジナル(角丸加工なし)で保存、表示時に CSS で角丸
- 「該当なし」は 手動入力フォームに切替(クラウドソース機能は MVP では作らない)
詰まったこと / 気づき:
- Next.js 16 で Server Action を form の action に渡している場合、
encType="multipart/form-data"を明示すると警告(React が自動でやってくれる) - カタログ画像のアップロードは Server Action(Service Role)経由で Storage に上げる方が、Client から直接上げるより安全
- 「サブスク追加グリッドの最後尾に + その他」案は、件数が増えると見つけにくいので却下 → ヘッダー右のテキストリンクに変更
subscriptions.amountは契約時のスナップショット(プラン値ではなくユーザーが確定した値)として独立させる方が、後でカタログ料金が変わっても影響しない
次回やること:
- Phase B 本番運用: 30〜100件のサービス + プランをローカル管理画面で登録(ロゴ画像も)
- ローカル → 本番 sync スクリプト(
service_catalog/service_plansのデータと Storage の画像を本番に転送) - ドッグフーディング再開
- monthly_snapshots の月初自動記録、リマインダー実装、iOS 技術選定
12:15 - Web アイコン整備、ロゴ拡張、為替レート日次 cron 化
やったこと:
- Web アイコンを紫 S ロゴに差し替え(ChatGPT に依頼して取得した PNG を ImageMagick で各サイズに変換)
app/favicon.ico(マルチサイズ)/app/icon.png(512×512)/app/apple-icon.png(180×180)を Next.js App Router の規約場所に配置
- Chrome タブで白縁が出る問題 →
app/favicon.icoを削除してapp/icon.png単独に変更(PNG なら透明維持される、モダンブラウザは全て対応) - ロゴサブタイトル「サブスクをスマートに管理」を Subnote 文字の下に追加(
Logoコンポーネント拡張) - ロゴ
mdサイズを拡大(アイコン 32→40px、Subnote 文字 lg→xl) - ログインボタンのデフォルト色を
bg-brand-300→bg-brand-500に修正(薄すぎて disabled に見えていた、ホバーでbrand-400に軽くなる挙動) - 為替レート日次更新を GitHub Actions cron で自動化
CRON_SECRETを Cloudflare Worker Secret と GitHub Secrets の両方に設定.github/workflows/cron-daily.yml作成: 毎日 00:00 UTC (09:00 JST) に/api/exchange-rates/refreshを Bearer 認証付き POSTworkflow_dispatchで手動実行も可
- アイコン画像 (
app/icon.png) を.devnotes/icon.pngにバックアップ(iOS 着手時の素材流用)
決めたこと:(詳細は DECISIONS.md 参照)
- 為替レート日次更新は Cloudflare Cron Triggers ではなく GitHub Actions で運用(OpenNext との統合不要、設定がシンプル)
- ファビコンは
app/icon.png単独で運用、favicon.icoは使わない(透明維持の確実性、Next.js App Router の規約で十分) - ロゴに「サブスクをスマートに管理」のサブタイトルを常時表示(ブランドの説明)
/api/exchange-rates/refreshは CRON_SECRET があれば必須にして、cron 専用化(ユーザーがブラウザから叩く運用は廃止)
詰まったこと / 気づき:
magickの-define icon:auto-resize=での.ico出力は、透過が崩れる場合がある。app/icon.png(PNG)に統一する方が確実- Chrome のファビコンキャッシュはしつこい。確認時はハードリロード or シークレットウィンドウが必要
exchange_ratesは全ユーザー共有のグローバルテーブル設計のため、誰か1人が API を叩けば全員に反映される(為替レート自体は個人ごとに違うものではないので正しい設計)- 為替レートが取得されていない時、
convertCurrencyは元の金額をそのまま返す挙動。$3 が ¥3 として加算される事象に注意(cron 化で解消)
次回やること:
- 1週間のドッグフーディング(実データで自分で使い込む、気付きは WIP.md に殴り書き)
- 改善点を反映
- iOS アプリの技術選定と着手判断(SwiftUI / RN / Expo / Flutter 等を比較)
- 必要なら独自 SMTP 切替(個人利用なら当面 Supabase 共有で問題なし)
2026-05-17
18:19 - GitHub 自動デプロイ運用化、独自ドメイン subnote.me 稼働、各種 UX 改善
やったこと:
- Cloudflare Workers Builds(GitHub 連携)を設定 →
git pushで自動 build/deploy 運用化- Build command:
pnpm install --frozen-lockfile=false && pnpm exec opennextjs-cloudflare build - Deploy command:
pnpm exec wrangler deploy - Build cache 有効化(2回目以降のビルドが大幅短縮)
- Build command:
pnpm-workspace.yamlにpackages: [.]を追加(Cloudflare Builds の初期化フェーズがpnpm install --frozen-lockfileで必要としていた)- Service Role Key を最後の rotate + Cloudflare Dashboard で Secret として再登録(Plaintext 漏洩を解消)
- Vercel プロジェクト削除
- subnote.me を Cloudflare Registrar で取得(年額 $13〜18、コスト価格・Cloudflare Zone 自動構成)
- Cloudflare Workers Custom Domain として
subnote.meを設定(SSL 自動発行) - Cloudflare の Build variables + Runtime variables の
NEXT_PUBLIC_SITE_URLをhttps://subnote.meに更新 - Supabase の Site URL を
https://subnote.me、Redirect URLs から旧workers.dev/vercel.appを削除 - ローカルの
.env.production.localもsubnote.meに更新 - UX 改善(複数):
- ログインの送信ボタンを
useFormStatusで連打防止 - ダッシュボードヘッダーのメアドを pill 化、クリックで表示/非表示(localStorage 永続化)
- ダッシュボードヘッダーからログアウトボタンを削除(設定画面に集約、重複解消)
- 月切替 UI をサマリーカード内右上に集約、「今月へ」を pill 化
- サブスク一覧の行を縦中央揃え + 「¥金額 / 月」を1行に
- サブスクフォームの保存・削除ボタンを
useFormStatusで連打防止 - 新規登録モーダルでサービス名に
autoFocus - サブスク保存後にモーダルが閉じない問題を修正: Server Action の
redirect("/")を削除し、Client 側でrouter.back()(modal)/router.push("/")(page)+router.refresh()
- ログインの送信ボタンを
決めたこと:(詳細は DECISIONS.md 参照)
- 独自ドメインは subnote.ikapps.com を経由せず、最初から subnote.me 直接(移行コスト回避)
- デプロイは Cloudflare Workers Builds (GitHub 連携) で自動化、ローカル
pnpm run deployは緊急時のみ - サブスク CRUD 後の画面遷移は Server Action の redirect を使わず、Client 側で navigate(Cloudflare 互換性)
- ログアウトは 設定画面のみに置く(ヘッダーから削除、重複・紛らわしさ解消)
詰まったこと / 気づき:
- Cloudflare Build variables(ビルド時に埋め込み)と Runtime variables(process.env 経由)は別管理。
NEXT_PUBLIC_*は前者、SUPABASE_SERVICE_ROLE_KEYは後者 - Supabase の Site URL に古い URL が残っていると、
emailRedirectToを新 URL で送っても Supabase 側で許可リストにないので Site URL(古い)にフォールバックされる - pnpm v10+ で
pnpm-workspace.yamlが存在するとpackagesフィールド必須(無いとpnpm installがエラー) - Cloudflare Workers Builds の初回設定では「Build variables」セクションへの環境変数追加を忘れがち(NEXT_PUBLIC_* 必須)
wrangler.jsoncのkeep_vars: true+ GitHub Builds 経由 deploy で、Dashboard の Secret が誤って上書きされる事故を回避
次回やること:
- ドッグフーディング(実データ投入)
monthly_snapshotsの月初自動記録(GitHub Actions / Cloudflare Cron Triggers / Supabase pg_cron いずれか)- 為替レート更新の cron 設定
- リマインダー実装方針の決定(メール / Web Push / アプリ内)
- 必要に応じて UI 微調整
14:45 - Vercel から Cloudflare Workers (OpenNext) へ移行、本番動作確認まで
やったこと:
- 1〜3 コミット分の作業(連打防止・メアドトグル・ヘッダー整理)を本番反映
LoginSubmitButton(useFormStatusで pending 中 disable)HeaderEmail(localStorage で表示/非表示記憶、Eye / EyeOff アイコン)- ダッシュボードヘッダーからログアウトボタンを削除(設定画面側に集約)
- Vercel に初回デプロイ → 動作確認できたが、Hobby プランの「商用利用不可」規約に気付き、Cloudflare Pages 案を検討
- 比較検討の結果、Cloudflare Workers (OpenNext for Cloudflare) に移行決定
- 移行作業:
@opennextjs/cloudflare+wranglerを dev deps に追加wrangler.jsonc/open-next.config.ts作成next.config.tsにinitOpenNextCloudflareForDev()追加package.jsonにpreview/deploy/cf-typegenスクリプト追加.gitignoreに.open-next/.wrangler/cloudflare-env.d.ts追加
- Next.js 16 の
proxy.tsが Cloudflare Workers で動かない問題に遭遇(Node.js runtime 固定で Edge 不可)→middleware.tsに戻してruntime = "experimental-edge"で解決 wrangler login+pnpm run deployで初回デプロイ成功(https://subnote.ichirokisanuki.workers.dev)- 環境変数の闇に何度も詰まる:
- 当初ローカルの
.env.local(http://127.0.0.1:54321)がビルドに埋め込まれ「error code: 1003」(Cloudflare Direct IP Access Not Allowed) .env.production.localを作って本番 URL を入れた- さらに
.env.localも build 時に読まれて Service Role Key がvarsとして deploy されてしまう問題 →.env.localを.env.development.localにリネームして dev のみで読まれるように - deploy のたびに Cloudflare Dashboard の Secret が消える問題 →
wrangler.jsoncにkeep_vars: trueを追加
- 当初ローカルの
- 本番でマジックリンクログイン → ダッシュボードまで動作確認 OK
決めたこと:(詳細は DECISIONS.md 参照)
- デプロイ先を Vercel → Cloudflare Workers (OpenNext for Cloudflare) に移行(商用 OK + 無料枠が広い)
- Next.js 16 の middleware は
middleware.ts+runtime = "experimental-edge"で運用(Cloudflare 互換) - ローカル開発用 env file は
.env.development.localを使う(.env.localは build 時にも読まれて事故るため) wrangler.jsoncにkeep_vars: trueを入れて、deploy 時に Dashboard の secret/vars を保持
詰まったこと / 気づき:
- Vercel Hobby は非商用専用、商用は Pro $20/月で個人ツールにはきつい
- OpenNext for Cloudflare は Next.js 16 の
proxy.tsを未対応、middleware.ts+experimental-edgeで運用が現実解 wrangler.jsoncのkeep_vars: trueが無いと、ローカル deploy のたびに Cloudflare の Secret が全部消える(やられた)- wrangler の WARNING が Cloudflare Dashboard の Secret 値を露出する仕様で、3回 Service Role Key を rotate するハメに(最後にもう一度 rotate 必要)
- GitHub 連携 + Cloudflare Workers Builds に切り替えれば、ローカル deploy 起因の事故は無くなる
次回やること:
- Service Role Key をもう一度 rotate(漏洩したため)
- GitHub 連携 + Cloudflare Workers Builds で自動デプロイ化(ローカル deploy を廃止)
- Vercel プロジェクト削除
- Supabase の Redirect URLs から vercel.app を削除
11:59 - 月切替・前月比・DashboardBoard 刷新・Claude Design 寄せのビジュアル磨き
やったこと:
monthly_snapshotsテーブル + マイグレーション(year_monthPK、tag_totalsjsonb、RLS は select のみ自分の行、書き込みは Service Role 経由)/api/snapshots/recordRoute Handler(ログインユーザーの当月 or 任意月のスナップショットを upsert、開発用に GET でも叩ける)- 月切替対応:
app/page.tsxで?month=YYYY-MMを受け、今月(live)と過去月(snapshot)で集計の取得元を切り替え - 前月比 pill(前月 snapshot との差を計算、↑赤 / ↓緑 / → グレー)
- 月切替 UI(← 前月 / 今月バッジ / 今月に戻る / 翌月 →、未来は disable)
- 過去12ヶ月の棒グラフ(
MonthlyChart、CSS divs で自作、今月のバーは紫、過去月は灰、データなしは薄バー) DashboardBoardClient Component に集約: タグフィルタ(クリック可・「すべて」含む)/ 検索 / ソート(金額高い・安い・次回課金近い・名前順)/ 件数表示- サブスク一覧: イニシャルアイコン(サービス名のハッシュで色決定)+ 相対日付(今日 / 明日 / N日後 / 来週 / M/D 課金)
- Claude Design 寄せのビジュアル磨き:
globals.cssの@themeに brand-50..900(indigo 系)を追加Logo共通コンポーネント(紫角丸 + 白 S + Subnote)- lucide-react 導入(Settings / LogOut / X / ArrowLeft アイコン)
- ログイン画面: ロゴ中央、白カード(角丸大)+ 薄シャドウ、紫薄ボタン、補助テキスト
- ダッシュボードヘッダー: Logo + メアド pill + 設定/ログアウトをアイコンボタンに
- サマリーカード: フォント大(4xl/5xl)+ 前月比 pill + 年額換算/件数 + 棒グラフ
- タグチップ: 白枠 + 選択中は薄紫
- サブスク一覧: rounded-2xl + shadow-sm
- モーダル: bg-black/40 + backdrop-blur + 角丸大 + X アイコン
- フォーム: 課金サイクルをセグメントコントロール(peer + sr-only ラジオ)+ 紫保存ボタン + brand-500 focus ring
- 設定画面: ロゴ統一 + 戻るリンクにアイコン + メアド pill + ログアウトを赤縁ボタン
決めたこと:(詳細は DECISIONS.md 参照)
- 前月比は
monthly_snapshotsテーブル方式(A 方式)+ 月切替は表示中の月を切り替えるシンプル UI (a) - プラン名(例: 「コンプリートプラン」)は memo で代用(専用フィールドは追加しない)
- ブランドカラーは Tailwind indigo 相当を
--color-brand-Nとして@themeで定義、bg-brand-500などで参照 - サブスクのイニシャルアイコン色はサービス名のハッシュベースで自動決定(ブランドロゴ色のハードコードはしない)
- 棒グラフは外部ライブラリを使わず CSS divs で自作(Recharts / Chart.js を入れない)
- アイコンライブラリは lucide-react を採用
詰まったこと / 気づき:
- Claude Design の HTML(3.9MB)は CSS インライン化されていて直接読めない → テキストノード抽出 + ユーザーのスクショ共有で情報収集
- Tailwind v4 のカスタムカラーは
@themeブロックに--color-brand-Xで定義する形式 - React の
defaultValueは initial mount 時のみ評価される → 設定画面のセレクトにkey={currentCurrency}を付けて再マウントを強制(既存対応の再確認)
次回やること:
- 自分で実データを入れてドッグフーディング
- 月初スナップショット記録の自動化(cron / Vercel Cron / pg_cron いずれか)
- 本番デプロイ(Vercel + Supabase 本番プロジェクト)
- 必要に応じて UI のさらなる磨き
09:58 - Web MVP のコア機能を一通り実装(基盤〜CRUD〜設定〜為替)
やったこと:
- Next.js プロジェクト初期化(App Router + TypeScript + Tailwind v4 + Turbopack、一時ディレクトリ作成 → subnote/ にマージで既存ファイル保護)
- pnpm / supabase CLI を brew でインストール
- Supabase 依存追加(
@supabase/ssr,@supabase/supabase-js)+ Service Role 用 admin クライアント supabase init+ ローカル Supabase(Docker)起動、supabase/config.tomlのsite_urlをhttp://localhost:3000に修正、additional_redirect_urlsを http で全パス許可に修正- SQL マイグレーション作成:
subscriptions/exchange_rates/user_settings+ RLS + 新規ユーザー登録時にuser_settings行を自動生成するトリガー - ログイン画面(
/login、マジックリンク送信フォーム)+ 認証コールバック route handler + ホームの認証ガード - ダッシュボード: 月額合計(表示通貨換算)/ タグ別集計 / 検索ボックス(クライアントサイドフィルタ)/ サブスク一覧
- サブスク登録モーダル + フルページ(Next.js intercepted route)、SubscriptionForm は新規/編集を共通化(
updateSubscription.bind(null, id)) - 編集 + 削除(削除は
formAction経由で呼ぶことで Server Action の navigate が正常に動くよう修正) - 「+ 詳細を追加 / − 詳細を閉じる」で次回課金日・メモを折りたためる UX
- 設定画面(
/settings): 表示通貨切替(即時保存 + 「保存しました ✓」表示)、ログアウト、為替レート最終更新時刻 - 為替レート取得バッチ:
/api/exchange-rates/refreshRoute Handler、Frankfurter API から JPY/USD/EUR/GBP の相互レートを upsert - 設定画面の「今すぐ更新」ボタンは廃止(運用は cron 任せ、開発時は curl で叩ける)
- Next.js 16 の deprecation 対応:
middleware.ts→proxy.ts、関数名もmiddleware→proxy
決めたこと:(詳細は DECISIONS.md 参照)
- 設定の保存は「即時保存方式」(保存ボタン廃止)
- 為替レート「今すぐ更新」ボタンは設定画面には置かない(運用は cron)
- 削除ボタンは
formAction経由で呼ぶ(手動await action()ではなく)
詰まったこと / 気づき:
- マジックリンクが
otp_expiredで/login#error=...に戻る →additional_redirect_urlsが https のみ、site_url が 127.0.0.1 と localhost で不一致だった。http://localhost:3000/**,http://127.0.0.1:3000/**を追加して解決 - 削除ボタンを
<button onClick={async () => await deleteAction()}>で呼んでいたら 404 になった →formAction={deleteAction}経由にして解決(Server Action の redirect が正常に動く) - 設定の保存 UI で「何も変化しない」UX 課題 → 即時保存 + 「保存しました ✓」表示に切替で解消
- pnpm 10/11 の
allowBuilds設定が必要(sharp, unrs-resolver の postinstall)
次回やること:
- 一度自分で実データを入れてドッグフーディングしてみる
- 使ってみての改善点を洗い出す(ROADMAP 今四半期セクションに)
- 必要なら shadcn/ui 導入で UI ブラッシュアップ
2026-05-16
20:23 - プロジェクト初期設計(MVP の骨格固め)
やったこと:
- プロジェクト概要を整理: サブスク料金管理ツール、Web / iOS / Android の総称
- MVP のコア機能を決定(サービス名 + 月額/年額料金 + 複数タグ、月額合計、タグ別集計、更新日リマインダー)
- 技術スタックを比較検討の上で決定
- データモデルを設計(subscriptions / exchange_rates / user_settings)
- Web 画面構成を設計(ダッシュボード、モーダル中心の登録 UI、検索ボックス)
- CLAUDE.md にプロジェクト概要・データモデル・画面構成を追記
決めたこと:(詳細は DECISIONS.md 参照)
- 技術スタック: Next.js + Supabase
- 認証: マジックリンク / OTP(パスワードレス)
- タグは text[] 配列で持つ(正規化テーブル使わない)
- 通貨は多通貨対応、為替レートは外部 API で自動換算
- 月額合計は年額÷12 で月額換算して合算
- next_billing_date は任意項目
- 登録/編集 UI は intercepted route でモーダル中心
- タグの色分けは MVP では行わない(全部グレー)
- iOS/Android 技術選定、リマインダー実装方法、マネタイズは後回し
次回やること:
- Next.js プロジェクト初期化(App Router、Tailwind)
- Supabase プロジェクト作成と環境変数設定
- データモデルの SQL マイグレーション作成
最近のコミット
- a73ada0 .devnotes 更新: Codex実装コミット・RevenueCat Play連携完了(RTDN疎通)・iOS配信地域未設定事故の検出と修正を記録 2026/7/23
- 6a4e880 Android: RevenueCat の Play Store 公開キーを設定(プレースホルダ差し替え) 2026/7/23
- d90c3cf Android を iOS 1.0.0 パリティに拡充(Codex実装レビュー済み: RevenueCat課金/アカウント削除/SiwA/Firebase計測/UX磨き込み・バグ修正)+ versionName 1.0.0 2026/7/23
- 6550bad .devnotes 更新: Android パリティ始動(Codex 依頼書→実装完了・レビュー待ち)+ Play Console 土台一式・15%手数料登録を記録 2026/7/23
- 3b647aa .devnotes 更新: iOS 1.0.0 審査通過→公開ボタン押下(正式リリース)・Crashlytics疎通確認を記録 2026/7/23
- a6fa43f .devnotes 更新: iOS 1.0.0 審査2回目リジェクト(2.3.10 Android言及)対応・再提出を記録 2026/7/22
- e279606 .devnotes ROADMAP: iOS アプリ内レビュー導線(審査通過後の予定)を追加 2026/7/22
- 42b6239 .devnotes 更新: iOS 1.0.0 審査リジェクト(3.1.2)対応・規約リンク追記して再提出 2026/7/22
- 412513c .devnotes 更新: iOS 1.0.0 審査提出・Web正式公開までを記録 2026/7/20
- 38bd006 Crashlyticsを導入しビルド3として構成 2026/7/20
README
subnote
概要
(記入予定)
セットアップ
(記入予定)
使い方
(記入予定)