review-antenna
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=詳細 //exportJSON/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保存 → メール通知
- 運営
me(ichirokisanuki@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 は完全削除済み
残・次にやること:
- (任意)Resend 独自ドメイン認証 — 他ユーザーの任意アドレスへ通知。reviewantenna.com 移行時にまとめてやると手戻り無し(meへの通知は今のまま届く)
- 実運用ドメイン reviewantenna.com への引っ越し(登録→DNS→nginx→BASE_URL/OAuthリダイレクト/Resend更新。コード/データ変更なし)
- 新規ユーザーのオンボーディング導線(サインアップ後の通知チャネル自動作成など)
- 一覧のフィルタ/並べ替え、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.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-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等チャネル追加、公開サービス化。
最近のコミット
- fd832dd DECISIONS: サービス名は Review Antenna で確定(ReviewReply案は不採用) 2026/6/12
- 4bd47ae レビューへの手動返信機能を追加(App Store / Google Play) 2026/6/12
- f9725dc スクショのぼかしがダイアログ枠にはみ出していたのを修正 2026/6/12
- d7b3d2f ASCガイドのスクショのマスクをグレー塗りからぼかしに変更 2026/6/12
- 18ebd4e ASCガイドのキーのロールをApp Managerのみに(返信に必要な権限のため)+スクショ差し替え 2026/6/12
- e5e3010 ストア/レビュー数の本文セルを横中央寄せに 2026/6/10
- 10217bd 平均値を控えめ+横中央に(星を主役に) 2026/6/10
- 6fc74e3 ヘッダー各見出しを列幅で横中央寄せに 2026/6/10
- be209c2 静的配信にキャッシュヘッダを付与(根本対処)+CSS版14 2026/6/10
- 10a02bd ヘッダー行を縦中央揃えに(th vertical-align:middle) 2026/6/10
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 * * * * 相当) |
| ドメイン | 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'