review-antenna
WIP(現在進行中)
Work In Progress
このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。
現在の状況
RBAC(複数ユーザー共同管理)を本番反映済み(2026-06-26、migration 0010/0011 適用)。 公開マルチテナント版が本番稼働中。Node.js (Hono) + node:sqlite + systemd。
- 本番: https://reviewantenna.ikapps.com/ (Google OAuth ログイン必須、Nginx + Let's Encrypt)
- RBAC: 1アカウント(テナント)を複数ユーザーで権限分けして共同管理
- ロール: オーナー / 編集者 / カスタマー対応(閲覧+返信) / 閲覧者(閲覧+通知受信)。
membershipsで (テナント↔ユーザー↔ロール) - メール招待(
/members、オーナーのみ)→受諾(メール一致必須+verified)→参加。アンカーowner固定(v1は所有者移譲なし) - ヘッダのテナント切替セレクタ(
active_owner_idをセッション保持)。ロール別にUI出し分け(編集/返信/メンバー管理) - 通知はテナントの購読メンバー全員へ配信(
review_deliveriesで(レビュー×メンバー)単位、channel無し除外、参加前は cutoff で除外) - 大方針: 既存
user_idを「テナントID」として温存・再解釈(apps/reviews/credentials 無改造)。 channels だけは本人データ(テナント再解釈の例外)。高権限POSTは CSRFトークン+form.owner_id で作用先確定(stale form対策)
- ロール: オーナー / 編集者 / カスタマー対応(閲覧+返信) / 閲覧者(閲覧+通知受信)。
- 既存機能(不変): 複数認証情報+まとめて登録(0009)、
/app詳細・返信、/export、GET /reviews(READ_TOKEN・運営用横断)、 毎時 systemd timer で全テナント poll→sqlite→SESメール通知。apps.credential_idで取得鍵を明示 - secret/設定は
/home/ubuntu/apps/review-antenna/.dev.vars(APP_ENC_KEY 固定)
残・次にやること:
- 本番で You が実ブラウザ(Google OAuth)でログインし、招待・テナント切替・ロール別UIの最終確認
- (任意)Google まとめて登録の実使用時は各SAで Play Developer Reporting API 有効化+権限付与
- (任意)Resend/SES 独自ドメイン認証、実運用ドメイン reviewantenna.com 引っ越し
- (任意・RBAC将来)所有者移譲、通知メールへのテナント名表示、channels の
active=0で「無効化」と「削除」を区別
注意: システム時計が実時刻とズレている(DEVLOGの時刻はユーザー確認した実時刻を使う)。
重要: 本番 APP_ENC_KEY は固定。変えると暗号化済み認証情報が復号不能になる(同一サーバー/.dev.varsを維持)。
メモ: RBAC適用前の本番DBバックアップは ~ubuntu/ra-backup-20260626-182147.db。サーバーに sqlite3 CLI は無く、migration は node:sqlite(--experimental-sqlite)で適用。migrationは「未適用を一度だけ」流す運用(0010の ALTER TABLE は再適用で失敗、適用前に PRAGMA table_info で確認)。
詳細は本番運用情報含めプロジェクトルートの 引き継ぎ.md に集約。
過去のWIPアーカイブ
(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)
2026-06-26 19:35 アーカイブ(RBAC前・0009反映状態)
公開マルチテナント版が本番稼働中。複数認証情報対応+まとめてアプリ登録を本番反映済み(2026-06-25、migration 0009)。
- 本番 reviewantenna.ikapps.com、Google OAuth 必須・テナント分離
/settingsで認証情報をニックネーム付きで複数登録、/apps/importでまとめて登録apps.credential_idで各アプリの取得鍵を明示、poll/返信も credential_id 単位で鍵解決- 残: Play Reporting API、Resend/SES独自ドメイン、reviewantenna.com 引っ越し、オンボーディング導線
2026-06-25 20:00 時点のスナップショット
公開マルチテナント版が本番稼働中(2026-06-08 cutover完了)。複数認証情報対応+まとめて登録を 実装・本番反映する直前の状態。
- 本番: https://reviewantenna.ikapps.com/ (Google OAuth 必須、テナント分離)
/settingsで per-user 認証情報(ASC/Google SA)を 1ストア1つだけ登録→AES-256-GCM保存- 毎時 systemd timer で全テナントをポーリング→sqlite→メール通知(SES)
- 運営
meは既存アプリ・レビューを保持。env フォールバックは撤去済み(DBの credentials を使用) - 残: Resend/SES独自ドメイン認証、reviewantenna.com 引っ越し、オンボーディング導線、通知チャネル追加
2026-06-03 16:30 時点のスナップショット
本番稼働 + 管理ダッシュボード(一覧/詳細)+ レビュー取得API + ★評価総数表示。 監視3アプリ(Vision Workout / Study Timer / 勉強タイマー)。 アプリ登録UI・アイコン表示・JSONダウンロードはこの後(18:12 セッション)で実装。 次: Slack/ntfy通知、レビュー集計、Resend独自ドメイン認証。
2026-06-02 時点のスナップショット
MVP完成・本番稼働中(App Store / Google Play 両対応)。
- URL: https://review-antenna.ichirokisanuki.workers.dev
- 毎時0分の Cron で両ストアの新着レビューを取得 → 新着のみD1保存 → メール通知
- 監視中: App Store「Vision Workout」(1563884276) / Google Play「勉強タイマー」(jp.thomsons.pomodoro)
- 通知先: ichirokisanuki@gmail.com(Resend、送信元 onboarding@resend.dev)
- secret登録済み: ASC3点 / Resend2点 / RUN_TOKEN / GOOGLE_SERVICE_ACCOUNT
- 次: Resend独自ドメイン認証、チャネル追加、公開サービス化
ROADMAP(計画)
ロードマップ
完了
- 技術スタック確定(Cloudflare Workers + D1 + Cron、Resend)
- App Store レビュー監視(ASC API、JWT/ES256)
- 本番デプロイ・Cron稼働
- メール通知(Resend)
- 初回 seed 処理・冪等性・SQL変数上限対策
- アプリ名自動取得・メール表示整形
- /run のトークン認証
- Google Play 対応(サービスアカウント認証)
- Google Play のアプリ名自動取得(Play Store og:title)
- レビュー取得API GET /reviews(READ_TOKEN認証)
- App Storeレビューのページング対応
- 管理ダッシュボード(アプリ一覧・レビュー詳細、無認証)
- ★評価総数の取得・表示(App Store / Google Play)
- 画面からアプリ登録・削除(登録時に名前/評価/アイコン同期取得)
- アプリアイコンの自動取得・一覧表示
- 全レビューのJSONダウンロード(/export)
- subrequest上限対策(seedを1回最大3アプリに分割)
- レビューへの手動返信(App Store / Google Play)
- 複数認証情報対応(同一ストアに複数鍵・ニックネーム)+まとめてアプリ登録(migration 0009、2026-06-25)
今週
- 一覧のフィルタ/並べ替え(ストア別・評価の低い順・★1だけ等)
- Resend 独自ドメイン認証(任意宛先に送信可能に)
- (任意)まとめて登録の Google 対応を実使用するなら、本番の各サービスアカウントで Play Developer Reporting API を有効化+権限付与
- (任意)アイコン割れ時のプレースホルダフォールバック、認証情報の label リネームUI
今月
- 通知チャネル追加(Slack / ntfy / LINE のいずれか)
- レビュー集計・分析(評価推移など)
今四半期 — 公開サービス化(単一コードのマルチテナント化)
- Phase 1: 基盤 —
users/sessions、Google OAuth ログイン、ログイン必須化・テナント分離(2026-06-08) - Phase 2: per-user 認証情報 —
credentials(AES-256-GCM)、アップロードUI+接続テスト、poll/asc/gp テナント鍵対応(2026-06-08) - Phase 3 cutover: 本番を多人数版へ切替(reviewantenna.ikapps.com、ログイン必須)(2026-06-08)
- RBAC(複数ユーザー共同管理)— オーナー/編集者/カスタマー対応/閲覧者の4ロール、メール招待+メンバー管理、 テナント切替、通知のメンバー配信。migration 0010/0011。Codex 3ラウンドレビューで設計収束(2026-06-26)
- RBAC将来拡張: 所有者移譲、通知メールへのテナント名表示、channels の active=0 で「無効化」と「削除」を区別
- Phase 3 残: Resend 独自ドメイン認証(他ユーザー通知用。reviewantenna.com 移行時に同時実施推奨)
- 実運用ドメイン reviewantenna.com への引っ越し(設定のみ。コード/データ変更なし)
- 新規ユーザーのオンボーディング導線(サインアップ後の通知チャネル自動作成など)
- (実ユーザー増時)$10/1GB 増強、tsx→ビルド済みJS で軽量化
いつか
- Amazon レビュー対応
- レビュー返信機能(ASC / Play の返信API)→ 手動返信を実装済み(AI返信ドラフトは将来)
- レビュー分析(評価推移、感情分析など)
DECISIONS(意思決定)
意思決定記録
このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。
2026-06-26: RBAC(複数ユーザー共同管理)の主要設計判断
背景: 1アカウントを複数ユーザーで権限分けして共同管理したい。既存は「1ユーザー=1テナント」で
全テーブルの user_id が所有者とテナントを兼ねている。共同管理には「テナント」と「メンバー/ロール」を
分離する必要があり、データモデル・認可・招待・通知の各層で方針を決める必要があった。設計は3エージェント
並列調査でたたき台→Codex 3ラウンドレビュー(R1 high3→R3 収束)で固めた(.devnotes/rbac-handoff.md)。
決定: 以下を確定し Phase 1〜4 で実装・本番反映した。
- データモデル: 既存
user_idを「テナントID」として温存・再解釈する(apps/reviews/credentials は スキーマ無改造)。新テーブルmemberships(owner本人も role='owner' で1行)とinvitationsを追加。 - channels は例外(本人データ): テナント再解釈の対象外とし
channels.user_idはログインユーザー本人。 通知先は各メンバーが自分で管理し、テナント削除でも消さない。 - テナントコンテキスト:
sessions.active_owner_idで保持+ヘッダのセレクタで切替。状態変更POSTは フォームのowner_idで作用先テナントを明示解決して認可(セッションのactiveだけに依存しない)。 - 招待の本人性: メール招待リンク方式。受諾は「招待メール == ログインメール」一致必須+Google
email_verified。トークン保持者なら誰でも可、は採らない。 - 通知の既読: 単一
reviews.notifiedではなくreview_deliveriesで (レビュー×メンバー) 単位に記録。 channel無しメンバーは配信対象外(分母に入れない)、notified は「対象全員配信済み or 対象0」で1。 - アンカーowner固定: テナントID本人の owner membership は削除・降格不可。v1は所有者移譲なし。
- CSRF: 高権限POSTにセッション単位のCSRFトークンを導入(
sessions.csrf_token)。
理由: user_id 温存は本コードベースの設計思想に最も素直で移行リスク最小(全 WHERE user_id=? の
渡し値を差し替えるだけ)。channels本人データ化はプライバシー(他人の宛先を抱えない)と「各自が自分の通知を
管理」という直感に合致。form.owner_id 認可は複数テナント所属時の stale form 誤爆を防ぐ(Codex R2 high指摘)。
メール一致必須はトークン漏洩・転送時の被害を限定(本番テナントの読み書き権限を渡す行為のため)。
review_deliveries 方式は複数受信者での一部失敗時の取りこぼし/二重送信を防ぐ。アンカー固定は ownerEmail 表示の
ズレ・「自分のテナントから抜ける」破綻を回避しv1スコープを最小化(移譲は将来 tenants テーブルで拡張可)。
2026-06-25: 複数認証情報のモデルと「まとめて登録」のスコープ
背景: 「1認証情報=そのキーで見えるアプリだけ監視できる」制約があり、別アカウント配下のアプリを 監視するには複数の認証情報が要る。同時に、ASC/サービスアカウントは「アクセス可能な全アプリ」を 列挙できるため、手入力ではなく一覧からまとめて登録したい。複数鍵にすると「各アプリをどの鍵で 取得するか」を明示する必要が生じる。
決定:
credentialsのUNIQUE(user_id,platform)を撤廃し、ニックネームlabelで複数登録可能に。appsにcredential_idを追加し、各アプリの取得鍵を登録時点で確定させる。- まとめて登録は App Store / Google Play 両対応。App Store は ASC
GET /v1/apps、 Google は別API(Play Developer Reporting APIapps:search、別スコープ)を使い、 失敗時は手動入力にフォールバック。 - 複数鍵の見分けはユーザー入力のニックネーム(自動ラベルは不採用)。
理由: 「鍵→アプリ一覧→登録」という自然な流れの中で credential_id が確定するため、
別案(poll時に複数鍵を総当たり)より無駄なAPI呼び出し・401/404が無くクリーン。既存アプリは
移行SQLで現行の単一鍵へ自動紐付くため破壊性が低い。Google は Reporting API の有効化・権限付与が
前提で全環境で成功するとは限らないため、ベストエフォート+手動フォールバックに留めた。
ニックネームは Issuer ID/client_email の自動表示より識別しやすい。
2026-06-12: サービス名は Review Antenna で確定(ReviewReply 案は不採用)
背景: マネタイズの軸として「レビューへの返信(+AI返信ドラフト)」機能を検討開始。返信が売りなら 受信メタファーの「Antenna」は名前と価値提案がズレるのではという疑問が出て、ReviewReply 案を検討した。
決定: サービス名は Review Antenna のまま確定。reviewantenna.com は空きを確認済み(購入は返信機能の手応えを見てから)。
理由: reviewreply は .com/.io/.ai すべて取得済み(.ai は2024年取得=AI返信系の競合が既にひしめく激戦ワード)。 一般名詞2語で指名検索に埋もれ、かつ「返信ツール」に名前が縛られて監視・通知・AI要約への拡張と再びズレる。 入口機能(見逃さない)を名乗り、本命機能(返信)はコピーで売る構造の方が素直。
2026-06-08: 公開サービス化=単一コードのマルチテナント化(2インスタンス並走はしない)
背景: 個人専用サービスを一般ユーザーも使える公開サービスへ拡張したい。当初「個人版=me./公開版=apex」で
サブドメイン分岐し2インスタンス並走する案も検討したが、Lightsail は 512MB(空き約140MB)で、Node常駐を
2本立てると基礎使用量(RSS、アクセス数とは無関係)で逼迫する懸念があった。
決定:
- 単一コードベースをマルチテナント化する(2インスタンス並走はしない)。個人利用は「テナント me」として
同一アプリ内で継続。データモデルは既に全テーブル
user_id完備のため素直に伸ばせる。 - 既存データの
user_id='me'はそのまま運営ユーザー(users.id='me')に対応づけ、移行コストを最小化。 - サブドメイン分岐は機能的に不要化(全員 apex にログイン)。
me.は残すなら見た目・入口だけ。 - 開発は
publicブランチ + ローカルで行い、安定後にサーバーの1本を置き換える。サーバー上で2本同時に 動くのは切替の瞬間だけ → メモリ問題を回避。 - 認証は Google OAuth。ユーザー認証情報(ASC
.p8+KeyID+IssuerID / Google SA JSON)は Webアップロード→AES-256-GCMで暗号化しDB保存(運営マスター鍵は env)。Resend は運営1本で代理送信 (独自ドメイン認証が前提)。 - 実ユーザーが付いたら Lightsail を $10/1GB へ増強、
tsx→ビルド済みJSで軽量化。
理由: データモデルが最初からマルチテナント前提だったため、別アプリ化より単一コード拡張が自然で 重複保守がない。ローカル開発+ブランチなら個人運用を一切壊さずに公開版を育てられ、メモリ制約も回避できる。 この決定は 2026-06-03「管理ダッシュボードは無認証」を公開版では覆す(公開版はログイン必須・テナント分離)。
2026-06-06: Cloudflare Workers + D1 → AWS Lightsail へ移行
背景: 当初 Cloudflare Workers + D1 + Cron で構築したが、subrequest 上限(19アプリ一括seedで超過)や D1 の SQL 変数上限など、ポーリング型・全件取得ワークロードと Workers 制約の相性問題が積み重なった。 既に Lightsail(multi-purpose-lightsail-server1)で複数サービスを Nginx 集約運用しており、 そこに相乗りすれば制約から解放され、運用も一元化できる。
決定: Node.js (Hono) + node:sqlite + systemd で Lightsail に移行。D1 互換ラッパー(src/db.ts)で
既存ロジックを無改変流用し、index.ts を server.ts(常駐) + poll.ts(毎時timer) に分割。
旧 Worker + D1 は完全削除。ドメインは reviewantenna.ikapps.com。
理由: subrequest/SQL変数の上限がなくなり全アプリ毎回・全件取得が可能。固定費は既存サーバーに同居で実質ゼロ増。 D1互換ラッパーにより移行コストを最小化。この決定は下記 2026-06-02 の Cloudflare 採用を覆すもの。
注意点(移行で得た知見): ① Node 22 の node:sqlite は --experimental-sqlite 必須。
② systemd の EnvironmentFile は値中の \n をエスケープ解釈して PEM/JSON を壊すため、秘密情報は
.dev.vars を dotenv で読む方式に統一した。
2026-06-03: 新規アプリのseedは1回のpollで最大3つに分割
背景: 19アプリを一括登録したところ、1回のポーリングで全アプリの全件取得(各最大数十ページ)が走り、Cloudflare の subrequest 上限(1実行あたりの外部リクエスト数)を超えてエラーになった。
決定: 全件seed(重い処理)は1回のpollで最大3アプリまで。残りは次回以降のCronで順次seedする。名前・評価総数・アイコンの軽い更新は全アプリ毎回実行。
理由: subrequest上限に収めつつ、多数アプリ登録にも耐える。新着検知(seed済みアプリは最新1ページ)は軽いので影響なし。seedは数回のCronで完了する。
2026-06-03: レビューのエクスポート形式はJSON
背景: 詳細画面から全レビューをダウンロードする際の形式(JSON / Markdown 等)を選ぶ必要があった。
決定: JSON(app情報のメタ + reviews配列)。ファイル名は {アプリ名}_{ストア}_{yyyymmdd}.json。
理由: 「AIが読みやすい形式」という要件に対し、構造化されパース可能なJSONが最適。AIにアプリ単位で渡して分析させやすい。
2026-06-03: 管理ダッシュボードは無認証
背景: 本番サイトにアプリ一覧・レビュー一覧のダッシュボードを追加するにあたり、認証(Basic/トークン)を付けるか決める必要があった。
決定: 無認証で公開する(/ と /app?id=)。
理由: 表示するレビューは元々 App Store / Google Play で一般公開されている情報のため、無認証でも実害は限定的。手軽さを優先。将来センシティブな情報(売上等)を載せる段階で認証を検討する。
2026-06-03: DB は D1 を継続(スケール時に段階移行)
背景: D1 はリレーショナルDBだが、書き込み単一プライマリ・1DB=10GB上限があり、スケール懸念の指摘があった。
決定: 当面 D1 を継続する。
理由: レビュー監視は書き込みが軽く読み取り・集計中心でRDBと相性が良い。今は自分専用なので D1 単一で十分。スケールの壁が見えたら、まずテナント別D1(Cloudflare推奨パターン)、それでも足りなければ Postgres(Neon/Supabase+Hyperdrive)移行という段階方針。SQL資産はほぼ流用できる。
2026-06-02: App Store は公式 ASC API を継続(RSS案を不採用)
背景: Apple Developer Program メンバーシップが期限切れで、公式 ASC API が使えるか不安があった。代替として認証不要の公開 RSS フィード案も検討した。
決定: 公式 App Store Connect API(customerReviews)を使い続ける。
理由: 実際に叩いたところ、メンバーシップ期限切れでも customerReviews は取得できた。RSS は他人のアプリも取れる利点があるが(公開サービス化では再検討の価値あり)、まずは全期間取得可・構造が安定している公式 API で進める。
2026-06-02: レビュー重複防止は INSERT OR IGNORE 方式
背景: 当初は既存IDを SELECT してから差分を INSERT していたが、IN句に200件渡して D1 の SQL 変数上限(100)を超え too many SQL variables で落ちた。
決定: 事前 SELECT を廃止し、id を PRIMARY KEY にした上で INSERT OR IGNORE + meta.changes 集計で新着件数を得る。一括 UPDATE は50件チャンク分割。
理由: SQL変数上限に依存せず、コードもシンプルになる。冪等性も自然に担保される。
2026-06-02: メール送信は Resend を採用
背景: Cloudflare Workers からのメール送信手段を選ぶ必要があった。
決定: Resend を採用。
理由: Workers と相性が良く実装が容易、無料枠 月3000通、独自ドメイン認証も容易。MailChannels は2024年に無料プランが終了したため不採用。Amazon SES は初期設定が重い。
2026-06-02: 技術スタックは Cloudflare Workers + D1 + Cron
背景: 定期ポーリング型のレビュー監視サービスの基盤選定。Lightsail サーバー案もあった。
決定: Cloudflare Workers + D1 + Cron Triggers。
理由: 無料枠が広く、Cron で定期ポーリング・D1 で既読位置の状態管理ができる。公開サービス化しても耐えられる構成。
DEVLOG(作業ログ)
開発日誌
このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。
2026-06-26
19:35 - RBAC(複数ユーザー共同管理)を設計→実装→本番デプロイ
1アカウント(テナント)を複数ユーザーで権限分けして共同管理する機能(RBAC)を新規追加。 3エージェント並列調査で設計を固め、別LLM(Codex)の3ラウンドレビューで収束させてから Phase 1〜4 を実装し、本番(reviewantenna.ikapps.com)へ反映した。
やったこと:
- 設計(
.devnotes/rbac-handoff.md): 既存user_idを「テナントID」として温存・再解釈する大方針。memberships/invitationsを追加し apps/reviews/credentials は無改造。Codexレビュー R1(high3/med5/low2)→R2(high1/med3/low2)→R3(収束判定)を全反映。やりとりは.gods-talk/に記録 - Phase 1(migration 0010・
src/authz.ts・auth.ts拡張): memberships/invitations(部分UNIQUE)/ sessions(active_owner_id・csrf_token)、全ユーザーを owner として backfill、resolveTenant/getRole/ canEdit 等の認可ヘルパー、email小文字正規化・emailVerified・getSessionContext/setActiveOwner - Phase 2(server/dashboard/credentials): テナント解決ミドルウェア+
gate()+CSRF。テナントデータはt.ownerUserId、channels はuser.id。状態変更POSTは form のowner_idで作用先を明示解決 (stale form の別テナント誤爆対策)。ヘッダのテナント切替セレクタ、ロール別UI出し分け、settings分割 - Phase 3(
src/invitations.ts・notify.ts・members UI): メール招待→受諾(メール一致必須+verified)、 メンバー管理(アンカーowner保護)、sendMail汎用化(招待のみResendフォールバック)、 OAuth callback の招待Cookie合流、通知購読(notify)トグル - Phase 4(migration 0011・poll.ts):
review_deliveriesで(レビュー×メンバー)単位配信、channel無し除外、 cutoff(COALESCE(notify_at,created_at) <= COALESCE(reviewed_at,fetched_at))、notified再計算 - 各フェーズで
npm run typecheck+ e2e/結合テスト(CSRF・テナント切替・ロール別UI・stale form・ 招待受諾・アンカー保護・メンバー配信・冪等性)をパス - 本番デプロイ: push→rsync→npm ci→停止→DBバックアップ(
ra-backup-20260626-182147.db)→ 0010/0011適用(owner backfill: users2=memberships2)→再起動。/health 200・/ 302・/login 200・ /reviews 401 を確認、ログにエラーなし
決めたこと:(詳細は DECISIONS.md 2026-06-26)channels は本人データ(テナント再解釈の例外)/ アンカーowner(テナントID本人)は固定・v1は所有者移譲なし/招待本人性はメール一致必須/ 通知既読は review_deliveries で(レビュー×メンバー)単位/テナントはセッション保持+POSTは form.owner_id で作用先確定/高権限POSTにCSRFトークン導入
詰まり/気づき:
- dev-login は
.dev.varsの Google OAuth 資格が読まれると無効化される → ローカル検証時はGOOGLE_OAUTH_CLIENT_ID=空で起動して dev-login を有効化 - Phase 4 テスト: owner の membership.created_at は backfill で実時刻になるため、過去日時のテスト レビューは cutoff で除外される(= 参加前の過去レビューを浴びせない挙動が正しく機能している証拠)
- サーバーに sqlite3 CLI 無し → migration は
node --experimental-sqliteで適用(前回 0009 と同様)
次回やること:
- 本番で You が実ブラウザ(Google OAuth)でログインし、招待・テナント切替・ロール別UIの最終確認
- 検証で
dev-public.dbにデモ用メンバーデータを投入済み(戻すならdev-public.db.bak) AGENTS.md(Codex生成スタブ)は未追跡のまま
2026-06-25
20:00 - 複数認証情報対応+まとめてアプリ登録(実装→本番デプロイ)
「1認証情報=そのキーで見えるアプリだけ」という制約に対し、(1)同一ストアに複数の認証情報を 登録できるようにし、(2)キーがアクセスできる全アプリを列挙して一括登録できるようにした。 設計判断は DECISIONS.md(2026-06-25)参照。
やったこと:
- migration
0009_multi_credentials.sql:credentialsのUNIQUE(user_id,platform)を撤廃し ニックネームlabel追加(再作成パターン)。appsにcredential_id追加し、既存アプリは 現行の単一鍵へ自動紐付け(移行) - 一覧取得API追加: ASC
GET /v1/apps(listAscApps)、Google は Play Developer Reporting APIapps:search(listGooglePlayApps、スコープ別=createGoogleTokenを scope 引数化) - まとめて登録
/apps/import: 認証情報を選ぶ→そのキーのアプリ一覧をチェックボックス表示 (登録済みは無効)→選択分をcredential_id付きで一括INSERT pollを user単位→credential_id単位グルーピングに変更(鍵ごとにトークン使い回し)。 返信も対象アプリのcredential_idから鍵解決。手動登録フォームは鍵セレクト式+ストアに応じた placeholder 切替- 設定画面: ストアごとに鍵を一覧(label/登録日/削除)+ニックネーム入力つき追加フォーム。 使用中の鍵は削除を拒否(先にアプリ削除を促す)
- ローカル検証(dev-public.db): migration適用・複数鍵・まとめて登録(ASC実APIで42件取得、 登録済み12件は無効表示)・一括/手動登録のcredential_id紐付け・削除ガード・型チェック 全OK
- 本番デプロイ: push→rsync→サービス停止→DBバックアップ→migration 0009適用(26アプリ全て credential_id紐付け・未設定0)→再起動→手動poll検証(checkedApps:26 / errors:[])。 ダウンタイム約40秒
詰まったこと / 気づき:
Response.redirectは絶対URL必須。新ハンドラで相対パスを渡し500→new URL(path, req.url)で修正- 本番サーバーに
sqlite3CLI が無い → アプリと同じnode:sqlite(--experimental-sqlite)で適用 - Google のまとめて登録は androidpublisher と別API・別スコープ。GCP側で Reporting API 有効化+ サービスアカウント権限付与が前提。未対応環境は一覧取得が失敗するため手動入力にフォールバック
- 一覧の Google Play アイコン割れはサーバー側は正常(URLは全件200/PNG)。ブラウザ拡張による
googleusercontent.comブロックが原因と判断。コード変更なし(割れ時のプレースホルダ フォールバックは将来の小改修候補)
次回やること:
- Google のまとめて登録を実使用するなら、本番の各サービスアカウントで Reporting API を有効化
- (任意)アイコン割れ時のフォールバック、認証情報の label リネームUI
2026-06-08
公開マルチテナント化(Phase 1〜3 cutover)
個人専用サービスを一般公開向けのマルチテナント版へ拡張し、本番を切り替えた。public ブランチで
開発→ローカル検証→mainマージ→本番デプロイ。詳細な意思決定は DECISIONS.md(2026-06-08)参照。
やったこと:
- Phase 1:
users/sessionsテーブル、Google OAuth ログイン+セッション(Cookie)、ダッシュボードの ログイン必須化・テナント分離(自分のアプリ/レビューのみ)。既存user_id='me'を運営ユーザーに対応づけ - Phase 2:
credentialsテーブル(AES-256-GCM 暗号化)、/settingsで per-user 認証情報 (ASC .p8/KeyID/IssuerID・Google SA JSON)をアップロード→接続テスト→暗号化保存。 poll をテナント単位に再構成(各自の鍵でトークン生成、未設定はスキップ)。me は env フォールバック - Phase 3 cutover: 本番
.dev.varsに GOOGLE_OAUTH/BASE_URL/APP_ENC_KEY 追加 →public→mainマージ → rsync → prod DB バックアップ&migration 0005/0006 適用 → 再起動。reviewantenna.ikapps.com が ログイン必須の多人数版に。本番検証(/→/login、OAuthリダイレクトhttps、dev-login無効、poll 20アプリ/エラー0)OK
詰まったこと / 気づき:
- Google OAuth クライアントは web 種別で作成。リダイレクトURIに localhost:8003 と本番httpsの両方を登録。 同意画面テスト中はテストユーザー登録 or 公開が必要(email/profileのみなら審査不要)
- ドメインはコードから
BASE_URL(env) に外出し済みのため、後で reviewantenna.com へ移すのは設定変更のみ (コード・データ移行ゼロ、再ログイン1回)。APP_ENC_KEY は固定必須(暗号化済み認証情報の復号鍵) - 本番には sqlite3 CLI が無いため、migration は node:sqlite の exec で適用した
残: Resend独自ドメイン認証(reviewantenna.com移行時に同時推奨)、新規ユーザーのオンボーディング導線。
2026-06-06
20:10 - Cloudflare Workers/D1 → AWS Lightsail へ移行完了
別セッションで進めていた移行(Node移植・D1データ取り込み)の残りを処理し、本番をLightsailへ切替。
やったこと:
- 本番デプロイ:
/home/ubuntu/apps/review-antenna/へ rsync、npm ci --omit=dev、DB(14277件)を/var/lib/review-antenna/へ配置(整合性チェックOK) - systemd: 常駐
review-antenna.service(Hono、ポート8003)+ 毎時review-antenna-poll.timer - Route53 に
reviewantenna.ikapps.comA レコード作成 → nginx リバースプロキシ + certbot で SSL - 疎通確認: /health・ダッシュボード・/reviews API(401含む)・poll(20アプリ・エラー0)すべてOK
- 旧 Cloudflare Worker + D1 を完全削除(REST API経由。workers.dev URL も404化を確認)
tsxを devDeps → deps へ移動、wrangler.toml・wrangler 依存・d1:export を撤去- README / 引き継ぎ.md / Lightsailサーバー側 README を新構成に更新
詰まったこと / 気づき:
- サーバーは Node v22 →
node:sqliteに--experimental-sqliteフラグ必須(systemd で NODE_OPTIONS 指定) - systemd の EnvironmentFile は値中の
\nをnにエスケープ解釈し、PEM鍵・JSON鍵を破壊 (-----BEGIN...-----nMIGT...となり base64 不正)。→ 秘密情報は/etc/*.envをやめ、 WorkingDirectory の.dev.varsを dotenv で読む方式(env.ts のローカル経路と統一)に変更して解決。 - wrangler CLI はこのサンドボックスでハング(TTY依存)→ Cloudflare REST API + OAuthトークンで削除実行。
- Lightsail は 512MB RAM(空き約140MB)。常駐サーバーは ~100MB で収まり許容範囲。
次回やること: 一覧のフィルタ/並べ替え、Slack/ntfy通知、レビュー集計、Resend独自ドメイン認証。
2026-06-03
18:12 - アプリ登録/削除UI・アイコン表示・JSONダウンロード
やったこと:
- 管理画面からアプリ登録・削除(無認証、削除は確認ダイアログ、登録後に即seed)
- 登録時に名前・★評価総数・アイコンを同期取得(登録直後からIDでなく名前が出る)
- アプリアイコンの自動取得・一覧表示(App Store=artworkUrl100、Google Play=og:image)
- 全レビューのJSONダウンロード
GET /export(AIが読みやすいapp情報+reviews配列、全件)- ファイル名
{アプリ名}_{AppStore|GooglePlay}_{yyyymmdd}.json(日本語はfilename* UTF-8)
- ファイル名
- ダッシュボードの見た目改善(ストアバッジを1行固定+色分け、削除ボタン等の折り返し解消)
- 監視アプリを19個まで拡大
決めたこと:(DECISIONS.md にも転記)
- 新規アプリの全件seedは1回のpollで最大3アプリまで分割(subrequest上限対策)
- レビューのエクスポート形式はJSON
詰まったこと / 気づき:
- 19アプリ一括登録で Cloudflare の subrequest 上限(1実行あたりの外部リクエスト数)超過 → 重いseedを1回最大3アプリに分割、軽い補完(名前/評価/アイコン)は全アプリ毎回。
- 登録直後はseed未完了でアプリ名がIDのまま見える → 名前・アイコンを登録時に同期取得して解消。
- システム時計が実時刻とズレている(dateコマンドが深夜を返すが実際は夕方)。 DEVLOGの時刻はユーザーに確認した実時刻を使う。前回の「03:19」も実際は夕方だったため修正。
次回やること: 一覧のフィルタ/並べ替え(ストア別・評価順)、Slack/ntfy通知、Resend独自ドメイン認証。
16:30 - レビュー取得API・管理ダッシュボード・★評価総数表示
やったこと:
- レビュー取得API
GET /reviews追加(READ_TOKEN認証、app/limit/from/to でD1から取得) - App Storeレビュー取得をページング対応(links.next を辿る)
- Google Play のアプリ名自動取得(Play Store ページの og:title)
- 管理ダッシュボード(
/アプリ一覧、/app?id=レビュー詳細、無認証、ダークモード対応) - ★評価総数の取得・表示(App Store=iTunes lookup jp、Google Play=Play Store の JSON-LD ratingCount) → 一覧で「文章レビュー数 (★評価総数)」形式
- 共通HTMLユーティリティを
src/html.tsに切り出し(notify.ts と共有) - 監視対象に Study Timer(6444372570) を追加(文章レビュー463件)
決めたこと:(DECISIONS.md にも転記)
- DB は D1 を継続(自分用のため。スケール時はテナント別D1→Postgres移行という段階方針)
- ダッシュボードは無認証(レビューは元々ストアで公開情報のため)
気づき:
- ASC customerReviews は文章付きレビューのみ返す。Vision Workoutは★評価4944だが文章レビューは 201件で、それは全件取得済み(取りこぼしではない)。「4900」は星だけ評価を含む総数。
- 公式API(ASC/Play)は自分のApple Developer/Play Console権限内のアプリのみ取得可。他人のは403で不可。
- ★評価総数は国別(iTunes lookup country=jp)。日本中心アプリなので jp で4944が一致。
- Cloudflare secret 追加直後は伝播待ちで undefined になることがある→再デプロイで確実化。
次回やること: Slack/ntfy通知追加、レビュー集計・分析、Resend独自ドメイン認証、公開サービス化。
2026-06-02
18:40 - ゼロからレビュー監視サービスをMVPまで構築
実装ゼロの状態から、App Store / Google Play 両対応のレビュー監視・メール通知サービスを 本番稼働まで一気に構築した。
やったこと:
- 技術スタック確定(Cloudflare Workers + D1 + Cron、メール=Resend)
- API調査(App Store Connect / Google Play Developer API)
- 最小構成の実装(src/index.ts の poll、asc.ts、notify.ts、D1スキーマ)
- App Store Connect 実接続成功(JWT/ES256、200件取得・保存・冪等性)
- 本番デプロイ(D1作成、secret設定、Worker deploy、Cron毎時0分)
- メール通知(Resend)接続、実着信確認
- 初回 seed 処理(既存レビューを通知抑止)、SQL変数上限対策
- アプリ名の自動取得(iTunes lookup)、メールのplatform表示整形
- /run に Bearer トークン認証(RUN_TOKEN)
- Google Play 対応(src/gp.ts、サービスアカウントRS256→OAuth→reviews)、実着信確認
- base64url等を src/jwt.ts に共通化
詰まったこと / 気づき:
- D1 の SQL 変数上限は100。IN句に200件で
too many SQL variables。 → INSERT OR IGNORE + meta.changes 集計に変更して解決。 - Apple Developer Program メンバーシップ期限切れでも customerReviews は取得できた。
- Google Play は直近7日のレビューのみ・タイトル無し。Play Console の権限付与は 「ユーザーと権限」でサービスアカウント招待する方式(旧 設定>APIアクセスは廃止)。
- Cloudflare secret 追加直後は伝播が間に合わず undefined になることがある。再デプロイで確実化。
監視中: App Store「Vision Workout」(1563884276) / Google Play「勉強タイマー」(jp.thomsons.pomodoro)
次回やること: Resend独自ドメイン認証(任意宛先送信)、Slack/ntfy等チャネル追加、公開サービス化。
最近のコミット
- 0bc9593 まとめて登録に「すべて選択/すべて解除」ボタンを追加(登録済みは除外) 2026/6/26
- 14bf0aa まとめて登録ピッカーのボタンを「アプリ一覧を見る」→「アプリを選択する」に変更 2026/6/26
- 5248176 UIをApple風ライトミニマルに刷新(Appleブルー・丸ピル・SFフォント・iOS風セグメント) 2026/6/26
- 41af988 ヘッダのテナント切替UIを整理(ロール二重表示の解消・セレクタ整形・改行防止) 2026/6/26
- c399fa2 ナビゲーションの「← 一覧」を「← ホーム」に統一 2026/6/26
- 3c75685 メンバー一覧から本名を除去しメール表示に統一(オーナー固定表記も簡潔化) 2026/6/26
- 9d1fbc9 ヘッダ: 自分のテナントのみのときテナント表示を省略(メール重複を解消) 2026/6/26
- 66d7d6f ヘッダのテナント表示を本名(Google表示名)からオーナーのメールに変更 2026/6/26
- 0c10b59 .devnotes を更新(RBAC 設計→実装→本番デプロイ完了分) 2026/6/26
- 656d394 RBAC Phase 3+4: 招待フロー・メンバー管理・通知のメンバー配信 2026/6/26
README
review-antenna(Review Antenna)
概要
App Store・Google Play・Amazon 等のレビューを監視し、新着投稿をメール等で通知するサービス。
- 当初は自分用、現在はマルチテナントの公開サービス(Google OAuth ログイン、ユーザーごとに分離)
- 通知チャネルはメール(Resend)をはじめ、Slack・ntfy・LINE 等を後から追加できる設計
- 公開URL: https://reviewantenna.ikapps.com (ログイン必須。将来 reviewantenna.com へ移行予定)
- 各ユーザーは
/settingsで自分の App Store / Google Play API認証情報をアップロード(AES-256-GCM で暗号化保存)
技術スタック
Node.js (Hono) + node:sqlite + systemd。AWS Lightsail(multi-purpose-lightsail-server1)で常駐し、 systemd timer で毎時ポーリングする。SQLite で「どこまで読んだか」を保持する。
2026-06 に Cloudflare Workers + D1 + Cron Triggers からこの構成へ移行した。 D1 互換の最小ラッパー(
src/db.ts)で既存ロジックを無改変のまま流用している。
- 先行ターゲット: App Store Connect API(JWT/ES256 認証、全期間取得可)
- Google Play Developer API(直近7日のみ取得可のためポーリング間隔に注意)
- 通知メール送信: Resend
アーキテクチャ
systemd timer(毎時0分) ─▶ poll.ts ─▶ apps から監視対象を取得
─▶ ASC/Play API でレビュー取得(トークンは使い回し)
─▶ 新着のみ reviews に INSERT OR IGNORE(notified=0)
─▶ notified=0 を user 単位で集約 → channels(email) へ送信
─▶ 送信成功で notified=1
systemd service(常駐) ─▶ server.ts (Hono) ─▶ ダッシュボード / 登録・削除 / API / 手動 /run
nginx(443, Let's Encrypt) でリバースプロキシ
| ファイル | 役割 |
|---|---|
src/server.ts |
常駐 HTTP サーバー(Hono)。ダッシュボード・API・登録/削除・認証・設定・/run・/health |
src/auth.ts |
Google OAuth2 + セッション(sessions)+ email突き合わせによるユーザーupsert |
src/credentials.ts |
テナント別の認証情報 get/save/status+接続テスト(me は env フォールバック) |
src/crypto.ts |
認証情報の AES-256-GCM 暗号化/復号(鍵は APP_ENC_KEY から導出) |
src/poll.ts |
ポーリング本体。systemd timer から毎時実行。ユーザー単位に各自の認証情報で取得 |
src/db.ts |
node:sqlite による D1 互換ラッパー(prepare/bind/all/first/run/batch) |
src/env.ts |
環境変数の読込(.dev.vars を dotenv でロード。複数行PEM対応) |
src/asc.ts |
App Store Connect クライアント(JWT生成 + レビュー取得 + アプリ情報) |
src/gp.ts |
Google Play クライアント(サービスアカウントRS256→OAuth→reviews) |
src/notify.ts |
通知送信(type 分岐。現状 email/Resend のみ) |
src/dashboard.ts |
ダッシュボード(一覧/詳細)・JSONエクスポートのHTML生成 |
src/jwt.ts |
base64url / pemToDer の共通ユーティリティ |
src/types.ts |
Env と共有型(DB互換インターフェース含む) |
migrations/ |
apps / reviews / channels スキーマ(0001〜0004) |
deploy/ |
systemd unit(service/timer)・nginx 設定 |
ローカル開発
npm install
# シークレットを用意(複数行PEMもそのまま貼れる)
cp .dev.vars.example .dev.vars # 中身を埋める
# スキーマ適用(初回のみ。DB ファイルは ./review-antenna.db)
cat migrations/*.sql | sqlite3 review-antenna.db
npm run server # 常駐サーバー起動(http://localhost:8003)
npm run poll # 単発ポーリング
npm run typecheck
Node 24 未満(本番サーバーは v22)では node:sqlite に --experimental-sqlite が必要。
systemd unit では NODE_OPTIONS=--experimental-sqlite を設定している(ローカル v24+ では不要)。
監視対象・通知先の登録
アプリは管理ダッシュボードの登録フォームから追加できる。SQL で直接入れる場合:
-- 監視するアプリ(external_id は App Store の数値 appId / Google Play のパッケージ名)
INSERT INTO apps (user_id, platform, external_id, name, created_at)
VALUES ('me', 'app_store', '1234567890', 'My App', unixepoch()*1000);
-- 通知先(config は JSON)
INSERT INTO channels (user_id, type, config, created_at)
VALUES ('me', 'email', '{"to":"you@example.com"}', unixepoch()*1000);
本番(Lightsail)
| 項目 | 値 |
|---|---|
| 配置先 | /home/ubuntu/apps/review-antenna/(rsync デプロイ) |
| シークレット | /home/ubuntu/apps/review-antenna/.dev.vars(chmod 600、git管理外) |
| DB | /var/lib/review-antenna/review-antenna.db |
| ポート | 8003(nginx → 127.0.0.1:8003) |
| 常駐 | review-antenna.service(systemd、Restart=always) |
| 毎時poll | review-antenna-poll.timer → review-antenna-poll.service(0 * * * * 相当) |
| seedキャッチアップ | review-antenna-seed.timer → review-antenna-seed.service(5分間隔・未seedのみ取り込み。登録直後seedの安全網) |
| ドメイン | https://reviewantenna.ikapps.com (Let's Encrypt SSL) |
デプロイ手順・unit の実体は deploy/ を参照。再デプロイは概ね次の通り:
# ローカルから(node_modules/.git/*.db/.dev.vars は除外)
rsync -az --delete --exclude node_modules --exclude .git --exclude '*.db*' \
--exclude .dev.vars --exclude .wrangler --exclude .devnotes \
-e "ssh -i ~/.ssh/lightsail-multipurpose" ./ ubuntu@13.230.63.19:/home/ubuntu/apps/review-antenna/
ssh -i ~/.ssh/lightsail-multipurpose ubuntu@13.230.63.19 \
'cd /home/ubuntu/apps/review-antenna && npm ci --omit=dev && sudo systemctl restart review-antenna'
手動ポーリング: ssh ... 'sudo systemctl start review-antenna-poll.service'
ログ: ssh ... 'journalctl -u review-antenna.service -f'