← 一覧に戻る

review-antenna

GitHub ↗ TypeScript 最終push: 2026/6/12 19:46

WIP(現在進行中)

Work In Progress

このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。

現在の状況

公開マルチテナント版が本番稼働中(2026-06-08 cutover完了)。Node.js (Hono) + node:sqlite + systemd。

  • 本番: https://reviewantenna.ikapps.com/ (Nginx + Let's Encrypt SSL)
    • Google OAuth ログイン必須。ユーザーごとにアプリ・レビュー・認証情報を分離(テナント分離)
    • / 自分のアプリ一覧(登録/削除、アイコン・★評価総数・平均)/ /app?id= 詳細 / /export JSON
    • /settings自分のASC(.p8/KeyID/IssuerID)・Google Play(SA JSON) をアップロード→接続テスト→AES-256-GCM暗号化保存
  • レビュー取得API: GET /reviews?app=&limit=&from=&to=(READ_TOKEN認証、不変)
  • 毎時0分 systemd timer で全テナントをポーリング(各自の認証情報でレビュー取得)→ sqlite保存 → メール通知
  • 運営 meichirokisanuki@gmail.com)は既存20アプリ・レビュー14277件を保持。認証情報は当面 env フォールバック
  • secret/設定は /home/ubuntu/apps/review-antenna/.dev.vars(ASC3/Resend2/RUN/READ/GOOGLE + GOOGLE_OAUTH 2/BASE_URL/APP_ENC_KEY)
  • 旧 Cloudflare Worker + D1 は完全削除済み

残・次にやること:

  1. (任意)Resend 独自ドメイン認証 — 他ユーザーの任意アドレスへ通知。reviewantenna.com 移行時にまとめてやると手戻り無し(meへの通知は今のまま届く)
  2. 実運用ドメイン reviewantenna.com への引っ越し(登録→DNS→nginx→BASE_URL/OAuthリダイレクト/Resend更新。コード/データ変更なし)
  3. 新規ユーザーのオンボーディング導線(サインアップ後の通知チャネル自動作成など)
  4. 一覧のフィルタ/並べ替え、Slack/ntfy通知、レビュー集計

未確認: 本番ブラウザでのGoogleログイン(サーバー側検証は完了、ユーザー確認待ち)。 注意: システム時計が実時刻とズレている(DEVLOGの時刻はユーザー確認した実時刻を使う)。 重要: 本番 APP_ENC_KEY は固定。変えると暗号化済み認証情報が復号不能になる(同一サーバー/.dev.varsを維持)。

詳細は本番運用情報含めプロジェクトルートの 引き継ぎ.md に集約。


過去のWIPアーカイブ

(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)

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アプリに分割)

今週

  • 一覧のフィルタ/並べ替え(ストア別・評価の低い順・★1だけ等)
  • Resend 独自ドメイン認証(任意宛先に送信可能に)

今月

  • 通知チャネル追加(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)
  • Phase 3 残: Resend 独自ドメイン認証(他ユーザー通知用。reviewantenna.com 移行時に同時実施推奨)
  • 実運用ドメイン reviewantenna.com への引っ越し(設定のみ。コード/データ変更なし)
  • 新規ユーザーのオンボーディング導線(サインアップ後の通知チャネル自動作成など)
  • (実ユーザー増時)$10/1GB 増強、tsx→ビルド済みJS で軽量化

いつか

  • Amazon レビュー対応
  • レビュー返信機能(ASC / Play の返信API)
  • レビュー分析(評価推移、感情分析など)

DECISIONS(意思決定)

意思決定記録

このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。


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.tsserver.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-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 追加 → publicmain マージ → 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.com A レコード作成 → 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 は値中の \nn にエスケープ解釈し、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等チャネル追加、公開サービス化。

最近のコミット

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.timerreview-antenna-poll.service0 * * * * 相当)
ドメイン 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'