Back to Rules
📋
GitHub Store CLAUDE.md
🩵 A free, open-source app store for GitHub releases — browse, discover, and install apps with one click. Powered by Kotlin and Compose Multiplatform for Androi
CLAUDE.md
> Sourced from [OpenHub-Store/GitHub-Store](https://github.com/OpenHub-Store/GitHub-Store) — [Apache-2.0](https://github.com/OpenHub-Store/GitHub-Store/blob/31d26f3d8c3286a9d70c07d4fbf330f67e3d3d05/feature/apps/CLAUDE.md).
# CLAUDE.md - GitHub Store
## Project Overview
GitHub Store is a cross-platform app store for GitHub releases, built with **Kotlin Multiplatform (KMP)** and **Compose Multiplatform**. Targets **Android** (min API 26) and **Desktop** (Windows, macOS, Linux via JVM).
Package: `zed.rainxch.githubstore` | Version: 1.6.2 (code 13) | Target SDK: 36
## Build & Run Commands
```bash
# Android
./gradlew :composeApp:assembleDebug
./gradlew :composeApp:assembleRelease
# Desktop (run in dev mode)
./gradlew :composeApp:run
# Desktop installers
./gradlew :composeApp:packageExe :composeApp:packageMsi # Windows
./gradlew :composeApp:packageDmg :composeApp:packagePkg # macOS
./gradlew :composeApp:packageDeb :composeApp:packageRpm # Linux
# Full build check
./gradlew build
```
**Requirements:** JDK 21+ (Temurin recommended), Android SDK for Android builds.
## Project Structure
```
composeApp/ # Main app module (entry points, navigation, DI wiring)
src/commonMain/ # Shared UI & app wiring
src/androidMain/ # Android entry point (MainActivity)
src/jvmMain/ # Desktop entry point (DesktopApp.kt)
core/
domain/ # Shared interfaces, models, use cases (no framework deps)
data/ # Shared repos, networking (Ktor), database (Room), DI
presentation/ # Shared theming (Material 3) & reusable UI components
feature/
apps/ # Installed applications management
auth/ # GitHub OAuth device flow authentication
details/ # Repository details, releases, readme, downloads
dev-profile/ # Developer/user profile display
favourites/ # Saved favorite repositories (presentation-only)
home/ # Main discovery screen (trending, hot, popular)
profile/ # User profile, settings, appearance, proxy, Shizuku installer
search/ # Repository search with filters
starred/ # Starred repositories (presentation-only)
build-logic/convention/ # Custom Gradle convention plugins
```
Each feature has up to 3 sub-modules: `domain/` (interfaces & models), `data/` (implementations & DI), `presentation/` (screens & ViewModels). Some features (favourites, starred) are presentation-only and use core repositories directly.
## Architecture
**Clean Architecture + MVVM** with strict layer separation per feature module:
- **Domain** - Repository interfaces, models, use cases (no framework dependencies)
- **Data** - Repository implementations, Ktor API clients, Room DAOs, DTOs, mappers
- **Presentation** - ViewModels with `StateFlow`/`Channel`, Compose screens
### State Management Pattern
Every screen follows the same State/Action/Event pattern:
```kotlin
class XViewModel : ViewModel() {
private val _state = MutableStateFlow(XState())
val state = _state.asStateFlow() // or .stateIn() with WhileSubscribed
private val _events = Channel<XEvent>()
val events = _events.receiveAsFlow()
fun onAction(action: XAction) { ... }
}
```
- `State` - data class holding all UI state
- `Action` - sealed interface for user input (clicks, refreshes, etc.)
- `Event` - sealed interface for one-off effects (navigation, toasts, scroll)
### Navigation
Type-safe navigation using `@Serializable` sealed interface `GithubStoreGraph`:
```
HomeScreen, SearchScreen, AuthenticationScreen, ProfileScreen,
FavouritesScreen, StarredReposScreen, AppsScreen, SponsorScreen
DetailsScreen(repositoryId, owner, repo, isComingFromUpdate)
DeveloperProfileScreen(username)
```
Routes defined in `composeApp/.../app/navigation/GithubStoreGraph.kt`, wired in `AppNavigation.kt`.
### Dependency Injection
**Koin** - modules defined in each feature's `data/di/SharedModule.kt`, registered in `composeApp/.../app/di/initKoin.kt`. ViewModels injected via `koinViewModel()`.
### Core Modules
| Module | Purpose | Key Contents |
|--------|---------|--------------|
| `core/domain` | Shared contracts | Repository interfaces (`FavouritesRepository`, `StarredRepository`, `InstalledAppsRepository`, `ThemesRepository`, `ProxyRepository`, `RateLimitRepository`), models (`GithubRepoSummary`, `GithubRelease`, `InstalledApp`, `ProxyConfig`, `InstallerType`, `ShizukuAvailability`), system interfaces (`Installer`, `InstallerInfoExtractor`, `InstallerStatusProvider`, `PackageMonitor`) |
| `core/data` | Shared implementations | `HttpClientFactory` (Ktor + interceptors), `AppDatabase` (Room), `ProxyManager`, `TokenStore`, `LocalizationManager`, platform-specific clients (OkHttp for Android, CIO for Desktop), Shizuku integration (Android: `ShizukuServiceManager`, `ShizukuInstallerWrapper`, `ShizukuInstallerServiceImpl`, `AndroidInstallerStatusProvider`; Desktop: `DesktopInstallerStatusProvider`) |
| `core/presentation` | Shared UI | `GithubStoreTheme` (Material 3), reusable components (`RepositoryCard`, `GithubStoreButton`), formatting utils, localized strings (13 languages) |
## Tech Stack
| Area | Library | Version |
|------|---------|---------|
| Language | Kotlin | 2.3.10 |
| UI | Compose Multiplatform | 1.10.1 |
| HTTP | Ktor | 3.4.0 |
| Database | Room | 2.8.4 |
| DI | Koin | 4.1.1 |
| Serialization | Kotlinx Serialization | 1.10.0 |
| Preferences | DataStore | 1.2.0 |
| Image Loading | Landscapist (Coil3) | 2.9.5 |
| Logging | Kermit | 2.0.8 |
| Permissions | MOKO Permissions | 0.20.1 |
| Navigation | Navigation Compose | 2.9.2 |
| Markdown | Multiplatform Markdown Renderer | 0.39.2 |
| Shizuku | Shizuku API | 13.1.5 |
| Background Work | WorkManager | 2.11.1 |
| Date/Time | Kotlinx Datetime | 0.7.1 |
All versions managed in `gradle/libs.versions.toml` (Version Catalog).
## Convention Plugins
Custom Gradle plugins in `build-logic/convention/` standardize module setup:
| Plugin | Use For |
|--------|---------|
| `convention.kmp.library` | KMP shared library modules (domain, data) |
| `convention.cmp.library` | Compose Multiplatform library modules (core/presentation) |
| `convention.cmp.feature` | Feature presentation modules (auto-adds Compose + Koin + core:presentation) |
| `convention.cmp.application` | Main app module |
| `convention.room` | Room database modules |
| `convention.buildkonfig` | Build-time config (API keys from local.properties) |
## Adding a New Feature
1. Create `feature/<name>/domain/`, `feature/<name>/data/`, `feature/<name>/presentation/`
2. Add `build.gradle.kts` in each using the appropriate convention plugin
3. Add `include` entries in `settings.gradle.kts`
4. Define domain interfaces/models in `domain/`
5. Implement repository + Koin DI module in `data/di/SharedModule.kt`
6. Create ViewModel (State/Action/Event pattern) and Screen in `presentation/`
7. Add navigation route to `GithubStoreGraph.kt` and wire in `AppNavigation.kt`
8. Register the Koin module in `initKoin.kt`
## Key Configuration
- **GitHub OAuth:** Set `GITHUB_CLIENT_ID` in `local.properties`. Callback URL: `githubstore://callback`. Deep link: `githubstore://repo`
- **Shizuku (Android):** Optional silent install via `ShizukuProvider` (registered in AndroidManifest). Requires Shizuku app running with ADB or root. AIDL service passes APK via `ParcelFileDescriptor` to `pm install -S`. Falls back to standard installer on failure.
- **Gradle properties:** Config cache enabled, build cache enabled, 4GB Gradle heap, 3GB Kotlin daemon heap
- **Code style:** Official Kotlin style (`kotlin.code.style=official`)
- **Desktop logs:** `CrashReporter` (installed as the first line of `DesktopApp.main`) tees `System.out`/`System.err` to a rotating `session.log` and writes `crash-<timestamp>.log` on uncaught exceptions. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Windows), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android uses Logcat — no CrashReporter.
- **`X-GitHub-Token` header:** **Client-side**, `BackendApiClient.getRepo` / `getReleases` / `getReadme` / `getUser` always attach the header when a token exists in `TokenStore.currentToken()` (sourced through the `private` helper `currentUserGithubToken()`); they don't gate it on what the backend will do with it. **Backend-side**, the same header is consumed on every passthrough route — `/v1/search`, `/v1/search/explore`, `/v1/repo/{owner}/{name}` (only when the lazy-fetch DB-miss path actually hits GitHub upstream — cached hits don't need it), `/v1/releases/{owner}/{name}`, `/v1/readme/{owner}/{name}`, `/v1/user/{username}` — and re-sent as `Authorization: token $token` to `api.github.com`. The upstream call then runs under the user's own 5000/hr OAuth quota; the per-token rate-limit bucket on the backend's `search` plugin is keyed by token-hash (or IP for anon), so a logged-in user always gets their own per-user 60/min slot regardless of POP. **Without the header**, the backend round-robins a 4-token service pool (20k/hr aggregate) outside the daily quiet window; if the user's own token gets 403/429 from GitHub, the backend retries once with a pool token (also outside the quiet window) before surfacing the failure. The DB-side resource cache slot is keyed `|authed` vs `|anon` per token presence, so a 4xx response under one token tier doesn't poison the cache for the other tier. DB-only routes never read the header: `/v1/categories`, `/v1/topics`, `/v1/events`, `/v1/auth/device/start`, `/v1/auth/device/poll`, `/v1/badge/...` — and the client doesn't send it on those either (the per-route `httpClient.get` block decides). Never logged (no Ktor `Logging` plugin installed). **Status-code semantics** on passthrough routes: backend remaps GitHub-upstream 401 (rejected token, e.g. revoked PAT) into a backend `502` so the client never sees a "session expired" 401 on these endpoints; `502` therefore means *either* "GitHub unreachable" *or* "upstream rejected our auth" and is handled the same way — fall back to direct GitHub via `shouldFallbackToGithubOrRethrow`. `429` from these routes means the backend exhausted both the user's token bucket *and* the pool retry (or the request landed in the quiet window where pool retry is disabled); the client must **not** fall back to direct GitHub on `429` (same wall, same token), only back off and retry later — `shouldFallbackToGithubOrRethrow` already returns `false` for the entire 4xx range. The client's `UnauthorizedInterceptor` only installs on `createGitHubHttpClient` (direct GitHub calls), and `AuthenticationStateImpl` additionally debounces consecutive 401s under the same token (token snapshot threaded through from the request, reset on any non-401 response) so a single transient direct-GitHub 401 can't sign the user out.
- **Device-flow auth proxy:** `feature/auth` calls `/v1/auth/device/start` and `/v1/auth/device/poll` on the backend as the primary path so users on networks that throttle `github.com` (China, corporate filters) can still complete login. Each session picks one `AuthPath` (`Backend` or `Direct`) at start and sticks to it; `AuthenticationRepositoryImpl` only escalates `Backend → Direct` on infrastructure errors (`HttpRequestTimeoutException`, `SocketTimeoutException`, `ConnectTimeoutException`, `BackendHttpException` with 5xx). HTTP 4xx and GitHub's valid-but-negative 200-bodies (`authorization_pending`, `slow_down`, `access_denied`, `expired_token`, `bad_verification_code`) are real answers, never cause fallback. `AuthenticationViewModel` persists `auth_path` in `SavedStateHandle` so activity recreation resumes on the same path. The Direct path still requires `BuildKonfig.GITHUB_CLIENT_ID` — both paths use the same OAuth App, so client-side `GITHUB_CLIENT_ID` must match the backend's `GITHUB_OAUTH_CLIENT_ID`. Shared backend constants live in `core/data/network/BackendEndpoints.kt` (`BACKEND_ORIGIN`, `BACKEND_BASE_URL`). Backend responses carry `X-Request-ID` — `GitHubAuthApi` embeds it in every error message via `asRequestIdTag()` so bug reports can cite the ID and it maps straight to backend logs. Backend rate limits (10 starts/hr, 200 polls/hr per source IP) are hard — do not add retry loops on top of Ktor's existing `HttpRequestRetry(maxRetries = 2)`.
## Coding Conventions
- Packages follow `zed.rainxch.{module}.{layer}` pattern
- Private state properties use underscore prefix: `_state`
- Sealed classes/interfaces for type-safe navigation routes, actions, events
- Repository pattern: interface in `domain/`, implementation in `data/`
- Composition over inheritance via Koin DI
- Source sets: `commonMain` for shared, `androidMain` for Android, `jvmMain` for Desktop
- Feature CLAUDE.md files exist in each `feature/` directory for module-specific guidance
Add to your project
Paste into your project's CLAUDE.md or ~/.claude/CLAUDE.md for global rules.
More for Kotlin
MCP servers for Kotlin
Browse all MCP servers →Browse by Tag
Get the Claude Code Starter Pack
Top CLAUDE.md rules for Next.js, TypeScript, Python, Go, and React — free.
