This document is the detailed execution plan for Phase 0 of
MyFAQ.app. It expands the single-bullet summary in
plans/mobile-app-plan.md into the concrete tasks, tooling, and exit
criteria needed to stand up the project before any user-visible
feature work begins.
Phase 0 ends when a developer can check out the repository, run a
single command per platform, and get a signed debug build of both
apps installed on a simulator / emulator — with the shared module,
generated API client, encrypted database, secure storage, and
Entitlements facade stub all wired up end-to-end but not yet
exercised by any real screen.
- Stand up the mobile source tree inside the existing
phpMyFAQ/MyFAQrepository (https://github.com/phpMyFAQ/MyFAQ) with a reproducible, hermetic build on macOS (iOS) and Linux (Android). - Lock the Kotlin Multiplatform module layout and the two native host projects (SwiftUI + Jetpack Compose) so later phases only add features, never re-shape the build.
- Commit the first generated Kotlin API client from the pinned phpMyFAQ
v3.2OpenAPI specification and prove it compiles into both targets. - Implement the data-at-rest and credentials-at-rest primitives every
later phase depends on: encrypted SQLDelight database, Keychain /
Keystore wrappers, and an
Entitlementsfacade stub that always returnsfalse. - Have CI green on every push, with lint, unit tests, and signed-artifact debug builds for both platforms.
- No UI beyond a placeholder screen on each platform that demonstrates the shared module is wired in (e.g. a label displaying the pinned phpMyFAQ version the generated client was built against).
- No networking against a real phpMyFAQ instance. All Phase 0 tests run
against Ktor
MockEnginefixtures. - No Pro / billing integration.
Entitlementsis a hard-coded stub. - No store listings, screenshots, or privacy disclosures. Those land in Phase 4.
- The skeleton below checked into
phpMyFAQ/MyFAQonmainalongside the existingplans/andLICENSE. - A green GitHub Actions run covering lint, unit tests, and debug builds for both platforms.
- A tagged
mobile-v0.0.0-foundationspre-release on GitHub so future phases have a stable baseline to diff against. Themobile-tag prefix keeps the mobile release train independent of any future non-mobile tags in the same repository. docs/mobile/phase-0-handoff.mdlisting the decisions made, the tool versions pinned, and any carry-overs into Phase 1.
The mobile source tree is nested under a top-level mobile/
directory so it coexists cleanly with the existing plans/ and
LICENSE, and so any future non-mobile content in the same
repository stays out of the Gradle / Xcode working set.
phpMyFAQ/MyFAQ (repository root — this checkout)
├── LICENSE
├── CLAUDE.md
├── plans/ # existing planning docs
│ ├── mobile-app-plan.md
│ └── phase-0-foundations.md
├── docs/
│ └── mobile/
│ ├── architecture.md # module diagram + boundaries
│ ├── build.md # how to build locally
│ └── phase-0-handoff.md
├── .github/
│ └── workflows/
│ ├── mobile-ci.yml # lint + unit + debug builds
│ ├── mobile-openapi-sync.yml # regenerates the API client on
│ │ # phpMyFAQ release tags
│ └── mobile-release.yml # tag-driven signed artifact build
└── mobile/
├── settings.gradle.kts # Gradle root for the mobile build
├── build.gradle.kts
├── gradle.properties
├── gradlew / gradlew.bat
├── buildSrc/ # shared Gradle conventions in Kotlin
├── gradle/
│ └── libs.versions.toml # single source of truth for deps
├── shared/ # Kotlin Multiplatform module
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/kotlin/
│ │ └── app/myfaq/shared/
│ │ ├── api/ # generated client lives here
│ │ ├── data/ # SQLDelight models, DAOs
│ │ ├── domain/ # pure models, no framework deps
│ │ ├── entitlements/ # Entitlements facade
│ │ └── platform/ # expect declarations
│ ├── commonTest/
│ ├── androidMain/kotlin/ # actual impls for Android
│ └── iosMain/kotlin/ # actual impls for iOS
├── androidApp/ # Jetpack Compose host
│ ├── build.gradle.kts
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── kotlin/app/myfaq/android/
├── iosApp/ # SwiftUI host
│ ├── iosApp.xcodeproj
│ └── iosApp/
│ ├── iosAppApp.swift
│ └── ContentView.swift
├── spec/
│ └── openapi/
│ ├── v3.2.yaml # pinned spec copy
│ └── VERSION # phpMyFAQ tag it was captured at
├── scripts/
│ ├── generate-api-client.sh # wraps openapi-generator
│ └── bootstrap.sh # one-shot dev env check
├── .editorconfig
├── .gitignore # Gradle / Xcode / DerivedData
└── README.md # mobile-specific quickstart
The repository root LICENSE is reused — the mobile tree does not
ship its own. GitHub Actions workflow files must live at
.github/workflows/ (not inside mobile/), so mobile workflows are
prefixed mobile-*.yml to keep them visually grouped and out of the
way of any non-mobile workflows added later.
Recorded in mobile/gradle/libs.versions.toml and in
docs/mobile/phase-0-handoff.md. Bump deliberately; never
auto-upgrade.
- Kotlin: latest stable at Phase 0 kickoff (target
2.x). - Gradle: matching Kotlin's recommended minimum.
- Android Gradle Plugin: matching Kotlin + AGP compatibility matrix.
- JDK: Temurin 17 for both local builds and CI.
- Android compile SDK: current stable (expected API 35 at kickoff).
- Android min SDK: 26, per
mobile-app-plan.md. - Xcode: latest stable supported by the KMP Kotlin version.
- iOS deployment target: iOS 16, per
mobile-app-plan.md. - Ktor, kotlinx.serialization, SQLDelight, Koin, openapi-generator: latest stable at kickoff, pinned verbatim.
Exact numeric versions are filled in at the moment the repository is created and captured in the handoff doc so future maintainers can reproduce the kickoff state.
The shared module exposes a minimal surface to native code and hides
every framework dependency inside commonMain.
commonMain— all business logic, DTOs, API client, DAOs,Entitlementsfacade, Koin modules.commonTest— JVM-backed tests with a KtorMockEngineand an in-memory SQLDelight driver.androidMain—actualimplementations for secure storage, the SQLCipher driver, and the Android Keystore entitlement cache.iosMain—actualimplementations for Keychain wrappers and the Core Data-protected SQLDelight driver.
// commonMain
expect class SecureStore {
fun put(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
expect fun createDatabaseDriver(
name: String,
passphrase: ByteArray,
): SqlDriver
expect object EntitlementsPlatform {
fun isPro(): Boolean // Phase 0: always false
}Nothing else is expected in Phase 0 — additional platform hooks
(push, billing, biometrics) land in their owning phases.
apiModule— KtorHttpClient,Json, and the generated API client.dataModule— SQLDelight database, DAOs.platformModule—SecureStoreandEntitlementsPlatformbindings.appModule— aggregates the above; consumed by both hosts via astartKoincall in a sharedinitKoin()helper.
The pinned copy at spec/openapi/v3.2.yaml is captured from the
phpMyFAQ tag recorded in spec/openapi/VERSION. Phase 0 captures this
once, from the phpMyFAQ 4.2.0 tag, in keeping with the locked
minimum-version decision.
- Generator:
openapi-generator-cliwith thekotlingenerator,library=multiplatform,serializationLibrary=kotlinx_serialization. - Invocation:
mobile/scripts/generate-api-client.sh, callable locally and from CI. The script writes output tomobile/shared/src/commonMain/kotlin/app/myfaq/shared/api/generated/. - Commit policy: the generated sources are committed. The build does
not regenerate on each run — reproducibility beats freshness. The
dedicated
mobile-openapi-sync.ymlworkflow opens a PR whenevermobile/spec/openapi/v3.2.yamlchanges.
A thin hand-written layer under api/ wraps the generated client so
calling code never imports generated types directly. This lets later
phases replace the generator without rippling into DAOs or UI.
interface MyFaqApi {
suspend fun meta(): Meta
// Phase 1 adds categories, faqs, search…
}Phase 0 implements exactly one endpoint through this wrapper — /meta
— because it is the single call the placeholder screen exercises and
it validates the full generated-client pipeline.
Configure kotlinx.serialization with
ignoreUnknownKeys = true and explicitNulls = false. This is a
Phase 0 decision that applies to every later DTO.
Only the tables required for the instance selector and for proving
encryption works are created in Phase 0. The full schema from
mobile-app-plan.md lands in Phase 2.
-- shared/src/commonMain/sqldelight/app/myfaq/shared/data/Instances.sq
CREATE TABLE instances (
id TEXT PRIMARY KEY NOT NULL,
display_name TEXT NOT NULL,
base_url TEXT NOT NULL,
api_version TEXT NOT NULL,
auth_mode TEXT NOT NULL,
last_successful_ping INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);Secrets (api_client_token, OAuth client ID / secret, session cookie)
are referenced by stable keys that resolve through SecureStore.
Nothing sensitive touches SQLite.
- Android: SQLCipher via the
sqldelight-android-driverwith a passphrase derived from a hardware-backed key in the Android Keystore. The key is generated lazily on first launch. - iOS:
NativeSqliteDriverwithonConfigurationenablingNSFileProtectionCompleteUntilFirstUserAuthentication. iOS does not use SQLCipher in v1 — Data Protection Class B meets the threat model inmobile-app-plan.md.
Phase 0 ships schema version 1. Every later phase adds a numbered
migration file under
shared/src/commonMain/sqldelight/migrations/. Never edit existing
migration files; always append.
- Android:
androidx.security.crypto.EncryptedSharedPreferencesfor theSecureStore, backed by a master key in the Keystore. Each instance UUID namespaces its entries. - iOS: Keychain Services via a small Objective-C bridging helper
exposed through
iosMain. Service =app.myfaq.ios, access group reserved for a future App Group / share extension.
Both implementations are unit-tested with platform-specific tests in
androidUnitTest and iosTest source sets, plus an abstract contract
test shared through commonTest so both adapters pass the same
behavioural suite.
// commonMain
object Entitlements {
fun isPro(): Boolean = EntitlementsPlatform.isPro()
}
// androidMain + iosMain (Phase 0)
actual object EntitlementsPlatform {
actual fun isPro(): Boolean = false
}UI code in Phase 1 already guards Pro entry points with
Entitlements.isPro(); Phase 3 replaces the actual bodies with real
StoreKit 2 and Play Billing implementations without touching any
calling site.
- Jetpack Compose, Material 3.
- Single activity, single screen (
MainActivity+AppRoot.kt). - The screen calls
MyFaqApi.meta()against a KtorMockEngineseeded with a fixture and renders the returned version string. This proves the shared module, Koin graph, generated client, and serialization are all wired correctly. R8/ minification is enabled for thereleasebuild even though no release artifact is published in Phase 0; catching keep-rule issues now is cheaper than catching them in Phase 2.
- SwiftUI
Appentry point, oneContentViewbacked by a shared observable that calls into the shared framework. - The shared framework is produced by the KMP
iosSimulatorArm64,iosArm64, andiosX64targets via aXCFrameworkassembled at build time.CocoaPodsis not used; the Xcode project references the framework via aFile > Add Packages…-equivalent local path to keep builds hermetic. - The app is unsigned in Phase 0 except for the Apple-provided development profile used by CI simulators.
Two GitHub Actions workflows land in Phase 0. mobile-release.yml
is committed but unused until Phase 1 tags mobile-v0.1.0.
All workflows live at .github/workflows/mobile-*.yml (workflow
files cannot live under mobile/) and scope their paths: filters
to mobile/**, .github/workflows/mobile-*.yml, and
plans/phase-0-foundations.md so they skip unrelated changes
elsewhere in the repository. Each job sets
defaults.run.working-directory: mobile so ./gradlew invocations
resolve against the nested Gradle root.
Runs on every push and pull request that touches mobile/**.
Jobs:
lint—./gradlew ktlintCheck detekt, plusswiftlintfor the iOS host.shared-unit—./gradlew :shared:allTestson Ubuntu. RunscommonTestagainst the JVM target andandroidUnitTest.android-build—./gradlew :androidApp:assembleDebugon Ubuntu. Uploads the APK as a workflow artifact for smoke testing.ios-build—xcodebuild -scheme iosApp -destination 'generic/platform=iOS Simulator'onmacos-latest, run frommobile/iosApp/. Depends on a cachedXCFrameworkproduced by./gradlew :shared:assembleXCFramework.shared-ios-test—./gradlew :shared:iosSimulatorArm64Testonmacos-latest. Runs theiosTestsource set (secure store contract tests, serialization round-trips).
Caches:
- Gradle and Kotlin/Native caches keyed on
libs.versions.toml. - Xcode DerivedData keyed on the KMP version and the iOS deployment target.
Secrets: none in Phase 0. Signing, notarization, and Play Publisher credentials are added in Phase 1 alongside the first real release.
Triggered workflow_dispatch plus a repository_dispatch from the
main phpMyFAQ/phpMyFAQ repo on tagged releases. Steps:
- Download the tagged
docs/openapi.yamlfrom the main repo. - Copy it to
mobile/spec/openapi/v3.2.yamland updatemobile/spec/openapi/VERSION. - Run
mobile/scripts/generate-api-client.sh. - Open a pull request against
mainwith the diff. A human reviews before merging.
Phase 0 establishes the patterns; Phase 1 fills them out.
- Fixture directory:
shared/src/commonTest/resources/fixtures/holds JSON captures from a real phpMyFAQ4.2.0dev install. At minimum, a/metaresponse lands here in Phase 0. - Ktor
MockEngine: all Phase 0 tests exercise the generated client againstMockEngineresponses constructed from the fixture directory. - Secure storage contract test: an abstract test in
commonTestis implemented byandroidUnitTestandiosTest, both running the same assertions against their native backend. - Serialization round-trip test: every DTO generated so far is
round-tripped through
Jsonto catch forward-compat regressions.
Four docs land with Phase 0:
mobile/README.md— mobile-tree summary, link tomyfaq.app, one-line build instructions per platform. The repository-rootREADME.md(if introduced) only points at this file.docs/mobile/architecture.md— module diagram, data flow, and theexpect/actualcontract surface.docs/mobile/build.md— prerequisites (JDK, Android SDK, Xcode),mobile/scripts/bootstrap.shusage, how to run tests, how to regenerate the API client.docs/mobile/phase-0-handoff.md— exact tool versions pinned, decisions recorded, and a checklist of everything Phase 1 can assume is in place.
- HTTPS-only HTTP client (
http://is rejected outside a dev flavor). - No credentials stored anywhere yet, but the wrappers that will hold them are tested and working.
- The encrypted database is created on first launch with a passphrase
that never leaves
SecureStore. - No analytics SDKs. No crash reporter. No telemetry. The first bytes
leaving the device are the
MockEngine-mocked calls in the placeholder screen. Entitlements.isPro()isfalse. No billing code paths exist.
- KMP tooling churn: Kotlin and KMP release cadence is aggressive.
Mitigation: pin every version in
libs.versions.toml, upgrade on an explicit ticket, never on a whim. - Generated client drift: the Kotlin generator's output can change
subtly between generator versions. Mitigation: pin the generator
version, commit the generated sources, and review the PR produced by
mobile-openapi-sync.yml. - iOS Keychain edge cases (first unlock, device restore): the contract test covers the happy path only; Phase 1 adds Keychain access-group and first-unlock cases when real credentials land.
- SQLCipher + SQLDelight integration sometimes lags the latest SQLDelight release. Mitigation: pin the SQLDelight version used in Phase 0 to a combination that has a known-good SQLCipher driver.
Phase 0 is done when all of the following are true:
- The layout above is present on
mainofphpMyFAQ/MyFAQunder themobile/directory, without disturbing the existingplans/orLICENSE. - A freshly cloned checkout builds on macOS and Linux with no manual
fix-ups, driven only by
mobile/scripts/bootstrap.shand./gradlew(run frommobile/). - Both host apps launch, pull
/metafromMockEngine, and render the version string. - The SQLDelight database is created encrypted on first launch, and
a round-trip
INSERT/SELECTagainstinstancessucceeds in a test. - The
SecureStorecontract tests pass on both platforms. Entitlements.isPro()returnsfalseon both platforms and is covered by a test.- CI is green on
mainfor three consecutive runs. docs/mobile/phase-0-handoff.mdis merged and lists the pinned tool versions.- The repository is tagged
mobile-v0.0.0-foundationsas the Phase 0 baseline.
Anything not on this list is out of scope for Phase 0 and belongs to Phase 1 or later. Resist the urge to add "just one more" screen, DAO, or endpoint — the point of Phase 0 is that every later phase starts from an identical, boring baseline.
Phase 1 can assume:
- A working generated client for phpMyFAQ
v3.2, with a hand-written wrapper interface. - A working encrypted database with a schema migration framework.
- A working secure-storage abstraction on both platforms.
- A working Koin graph shared by both hosts.
- A working CI pipeline that will run lint, unit tests, and debug builds for any new code added on top.
- A single
Entitlements.isPro()gate that can be flipped by the UI and will automatically start returning real values in Phase 3.
Phase 1 adds: the Workspaces screen, the real add-instance flow
(backed by /meta against a live instance), the Categories and FAQ
list screens, and the server-backed search path.