Let’s be honest: if you’re an indie iOS dev shipping your third app and still checking App Store rankings manually—or worse, paying $99/month for Sensor Tower—you’re leaking revenue and sanity. Keyword rank tracking isn’t optional anymore. It’s how you know whether that ASO tweak you shipped actually moved the needle on “budgeting app for freelancers” or just padded your analytics dashboard with noise. Enter aso-tracker: a lean, self-hosted, TypeScript-based rank tracker built by an indie dev for indie devs. At 68 stars (as of May 2024), it’s tiny—but it’s also the only open-source tool I’ve found that scrapes Apple’s App Store without hitting rate limits, handles regional keyword tracking natively, and doesn’t require you to hand over your App Store Connect credentials. I’ve run it for 17 days across 3 apps (iOS only, no Android), tracking 22 keywords across US, CA, and DE—and it’s caught 3 rank shifts before App Store Connect’s own analytics registered them.
What Is aso-tracker — And Why It’s Not Another “Me-Too” Rank Scraper
aso-tracker is a Node.js service (TypeScript, built with tsc + express) that polls Apple’s public App Store search endpoints—not the private, undocumented, rate-limited APIs that most commercial tools rely on. It uses Apple’s official search URL format:https://apps.apple.com/us/search?term=keyword&media=apps
…then parses the HTML response (yes, HTML—Apple still serves mobile search results as rendered pages, not JSON) to extract app position, bundle ID, and title. Crucially, it does not use Puppeteer, Playwright, or any headless browser. Instead, it leverages cheerio + node-fetch, with smart retry backoffs and region-aware URL generation. That means lower memory footprint, no Chromium bloat, and resilience against minor HTML structure changes (Apple did tweak their search DOM in March 2024—aso-tracker kept working; my Puppeteer-based fork of app-store-scraper broke for 36 hours).
It’s not a dashboard-first tool. There’s no built-in UI. You get a /api/ranks endpoint (JSON), a simple /api/health, and a /api/export for CSV. Everything else—charting, alerts, trend analysis—is up to you. That’s intentional. As the README says: “This is a building block, not a product.”
How to Install and Run aso-tracker (Docker-First, No Node.js Hell)
I tried the “npm install && npm start” path first. It worked—but then I remembered I’m running this on a $5/month Hetzner CX11 (1 vCPU, 2GB RAM), and tsc --build chewed 1.4GB RAM during compilation. Not sustainable. Docker is the only sane way to deploy this long-term.
Here’s the exact docker-compose.yml I use (tested on Docker 24.0.7, Compose v2.23.0):
version: '3.8'
services:
aso-tracker:
image: ghcr.io/dsm5e/aso-tracker:0.4.2
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- TRACKING_INTERVAL=3600000 # 1 hour in ms
- APP_STORE_REGIONS=us,ca,de
- APP_STORE_APPS=com.myapp.budget,com.myapp.timer,com.myapp.notes
- APP_STORE_KEYWORDS=free+budgeting+app,freelancer+expense+tracker,time+tracker+for+freelancers
ports:
- "3000:3000"
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
A few notes on those env vars:
TRACKING_INTERVAL=3600000: Yes, it’s in milliseconds. Default is1800000(30 min), but Apple heavily throttles repeated queries from the same IP. I found 1h per region+keyword combo is the sweet spot: enough data fidelity, zero 429s.APP_STORE_REGIONS: Comma-separated country codes exactly as used in App Store URLs (us,ca,de,jp,au). Not ISO 3166-1 alpha-2—Apple’s own region codes.APP_STORE_APPS: Bundle IDs only. No App Store IDs, no names. Case-sensitive. You must use the exact bundle ID (check Xcode → Signing & Capabilities → Bundle Identifier).APP_STORE_KEYWORDS: Space-separated, but URL-encoded terms. Sofreelancer expense trackerbecomesfreelancer+expense+tracker. No quotes, no commas.
After docker-compose up -d, check logs:docker-compose logs -f aso-tracker
You’ll see lines like:[INFO] Tracking keyword "freelancer+expense+tracker" in region "us" for app "com.myapp.budget"
Then, after ~90 seconds:[SUCCESS] Found app "com.myapp.budget" at position 7 in us for "freelancer+expense+tracker"
The data lands in ./data/ranks.json as a timestamped array. Example snippet:
{
"timestamp": "2024-05-12T08:23:41.123Z",
"region": "us",
"keyword": "freelancer+expense+tracker",
"app_id": "com.myapp.budget",
"position": 7,
"title": "BudgetFlow — Freelancer Expense Tracker"
}
Why Self-Host This? Who Actually Needs It?
Let’s cut the fluff: this is for indie devs, small studios (<5 people), and privacy-obsessed agencies who refuse to send their app’s bundle ID and keywords to a SaaS vendor. That’s it.
If you’re using AppRadar, Sensor Tower, or MobileAction—you’re paying $79–$299/month to track hundreds of keywords across dozens of apps. aso-tracker won’t replace that. It will, however, replace the spreadsheet you manually update every Tuesday—and the $19/month “lite” plan you’re overpaying for because you only need 3 apps and 15 keywords.
Here’s who shouldn’t run it:
- Teams needing real-time Slack alerts for rank drops (no built-in webhook support yet—though I added one in my fork).
- Devs without basic CLI/Docker literacy (you will need to
greplogs and inspectranks.json). - Anyone expecting App Store Connect-style graphs (no frontend, no historical visualization out of the box).
System requirements? I’m running it on that $5 Hetzner box alongside 2 other services (a lightweight PostgreSQL and a static nginx proxy). aso-tracker uses ~85MB RAM steady-state, peaks at 120MB during scraping, and hovers at 0.05–0.12 CPU load. Disk usage is trivial: 22 keywords × 3 regions × 24 scrapes/day × 30 days = ~1.2MB of JSON per month. You could run this on a Raspberry Pi 4 (4GB RAM) without blinking.
aso-tracker vs. Alternatives: The Real Trade-Offs
Let’s compare head-to-head—not on feature lists, but on what actually matters when your app’s ranking for “habit tracker” drops from #3 to #14 overnight.
| Tool | Self-hosted? | iOS Only? | Cost (annual) | Keyword Limit | Rate Limit Resilience | Data Export Format |
|---|---|---|---|---|---|---|
aso-tracker |
✅ Yes | ✅ Yes | $0 | Unlimited (but be reasonable) | ✅ Built-in jitter + region stagger | JSON, CSV |
app-store-scraper (npm) |
✅ Yes | ✅ Yes | $0 | Unlimited | ❌ Breaks on HTML changes, no built-in retry | JSON only |
| Sensor Tower | ❌ No | ✅ Yes | $999–$3,588 | 50–500 | ✅ Proprietary anti-block infra | CSV, PDF, API |
| AppRadar | ❌ No | ✅ Yes | $480–$2,400 | 20–200 | ✅ Yes, but hides methodology | CSV, API, dashboard |
ios-app-ranker (Python) |
✅ Yes | ✅ Yes | $0 | Unlimited | ⚠️ Uses requests-html; slower, heavier |
JSON |
The kicker? aso-tracker is the only one that lets you see exactly what Apple’s search page returned. I added a debug mode (DEBUG=true) that logs the raw HTML snippet it parsed for each query. When Apple changed <div class="we-lockup__content"> to <div class="we-lockup__content we-lockup__content--app"> in March, I spotted the breakage in 4 minutes—not 2 days waiting for a maintainer’s PR.
Also: aso-tracker respects robots.txt. Most scrapers don’t. That matters if you’re running from a shared IP (like a cloud VPS). Apple has blocked /32 subnets for aggressive scraping. This tool won’t get you banned.
Practical Tips, Gotchas, and My 17-Day Reality Check
Running this for over two weeks taught me things the README doesn’t say:
- Keyword encoding is not optional. Try
keyword=habit%20trackerinstead ofhabit+tracker? It fails. Apple’s search treats+as space only in the query param.%20breaks it. Stick to+. - Bundle IDs must be exact.
com.myapp.budget≠com.myapp.budget-ios. Check your App Store Connect page URL:https://apps.apple.com/us/app/my-budget-app/id1234567890→ theid1234567890is not your bundle ID. Your bundle ID is in Xcode. - Regional results are different—even for the same keyword. “Time tracker” in
usgave me position 5 for my app. Inde, it was position 12—but also returned 3 German-language apps with English titles.aso-trackerdoesn’t filter by language. That’s on you. - No built-in deduplication. If you track
freelancer+trackerandfreelancer+time+tracker, and both return your app at position 3, you’ll get two entries. Not a bug—just how it’s designed.
I built a tiny Python script to slice ranks.json into daily aggregates and push to InfluxDB. Then I used Grafana to plot position trends. Took 47 lines. Here’s the core loop:
import json
from datetime import datetime, timedelta
with open("./data/ranks.json") as f:
data = json.load(f)
# Group by day
by_day = {}
for r in data:
day = datetime.fromisoformat(r["timestamp"].split("T")[0]).date()
if day not in by_day:
by_day[day] = []
by_day[day].append(r)
# Find min position per day per keyword
for day, ranks in by_day.items():
for kw in set(r["keyword"] for r in ranks):
positions = [r["position"] for r in ranks if r["keyword"] == kw and r["position"] > 0]
if positions:
print(f"{day} | {kw} | best: {min(positions)}")
Final Verdict: Should You Deploy It?
Yes—but with eyes wide open.
aso-tracker is not production-ready out of the box. It has no auth, no rate-limiting on its own /api endpoints, no database (just append-only JSON), and zero error reporting for failed keyword/app combos. If your APP_STORE_APPS env var contains a typo, it fails silently. I patched that locally with a startup validation step that curls each app’s App Store page to confirm it exists.
That said, for what it does—and how little it asks in return—it’s exceptionally well-built. The TypeScript is clean, the Docker image is multi-stage (47MB final size), and the maintainer responds to issues within 48 hours (I filed one about German Umlauts in app titles—got a fix in 22 hours).
Is it worth deploying? If you’re tracking ≤ 5 apps and ≤ 30 keywords across ≤ 4 regions, and you have any comfort with Docker and reading logs: absolutely. You’ll save $1,200/year vs. Sensor Tower’s entry tier—and gain full ownership of your ranking data. The ROI kicks in around Day 8.
Rough edges to expect:
- No alerting (write your own cron +
curlor use Uptime Kuma + webhook). - No historical visualization (Grafana + InfluxDB is the obvious path).
- You will need to write a small script to dedupe, aggregate, or enrich the
ranks.jsondata. - No Android support—and no plans for it (the maintainer’s stance: “Apple’s search is predictable; Google Play’s is chaos”).
The TL;DR? aso-tracker is the htop of ASO tools: minimalist, reliable, and brutally honest about what it is. It won’t hold your hand. But if you know how to read a log, tweak a docker-compose.yml, and write a 20-line Python parser—you’ll get more actionable insight than most $200/month SaaS dashboards. And you’ll know exactly where your data lives. That alone is worth the 20 minutes it takes to deploy.
Comments