A Kotlin Multiplatform app with Firebase Authentication, Firestore, Realtime Database signaling, and Agora video/audio calling — all from a shared codebase using Compose Multiplatform and the experimental SwiftPM integration for iOS.
Experimental Notice This project uses several cutting-edge features:
- Kotlin SwiftPM Import — Consuming Swift Package Manager dependencies directly from Kotlin/Native (experimental)
- Kotlin 2.3.x (Titan) — Pre-release Kotlin build (
2.3.20-titan-222)
This is a proof-of-concept and learning resource — not a production template.
- Email/password sign up & sign in
- Google Sign-In (Credential Manager on Android, Google Sign-In SDK on iOS)
- Password reset via email
- Auth state persistence across app launches
- User listing via Cloud Firestore
- Video & audio calling via Agora RTC
- In-app call signaling via Firebase Realtime Database (no push notifications needed)
- Incoming call overlay with accept/reject
- Auto-timeout on unanswered calls (30s)
- Shared ViewModel & UI across platforms
- Dark-themed UI with animations
1. Caller taps call button
└─► Writes invite to /calls/{receiverUid} in Realtime DB
└─► Joins Agora channel
2. Receiver's app has real-time listener on /calls/{myUid}
└─► Shows full-screen IncomingCallOverlay (accept / reject)
3. Accept → Joins same Agora channel → Both connected
Reject → Clears invite from Realtime DB
Timeout → Auto-clears after 30 seconds
4. Channel name = sorted(callerUid, receiverUid).joinToString("_")
└─► Deterministic — same channel regardless of who calls whom
All platform-specific code uses Kotlin's expect/actual mechanism:
| Common (expect) | Android (actual) | iOS (actual) |
|---|---|---|
FirebaseAuth |
Firebase SDK + Credential Manager | FIRAuth + GIDSignIn via SwiftPM |
FirebaseFirestoree |
FirebaseFirestore SDK | FIRFirestore via SwiftPM |
CallSignaling |
Firebase Realtime Database SDK | FIRDatabase via SwiftPM |
CallService |
CallActivity with Agora RTC | UIViewController with Agora RTC |
| Layer | Technology |
|---|---|
| Shared UI | Compose Multiplatform 1.10.0 + Material3 |
| Shared Logic | Kotlin 2.3.x, Coroutines, ViewModel, StateFlow |
| Auth | Firebase Auth (Android SDK / iOS SwiftPM) |
| Database | Cloud Firestore (user data) + Realtime DB (call signaling) |
| Calling | Agora RTC SDK 4.6.2 (video + audio) |
| iOS Deps | SwiftPM (Firebase, GoogleSignIn, Agora) |
| Architecture | expect/actual, callbackFlow, StateFlow |
composeApp/src/
├── commonMain/ # Shared code
│ ├── App.kt # Navigation + global incoming call overlay
│ ├── AuthViewModel.kt # Auth state management (StateFlow)
│ ├── AuthUser.kt # User data class
│ ├── FirebaseAuth.kt # expect — auth interface
│ ├── FirebaseFirestoree.kt # expect — Firestore interface
│ ├── CallService.kt # expect — start video/audio calls
│ ├── CallSignaling.kt # expect — Realtime DB signaling + CallInvite model
│ └── ui/
│ ├── LoginScreen.kt
│ ├── SignUpScreen.kt
│ ├── HomeScreen.kt # User list with call buttons
│ ├── IncomingCallOverlay.kt # Full-screen accept/reject UI
│ └── AuthComponent.kt
├── androidMain/ # Android implementations
│ ├── FirebaseAuth.kt # Credential Manager + Firebase Auth
│ ├── FirebaseFirestoree.kt # FirebaseFirestore SDK
│ ├── CallSignaling.kt # Firebase Realtime Database
│ ├── CallService.kt # Launches CallActivity
│ ├── CallActivity.kt # Agora RTC + programmatic UI
│ └── MainActivity.kt
└── iosMain/ # iOS implementations
├── FirebaseAuth.kt # GIDSignIn + FIRAuth via SwiftPM
├── FirebaseFireStoree.kt # FIRFirestore via SwiftPM
├── CallSignaling.kt # FIRDatabase via SwiftPM
├── CallService.kt # Agora RTC + AgoraRtcEngineDelegate
└── MainViewController.kt
iosApp/ # Xcode project entry point
├── iosApp/
│ ├── iOSApp.swift # FirebaseApp.configure() + Google Sign-In URL handler
│ ├── ContentView.swift # Compose-to-UIViewController bridge
│ └── Info.plist # Permissions (camera, mic) + Google client ID
└── _internal_linkage_SwiftPMImport/ # Auto-generated SwiftPM linkage
- Android Studio (with KMP plugin)
- Xcode 16+
- A Firebase project with:
- Android app registered (package:
dev.jasmeetsingh.firebaseauth) - iOS app registered
- Authentication > Email/Password enabled
- Authentication > Google enabled
- Cloud Firestore database created
- Realtime Database created (for call signaling)
- Android app registered (package:
- An Agora account with an App ID
git clone <repo-url>Open the project in Android Studio (with KMP plugin installed).
- Go to Firebase Console → Create a project
- Register an Android app with package name
dev.jasmeetsingh.firebaseauth- Download
google-services.json→ place incomposeApp/
- Download
- Register an iOS app with bundle ID
dev.jasmeetsingh.firebaseauth.FirebaseAuthApp- Download
GoogleService-Info.plist→ place iniosApp/iosApp/
- Download
- Enable these in Firebase Console:
- Authentication → Sign-in method → Email/Password ✅
- Authentication → Sign-in method → Google ✅
- Cloud Firestore → Create database (test mode is fine to start)
- Realtime Database → Create database → use these rules:
{
"rules": {
"calls": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null"
}
}
}
}- Go to Agora Console → Create a project
- Choose "Testing mode: App ID" (no certificate)
- Copy the App ID and update it in:
composeApp/src/commonMain/kotlin/.../CallService.kt
const val AGORA_APP_ID = "your-agora-app-id-here"4a. Update Info.plist (iosApp/iosApp/Info.plist):
Open your downloaded GoogleService-Info.plist and copy these values:
| Info.plist key | Value from GoogleService-Info.plist |
|---|---|
GIDClientID |
CLIENT_ID |
CFBundleURLSchemes |
REVERSED_CLIENT_ID |
Camera & mic permissions are already in the template.
4b. Set your Team ID in iosApp/Configuration/Config.xcconfig:
TEAM_ID=YOUR_APPLE_TEAM_ID
(Find it in Xcode → Settings → Accounts → your team)
4c. SwiftPM linkage (one-time, after first Gradle sync):
XCODEPROJ_PATH='iosApp/iosApp.xcodeproj' ./gradlew :composeApp:integrateLinkagePackageThen resolve packages:
xcodebuild -resolvePackageDependencies -project iosApp/iosApp.xcodeproj -scheme iosAppAndroid:
Select the Android run configuration in Android Studio → Run, or:
./gradlew :composeApp:assembleDebugiOS:
Open iosApp/iosApp.xcodeproj in Xcode → select your real device → ⌘R
Or use the iOS run configuration in Android Studio.
Note: iOS simulator on Intel Macs is not supported with SwiftPM dependencies. Use a real device.
| Problem | Fix |
|---|---|
PRODUCT_NAME collision with Firebase module names |
Rename in Config.xcconfig to FirebaseAuthApp (already done) |
Missing package product '_internal_linkage_SwiftPMImport' |
Run xcodebuild -resolvePackageDependencies |
integrateLinkagePackage needed |
Run after adding/changing SwiftPM deps in build.gradle.kts |
Intel Mac — iosX64() fails with SwiftPM |
Don't add iosX64(). Use a real iOS device instead |
| Agora "no token" mode | Fine for testing. For production, enable token auth in Agora Console |
xcrun: error: missing DEVELOPER_DIR |
Run sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer |
| iOS buttons not working after a while | Kotlin/Native GC — ensure strong references to handlers (already handled) |