{"id":50114341,"url":"https://github.com/bootuz/keywordista","last_synced_at":"2026-05-23T14:00:55.775Z","repository":{"id":359576990,"uuid":"1246640731","full_name":"bootuz/keywordista","owner":"bootuz","description":"Self-hosted App Store keyword tracker for indie iOS devs. Vapor + SQLite + Svelte + SwiftUI menubar.","archived":false,"fork":false,"pushed_at":"2026-05-23T14:00:19.000Z","size":1295,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T14:00:23.924Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bootuz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T11:56:04.000Z","updated_at":"2026-05-23T13:46:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bootuz/keywordista","commit_stats":null,"previous_names":["bootuz/keywordista"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/bootuz/keywordista","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bootuz%2Fkeywordista","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bootuz%2Fkeywordista/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bootuz%2Fkeywordista/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bootuz%2Fkeywordista/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bootuz","download_url":"https://codeload.github.com/bootuz/keywordista/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bootuz%2Fkeywordista/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33398391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-23T14:00:25.513Z","updated_at":"2026-05-23T14:00:55.739Z","avatar_url":"https://github.com/bootuz.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Keywordista\n\nSelf-hosted App Store keyword tracker for indie iOS developers.\n\n[![CI](https://github.com/bootuz/keywordista/actions/workflows/ci.yml/badge.svg)](https://github.com/bootuz/keywordista/actions/workflows/ci.yml)\n[![License: Apache 2.0](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE)\n[![macOS 13+](https://img.shields.io/badge/macOS-13%2B-lightgrey.svg)](#install)\n\nTracks 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.\n\n---\n\n## Why\n\nThe 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.\n\nBuilt 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.\n\n---\n\n## Install\n\n\u003e **A signed/notarized DMG is on the way.** For now, build from source — it's two commands.\n\n### Prerequisites\n\n- **macOS 13** Ventura or later\n- **Swift 5.10+** (bundled with Xcode 15.3 / standalone toolchain)\n- **Node 18+** for the Svelte SPA build\n\n### From source\n\n```bash\ngit clone https://github.com/bootuz/keywordista.git\ncd keywordista\nmake open-mac-app\n```\n\nThe `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).\n\n#### Headless / dev mode\n\nPrefer no menubar app? Use the launcher script:\n\n```bash\n./keywordista\n```\n\nThis builds the SPA and `exec`s 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/`.\n\n---\n\n## How it works\n\n```\n                           ┌──────────────────────────────────┐\n                           │ Keywordista.app  (menubar shell) │\n                           │ ─ Spawns + supervises the server │\n                           │ ─ Picks a free port (8080–8090)  │\n                           │ ─ \"Open Dashboard\" in browser    │\n                           │ ─ Quit kills the child cleanly   │\n                           └────────────┬─────────────────────┘\n                                        │ spawns\n                                        ▼\n              ┌──────────────────────────────────────────────────┐\n              │ Vapor server (Swift, 127.0.0.1 only)             │\n              │ ├ REST API under /api/v1                         │\n              │ ├ Static Svelte SPA on /                         │\n              │ ├ Daily refresh job (03:00 UTC) + on-demand      │\n              │ └ Polite serial worker (~1 req/sec to iTunes)    │\n              └────────────┬───────────────────────┬─────────────┘\n                           ▼                       ▼\n                ┌──────────────────┐    ┌──────────────────────┐\n                │ SQLite (Fluent)  │    │ iTunes Search API    │\n                │ + append-only    │    │ (no key required)    │\n                │   rank history   │    └──────────────────────┘\n                └──────────────────┘\n```\n\n- **Append-only history.** Each refresh writes a `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.\n- **No auth.** The server binds to `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.\n- **Polite worker.** One job at a time, ~1 req/sec to iTunes. Stays well below Apple's edge-throttling threshold.\n\n---\n\n## Dev workflow\n\n```bash\n# All targets are in the Makefile — run `make help` for the catalog.\nmake build-web         # build the SPA into Public/\nmake build             # swift build the server\nmake dev-backend       # run the server in the foreground\nmake dev-web           # Vite dev server on :5173 (proxies /api → :8080)\nmake mac-app           # build Keywordista.app from sources\nmake open-mac-app      # build + open the .app\nswift test             # run server tests\n```\n\n### Building a release DMG\n\nThe `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`.\n\n**One-time setup** (only needed for full signing + notarization):\n\n```bash\n# Store notarytool credentials in your keychain. You'll need an\n# app-specific password from https://appleid.apple.com/account/manage\nxcrun notarytool store-credentials keywordista \\\n  --apple-id    \u003cyour-apple-id\u003e \\\n  --team-id     KHNA6PF8QV \\\n  --password    \u003capp-specific-password\u003e\n```\n\n**Build commands:**\n\n```bash\nmake dmg              # full release: sign + notarize + staple\nmake dmg-unsigned     # skip signing entirely (faster, for testing)\n\n# Or per-stage opt-out via env vars:\nKEYWORDISTA_SKIP_NOTARIZE=1 make dmg    # sign but don't notarize\n```\n\nContributors 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.\n\n#### Automated releases via GitHub Actions\n\nTagging `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`](.github/RELEASING.md) for the one-time secret-configuration ritual.\n\n### Project layout\n\n| Path | What lives there |\n|---|---|\n| `Sources/App/` | The Vapor server — models, controllers, services, jobs |\n| `Tests/AppTests/` | Swift Testing suite (20 tests) for scoring + repositories + services |\n| `web/` | The Svelte 5 + TypeScript + Tailwind SPA |\n| `mac/` | The SwiftUI `MenuBarExtra` app + the `Keywordista.app` build script |\n| `Public/` | Built SPA assets (regenerated by `npm run build`) |\n| `keywordista` | Single-command launcher script for headless / dev mode |\n\n---\n\n## API surface\n\nEverything under `/api/v1`. No auth — `127.0.0.1`-only.\n\n| Method | Path | What |\n|---|---|---|\n| `GET` | `/health` | Liveness probe (no auth needed; the menubar app pings this) |\n| `POST` | `/apps` | Add a watched app — body `{ appStoreId, lookupCountry }` |\n| `GET` | `/apps` | List watched apps |\n| `DELETE` | `/apps/:id` | Remove a watched app (cascades to its rank history) |\n| `POST` | `/keywords` | Add a tracked keyword — body `{ term, countryCode }` |\n| `GET` | `/keywords` | List keywords |\n| `DELETE` | `/keywords/:id` | Remove a keyword (cascade) |\n| `POST` | `/keywords/:id/refresh` | Enqueue one immediate refresh |\n| `POST` | `/refresh-all` | Enqueue refresh for every keyword |\n| `GET` | `/refresh-status` | `{ pending }` — how many jobs are queued |\n| `GET` | `/dashboard` | The dashboard table — one row per `(keyword, watched_app)` |\n| `GET` | `/keywords/:id/history?watchedAppId=…` | Full rank history for one (keyword, app) pair |\n| `GET` | `/settings/{asc,asa}` | Read App Store Connect / Apple Search Ads credential status |\n| `PUT`/`DELETE` | `/settings/{asc,asa}` | Update / clear those credentials |\n| `GET` | `/api/v1/version` | `{ current, latest, updateAvailable, downloadUrl }` — used by the menubar app's update check |\n\nSee `requests.http` for ready-to-run `curl`/JetBrains-HTTP examples.\n\n---\n\n## What's not here (yet)\n\n- **Apple Search Ads popularity scores.** v1 derives `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.\n- **Push / email rank-change alerts.** The data's all in the history table; a small job consumer would do it.\n- **Auto-updates of the menubar app itself.** The server can update independently (the menubar app pulls service tarballs from GitHub Releases — see `mac/Sources/Keywordista/`), but bumping the .app version still means downloading a new DMG.\n- **Linux / Windows.** Vapor runs everywhere; `Keywordista.app` is macOS-only. Linux users can clone + `./keywordista` from source.\n\n---\n\n## Contributing\n\nBug reports and PRs welcome — see [CONTRIBUTING.md](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.\n\nFound a vulnerability? See [SECURITY.md](SECURITY.md).\n\n---\n\n## License\n\n[Apache License 2.0](LICENSE). © 2026 Astemir Boziev.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbootuz%2Fkeywordista","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbootuz%2Fkeywordista","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbootuz%2Fkeywordista/lists"}