Self-hosted App Store keyword tracker for indie iOS developers.
Tracks where your apps rank for a set of keywords across multiple App Store regions, snapshots the top results, and remembers everything — so you can see real ASO trends instead of guessing from this-week-only screenshots. Runs entirely on your Mac: a Vapor service stores history in SQLite, a Svelte dashboard renders it, and a menubar app supervises the whole thing.
The keyword-tracking tools indie devs reach for are either expensive subscriptions, abandoned web apps, or a spreadsheet that goes stale within a week. Keywordista gives you the same dashboard you'd pay $50/mo for, but you own the data and the schedule.
Built for tracking dozens of keywords across half a dozen regions for a handful of apps — single-user scale. Not multi-tenant, not cloud-deployed, doesn't try to be.
A signed/notarized DMG is on the way. For now, build from source — it's two commands.
git clone https://github.com/bootuz/keywordista.git
cd keywordista
make open-mac-app
The make open-mac-app target builds the Vapor server, builds the SPA, assembles Keywordista.app, and opens it. A magnifying-glass icon appears in your menu bar. Click Open Dashboard → the browser opens http://127.0.0.1:8080/ (or :8081…:8090 if :8080 is already taken).
Prefer no menubar app? Use the launcher script:
./keywordista
This builds the SPA and execs the Vapor server in the foreground. Ctrl+C stops it. Same dashboard at http://127.0.0.1:8080/. Data lives in ./db.sqlite instead of ~/Library/Application Support/Keywordista/.
┌──────────────────────────────────┐
│ Keywordista.app (menubar shell) │
│ ─ Spawns + supervises the server │
│ ─ Picks a free port (8080–8090) │
│ ─ "Open Dashboard" in browser │
│ ─ Quit kills the child cleanly │
└────────────┬─────────────────────┘
│ spawns
▼
┌──────────────────────────────────────────────────┐
│ Vapor server (Swift, 127.0.0.1 only) │
│ ├ REST API under /api/v1 │
│ ├ Static Svelte SPA on / │
│ ├ Daily refresh job (03:00 UTC) + on-demand │
│ └ Polite serial worker (~1 req/sec to iTunes) │
└────────────┬───────────────────────┬─────────────┘
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ SQLite (Fluent) │ │ iTunes Search API │
│ + append-only │ │ (no key required) │
│ rank history │ └──────────────────────┘
└──────────────────┘
RankCheck row keyed by (keyword, watched_app, observed_at). We dedupe consecutive identical observations into a single row with firstSeenAt/checkedAt, so a stable rank doesn't bloat the DB but the timeline still tells you exactly when something changed.127.0.0.1 only. Anything that could send an HTTP request to it can already read the SQLite file directly — so the bearer-token gate would only add UX friction, not security.# All targets are in the Makefile — run `make help` for the catalog.
make build-web # build the SPA into Public/
make build # swift build the server
make dev-backend # run the server in the foreground
make dev-web # Vite dev server on :5173 (proxies /api → :8080)
make mac-app # build Keywordista.app from sources
make open-mac-app # build + open the .app
swift test # run server tests
The mac/build-dmg.sh script produces a signed + notarized DMG suitable for sharing on GitHub Releases. It does the full release flow: universal binaries (arm64 + x86_64) for both the menubar app and the server, Developer ID Application signing with hardened runtime + timestamp, DMG packaging, Apple notarization, and ticket stapling. Output lands in releases/Keywordista-$VERSION.dmg.
One-time setup (only needed for full signing + notarization):
# Store notarytool credentials in your keychain. You'll need an
# app-specific password from https://appleid.apple.com/account/manage
xcrun notarytool store-credentials keywordista \
--apple-id <your-apple-id> \
--team-id KHNA6PF8QV \
--password <app-specific-password>
Build commands:
make dmg # full release: sign + notarize + staple
make dmg-unsigned # skip signing entirely (faster, for testing)
# Or per-stage opt-out via env vars:
KEYWORDISTA_SKIP_NOTARIZE=1 make dmg # sign but don't notarize
Contributors without a Developer ID cert can use make dmg-unsigned to verify the build flow. The resulting DMG installs but Gatekeeper will show "unidentified developer" on first launch.
Tagging app-v0.1.0 and pushing the tag triggers .github/workflows/release-app.yml, which runs the same build-dmg.sh on a macos-15 runner with all signing + notarization secrets injected. See .github/RELEASING.md for the one-time secret-configuration ritual.
| Path | What lives there |
|---|---|
Sources/App/ |
The Vapor server — models, controllers, services, jobs |
Tests/AppTests/ |
Swift Testing suite (20 tests) for scoring + repositories + services |
web/ |
The Svelte 5 + TypeScript + Tailwind SPA |
mac/ |
The SwiftUI MenuBarExtra app + the Keywordista.app build script |
Public/ |
Built SPA assets (regenerated by npm run build) |
keywordista |
Single-command launcher script for headless / dev mode |
Everything under /api/v1. No auth — 127.0.0.1-only.
| Method | Path | What |
|---|---|---|
GET |
/health |
Liveness probe (no auth needed; the menubar app pings this) |
POST |
/apps |
Add a watched app — body { appStoreId, lookupCountry } |
GET |
/apps |
List watched apps |
DELETE |
/apps/:id |
Remove a watched app (cascades to its rank history) |
POST |
/keywords |
Add a tracked keyword — body { term, countryCode } |
GET |
/keywords |
List keywords |
DELETE |
/keywords/:id |
Remove a keyword (cascade) |
POST |
/keywords/:id/refresh |
Enqueue one immediate refresh |
POST |
/refresh-all |
Enqueue refresh for every keyword |
GET |
/refresh-status |
{ pending } — how many jobs are queued |
GET |
/dashboard |
The dashboard table — one row per (keyword, watched_app) |
GET |
/keywords/:id/history?watchedAppId=… |
Full rank history for one (keyword, app) pair |
GET |
/settings/{asc,asa} |
Read App Store Connect / Apple Search Ads credential status |
PUT/DELETE |
/settings/{asc,asa} |
Update / clear those credentials |
GET |
/api/v1/version |
{ current, latest, updateAvailable, downloadUrl } — used by the menubar app's update check |
See requests.http for ready-to-run curl/JetBrains-HTTP examples.
difficulty and entryBarrier from search results alone. ASA integration is a hookable seam — the credentials slot in /api/v1/settings/asa is already wired, just no fetcher consuming it yet.mac/Sources/Keywordista/), but bumping the .app version still means downloading a new DMG.Keywordista.app is macOS-only. Linux users can clone + ./keywordista from source.Bug reports and PRs welcome — see CONTRIBUTING.md. The ASO scoring heuristic in particular (Sources/App/Services/KeywordScorer.swift) is a documented best-effort approximation; sharper formulas with citations are explicitly invited.
Found a vulnerability? See SECURITY.md.
Apache License 2.0. © 2026 Astemir Boziev.