Problem
The dashboard currently shows keyword rank for one selected app at a time. When several apps compete in the same category, there is no way to see how they stack up side by side on a shared keyword set without manually switching between apps and mentally diffing the numbers.
Typical questions that are hard to answer today:
- On which keywords does my app lead vs. trail competitors?
- If I track keywords on App A and my teammate tracks a different set on App B, what does the unified picture look like?
- Where are the biggest rank gaps worth closing?
Proposal
Add a Compare Apps view reachable from the dashboard. The user picks N apps (2–10) and M keywords (1–100), and a matrix renders with rows = keywords, columns = apps, cells = current rank + change delta.
Key design point: ranks are already derivable from the existing aso_keywords.ordered_app_ids top-200 list, so an app's rank can be computed for any keyword it appears in even if that app has not explicitly tracked the keyword in app_keywords. This means the matrix is meaningful even when apps have different keyword lists.
No schema changes required. Two new read-only endpoints:
GET /api/aso/compare/keywords — union of keywords tracked by the selected apps, with coverage metadata.
POST /api/aso/compare/matrix — per-(app, keyword) rank/change data.
Why it fits this project
- Local-first, read-only — no new external API calls, no auth surface changes.
- Reuses existing primitives (
Button, Input, Badge, Card), existing route factory pattern, and the same SQL idiom already used by keyword-handlers.ts (json_each(ordered_app_ids)).
- Respects current boundary ownership: no changes to
keywordPipelineService or keywordWriteRepository.
- Adds 12 unit tests in
cli/db/app-compare.test.ts (full suite still passes: 393 tests).
Scope (what would ship)
- Compare entry point next to the sidebar Apps heading.
- Live-updating matrix (debounced refetch on selection changes).
- Custom keyword picker with "tracked by all / any" filter and search.
- Per-column sort (keyword, popularity, any app's rank).
- Inline removal of apps/keywords from the matrix.
- Fullscreen toggle on the matrix (+ ESC to exit).
- localStorage persistence of last comparison.
- Three distinct empty-cell states: not tracked, outside top 200, not yet researched — each with tooltip + aria-label.
Explicitly out of scope for v1 (could come later based on feedback):
- Shareable URL state / named saved comparisons
- CSV or Markdown export
- Sparkline / historical trend per cell
Status
Implementation PR: #1
Everything passes locally (typecheck + 393/393 tests + build). I have opened the PR alongside this issue so the code is visible for review, but fully happy to adjust scope, API shape, or integration approach — or hold off entirely — based on your feedback.
Thanks for building this tool.
Problem
The dashboard currently shows keyword rank for one selected app at a time. When several apps compete in the same category, there is no way to see how they stack up side by side on a shared keyword set without manually switching between apps and mentally diffing the numbers.
Typical questions that are hard to answer today:
Proposal
Add a Compare Apps view reachable from the dashboard. The user picks N apps (2–10) and M keywords (1–100), and a matrix renders with rows = keywords, columns = apps, cells = current rank + change delta.
Key design point: ranks are already derivable from the existing
aso_keywords.ordered_app_idstop-200 list, so an app's rank can be computed for any keyword it appears in even if that app has not explicitly tracked the keyword inapp_keywords. This means the matrix is meaningful even when apps have different keyword lists.No schema changes required. Two new read-only endpoints:
GET /api/aso/compare/keywords— union of keywords tracked by the selected apps, with coverage metadata.POST /api/aso/compare/matrix— per-(app, keyword) rank/change data.Why it fits this project
Button,Input,Badge,Card), existing route factory pattern, and the same SQL idiom already used bykeyword-handlers.ts(json_each(ordered_app_ids)).keywordPipelineServiceorkeywordWriteRepository.cli/db/app-compare.test.ts(full suite still passes: 393 tests).Scope (what would ship)
Explicitly out of scope for v1 (could come later based on feedback):
Status
Implementation PR: #1
Everything passes locally (typecheck + 393/393 tests + build). I have opened the PR alongside this issue so the code is visible for review, but fully happy to adjust scope, API shape, or integration approach — or hold off entirely — based on your feedback.
Thanks for building this tool.