subnote
WIP(現在進行中)
Work In Progress
このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。
現在の状況
フェーズ: 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 残り投入
継続メモ(プラットフォーム別の詰まりどころは下記アーカイブ参照):
- 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 不要。その他プラットフォーム別の詳細は下記アーカイブ
過去のWIPアーカイブ
(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)
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 プラットフォームに集約)
今四半期
- 本番デプロイ(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 ナビバーに並び替え/歯車を残す。高級感狙い) - 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 ユーザー指定) シンプル優先で当面実装しない
- 今月/今年の支出履歴ビュー
- サービス名サジェスト(公式アイコン・標準料金プランの補完、※カタログ機能で代替済み)
- マネタイズ方針の検討
DECISIONS(意思決定)
意思決定記録
このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。
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-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 マイグレーション作成
最近のコミット
- 79339b1 項目6 の丸め修正 migration を本番適用、対応状況を更新 2026/6/24
- d16d11f Codex findings 項目8 対応: admin の読み取りを service role 化 2026/6/23
- d7b6090 Codex findings 項目6 対応: snapshot 集計を合計後丸めに統一 2026/6/23
- e949ac7 Codex findings 保留B を対応(cron/Turnstile を fail-closed 化) 2026/6/23
- 8e80321 Codex findings 項目4・7 の migration を本番適用、対応状況を更新 2026/6/23
- 48de9ba Codex レビュー指摘の安全バッチA を対応 2026/6/23
- 13cd99c .devnotes を更新(Supabase auto-pause 復旧 + keep-alive 切り離し + cron 失敗メール通知を記録) 2026/6/23
- 0070361 一時テストエンドポイント alert-selftest を削除(疎通確認完了) 2026/6/23
- 9a96c34 一時テストエンドポイントを _alert-test→alert-selftest に改名(_ 始まりは Next.js の private folder で 404 になるため) 2026/6/23
- 3647c0c 一時: cron 失敗通知メールの疎通確認用エンドポイントを追加(確認後に削除) 2026/6/23
README
subnote
概要
(記入予定)
セットアップ
(記入予定)
使い方
(記入予定)