Native iOS and Android client for phpMyFAQ, built with Kotlin Multiplatform, SwiftUI, and Jetpack Compose.
Status: Phase 0 (foundations). The apps display a placeholder screen that proves the shared module, API client, encrypted database, and secure storage are wired end-to-end. No user-visible features yet -- those land in Phase 1.
- Website: myfaq.app
- phpMyFAQ minimum version: 4.2.0
- Business model: freemium (read + offline free forever; writes behind Pro unlock in Phase 3)
| Tool | Minimum | Install |
|---|---|---|
| JDK | 17+ | brew install openjdk or Temurin |
| Android SDK | API 35 | Android Studio or sdkmanager |
| Xcode | 15.0 | Mac App Store (macOS only) |
| XcodeGen | latest | brew install xcodegen |
| Git | 2.x | pre-installed on macOS |
Run the bootstrap checker:
mobile/scripts/bootstrap.shbrew install --cask android-commandlinetools
sdkmanager "platforms;android-35" "build-tools;35.0.0"
echo "sdk.dir=$HOME/Library/Android/sdk" > mobile/local.propertiesIf you see xcodebuild errors about IDESimulatorFoundation:
sudo xcodebuild -runFirstLaunch
sudo xcodebuild -license acceptphpMyFAQ/MyFAQ
├── plans/ Planning docs (source of truth)
│ ├── mobile-app-plan.md Architecture & feature plan
│ └── phase-0-foundations.md Phase 0 detailed execution plan
├── docs/mobile/ Technical docs
│ ├── architecture.md Module diagram & boundaries
│ ├── build.md Build reference
│ └── phase-0-handoff.md Pinned versions & decisions
├── .github/workflows/ CI/CD
│ ├── mobile-ci.yml Lint + test + debug builds
│ ├── mobile-openapi-sync.yml API client regeneration
│ └── mobile-release.yml Signed release (Phase 1)
└── mobile/ Source tree (Gradle root)
├── shared/ Kotlin Multiplatform module
│ ├── src/commonMain/ Business logic, API, DB, DI
│ ├── src/androidMain/ Android platform actuals
│ ├── src/iosMain/ iOS platform actuals
│ └── src/commonTest/ Shared tests
├── androidApp/ Jetpack Compose host
├── iosApp/ SwiftUI host (XcodeGen)
├── spec/openapi/ Pinned phpMyFAQ API spec
└── scripts/ Dev tooling
cd mobile
./gradlew :androidApp:assembleDebugThe debug APK lands at
mobile/androidApp/build/outputs/apk/debug/androidApp-debug.apk.
Install on a connected device or emulator:
adb install androidApp/build/outputs/apk/debug/androidApp-debug.apkOr open mobile/ as a project in Android Studio and run from the IDE.
# 1. Build the shared KMP framework
cd mobile
./gradlew :shared:assembleSharedDebugXCFramework
# 2. Generate the Xcode project (one-time, re-run after project.yml changes)
cd iosApp
xcodegen generate
# 3a. Open in Xcode and run (Cmd+R)
open iosApp.xcodeproj
# 3b. Or build from CLI
xcodebuild \
-project iosApp.xcodeproj \
-scheme iosApp \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
CODE_SIGNING_ALLOWED=NO \
buildTo run on a simulator from CLI after building:
xcrun simctl boot "iPhone 16" 2>/dev/null
xcrun simctl install booted \
build/Build/Products/Debug-iphonesimulator/iosApp.app
xcrun simctl launch booted app.myfaq.iosNote: After changing shared Kotlin code, rebuild the XCFramework (step 1) before rebuilding in Xcode. The pre-build script in
project.ymldoes this automatically but adds build time. For faster iteration, disable it in Xcode's Build Phases and run the Gradle task manually.
cd mobile
# Shared module tests (JVM — fast, no device/simulator needed)
./gradlew :shared:testDebugUnitTest
# Shared module iOS tests (requires macOS + Xcode)
./gradlew :shared:iosSimulatorArm64Test
# All shared tests
./gradlew :shared:allTests| Test file | What it validates |
|---|---|
MyFaqApiTest |
/meta deserialization, unknown-key tolerance, MetaLoader rendering |
EntitlementsTest |
Entitlements.isPro() always returns false |
SecureStoreContract |
Round-trip put/get/remove/clear (abstract; run by platform tests) |
| Layer | Technology |
|---|---|
| Shared logic | Kotlin Multiplatform |
| Android UI | Jetpack Compose, Material 3 |
| iOS UI | SwiftUI |
| HTTP | Ktor (MockEngine in Phase 0; OkHttp/Darwin in Phase 1) |
| Serialization | kotlinx.serialization |
| Database | SQLDelight + SQLCipher (Android) / Data Protection (iOS) |
| Secure storage | EncryptedSharedPreferences (Android) / Keychain (iOS) |
| DI | Koin |
| API codegen | openapi-generator (Kotlin multiplatform target) |
See mobile/gradle/libs.versions.toml
for the single source of truth. Key versions:
- Kotlin 2.1.20, Gradle 9.4.1, AGP 8.10.0
- Ktor 3.0.3, SQLDelight 2.0.2, Koin 4.0.0
- Android min SDK 26, iOS 16.0
The Kotlin API client is generated from the pinned phpMyFAQ OpenAPI
spec at mobile/spec/openapi/v3.2.yaml. To update:
# Download the spec from a phpMyFAQ release
curl -fsSL \
https://raw.githubusercontent.com/thorsten/phpMyFAQ/4.2.0/docs/openapi.yaml \
-o mobile/spec/openapi/v3.2.yaml
# Regenerate (requires openapi-generator-cli or Docker)
mobile/scripts/generate-api-client.shOr trigger the mobile-openapi-sync GitHub Actions workflow.
shared/src/commonMain/kotlin/app/myfaq/shared/
├── api/
│ ├── MyFaqApi.kt Interface + impl (Phase 0: /meta only)
│ ├── MetaLoader.kt Callback facade for platform UI
│ ├── HttpClientFactory.kt MockEngine wiring (Phase 0)
│ └── dto/Meta.kt /meta response DTO
├── data/
│ └── DatabaseFactory.kt Encrypted DB creation + passphrase mgmt
├── di/
│ └── SharedModule.kt Koin modules + initKoin()
├── domain/
│ └── Instance.kt Instance + AuthMode domain models
├── entitlements/
│ └── Entitlements.kt Pro gate facade + expect object
└── platform/
├── SecureStore.kt expect class (put/get/remove/clear)
└── DatabaseDriverFactory.kt expect class (create with passphrase)
| Phase | What ships | Status |
|---|---|---|
| 0 | KMP scaffold, CI, encrypted DB, secure storage, Entitlements stub | current |
| 1 | Workspaces, categories, FAQ list/detail, server search, paywall shell | planned |
| 2 | Offline: SQLite + FTS5, background sync, attachments (public v1.0.0) | planned |
| 3 | Pro: StoreKit 2 + Play Billing, login/OAuth2, ask/comment/rate (v2.0.0) | planned |
| 4 | Accessibility, localization, telemetry scrub | planned |
| 5 | Widgets, watch app, iPad split-view, push notifications | planned |
See plans/mobile-app-plan.md for the
full architecture plan and
plans/phase-0-foundations.md for
Phase 0 details.
- Check out the repo and run
mobile/scripts/bootstrap.sh - Make changes under
mobile/ - Run
cd mobile && ./gradlew :shared:allTestsbefore pushing - CI runs on every push to
mainand on PRs (path-filtered tomobile/**)
- Kotlin: enforced by ktlint + detekt (run via
./gradlew ktlintCheck detekt) - Swift: swiftlint (configured in CI)
- Commit messages: Conventional Commits
© 2026 Thorsten Rinne. All rights reserved.