-
Notifications
You must be signed in to change notification settings - Fork 3
firebase SETUP
End-to-end manual integration. For AI-assisted setup, use /sync-firebase-analytics.
# gradle/libs.versions.toml
[versions]
cmp-firebase = "1.0.0"
[libraries]
cmp-firebase = { module = "io.github.mobilebytelabs:cmp-firebase", version.ref = "cmp-firebase" }// build.gradle.kts (KMP shared module)
commonMain.dependencies {
implementation(libs.cmp.firebase.analytics)
}That's the only Gradle dependency. The library auto-bundles Ktor + GitLive Firebase Analytics + multiplatform-settings transitively.
Every consuming project provides its own Firebase credentials. The library bakes in none.
All apps in the workspace stream into ONE Firebase project (prod-applications-c7f87)
linked to ONE GA4 property. So the analytics/crash engine is configured with just two
org-level secrets — the single global analytics point — and each app only supplies its own
project-level stream identity. Every secret maps to a vault alias (canonical naming SoT:
core/registries/SECRETS_ORG_ALIAS_CANONICAL.yaml; resolve with /secrets pull,
RULE-SECRETS-VAULT-001 — never .env, never gh secret set):
| Scope | Secret | Vault alias | Category | Used by |
|---|---|---|---|---|
| ORG (workspace) | Firebase / GCP service-account JSON |
<ws>-firebase-sa (e.g. mbs-firebase-sa) |
google_services_sa |
growth GA4 Data API read · Firebase Management API · app discovery |
| ORG (workspace) | Shared GA4 property id (numeric, e.g. 473327398) |
<ws>-ga4-property-id (e.g. mbs-ga4-property-id) |
env_var |
growth fetch (slices per-app by streamId) |
| PROJECT (per app) | Android google-services.json
|
<proj>-firebase-google-services |
firebase_config_android |
native Android SDK init |
| PROJECT (per app) | iOS/macOS GoogleService-Info.plist
|
<proj>-firebase-ios-plist |
firebase_config_ios |
native Apple SDK init |
| PROJECT (per app) | Firebase app id (android / ios) |
<proj>-firebase-android-app-id · <proj>-firebase-ios-app-id
|
env_var |
per-app identity in the shared project |
| PROJECT (per app) | GA4 measurement id (G-XXXXXXXX, per data stream) |
<proj>-ga4-measurement-id |
env_var |
MP tier (jvm/linux/mingw) |
| PROJECT (per app) | Measurement-Protocol API secret (per stream) | <proj>-mp-api-secret |
env_var |
MP tier |
Property id vs measurement id — the property id is the org-shared numeric GA4 property (
<ws>-ga4-property-id); the measurement id is the per-app data-stream keyG-XXXX(<proj>-ga4-measurement-id). They are different values at different scopes — don't conflate. The native SDK reads its stream from the per-app config file (google-services.json / plist); only the MP tier passes the measurement id + api secret explicitly viaMpConfig.
Automatic resolution — the tooling knows where to look. Each alias carries a machine-readable
scope (workspace | project) in SECRETS_ALIAS_REGISTRY.yaml, and the resolver reports it:
core/scripts/secrets-resolve-path.sh mbs-firebase-sa --emit tier # → workspace
core/scripts/secrets-resolve-path.sh mbs-ga4-property-id --emit tier # → workspace
core/scripts/secrets-resolve-path.sh <proj>-firebase-google-services --emit tier # → projectSo /secrets pull and every consumer route automatically — workspace-tier secrets are ONE
shared value (direct-wired from the vault, never duplicated per project); project-tier secrets
materialize into the bound project's source/<repo>/secrets/live/…. Nothing hard-codes a path or a
project name — the alias's scope decides. (Single GA4: all apps push project-wise via their own data
stream, then platform-wise via kmp_platform, into the one org property.)
Call FirebaseKit.initialize() once at app startup, before any event logging. Android auto-initializes via a ContentProvider — no explicit call needed there. All other platforms:
import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseKit
import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseConfig
import dev.gitlive.firebase.FirebaseOptions
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig
// No-arg: each platform reads its own config file (plist / google-services.json)
FirebaseKit.initialize()
// OR — builder form: required for JS/web; also used to pass MpConfig for non-Firebase targets
FirebaseKit.initialize(
FirebaseConfig.builder()
.android(FirebaseOptions(/* optional override */))
.apple(FirebaseOptions(/* optional override */))
// `web` serves BOTH the `js` AND `wasmJs` targets — see the wasmJs note below.
.web(FirebaseOptions(
apiKey = "YOUR_WEB_API_KEY",
applicationId = "YOUR_APP_ID",
projectId = "your-firebase-project",
authDomain = "your-firebase-project.firebaseapp.com",
))
.measurementProtocol(MpConfig(measurementId = "G-XXXX", apiSecret = "..."))
.build()
)
// Optional: global fatal-exception capture (real on JVM/Android; no-op on native/JS/wasm)
FirebaseKit.installUncaughtHandler()Crash → GA4: every captured crash emits an
app_crashGA4 event withkmp_platform,exception_type, andfatalparams — visible in the same GA4 property and BigQuery export across all platforms. On the non-Firebase tier this requiresMpConfigto be set (otherwise no-op).
-
Firebase Console → Project Settings → General → Your apps → Android → Download
google-services.json -
Drop into
androidApp/google-services.json -
Apply the plugin in
androidApp/build.gradle.kts:plugins { id("com.google.gms.google-services") } -
Add Firebase BoM + Analytics SDK:
dependencies { implementation(platform("com.google.firebase:firebase-bom:32.0.0")) implementation("com.google.firebase:firebase-analytics") }
Pick one. Path A is the library's own commonMain surface and needs no plist and no Swift code; Path B is the classic native-config route. Do not do both.
Pass apple options to FirebaseKit.initialize(config) from commonMain. Firebase is configured
programmatically, so there is no GoogleService-Info.plist and no FirebaseApp.configure()
line at all:
FirebaseKit.initialize(
FirebaseConfig.builder()
.apple(FirebaseOptions(
applicationId = "1:123:ios:abc", // required
apiKey = "YOUR_API_KEY", // required
projectId = "your-firebase-project",
gcmSenderId = "123", // required by Apple's native FIROptions
))
.build()
)
gcmSenderIdis not optional on Apple — the nativeFIROptionsconstructor requires it. Values come from the same Firebase Console screen that would have produced the plist.
Use the no-arg FirebaseKit.initialize() and let the platform read its own config file:
-
Firebase Console → Project Settings → Your apps → iOS → Download
GoogleService-Info.plist -
Drop into the corresponding app target (drag into Xcode project)
-
In your
@main App(or AppDelegate):import FirebaseCore @main struct MyApp: App { init() { FirebaseApp.configure() } // ... }
No Podfile. From GitLive 3.0.0 the native firebase-ios-sdk is linked via SwiftPM, not
CocoaPods, and it flows across the Maven boundary automatically — do not re-declare it.
-
Requires Kotlin 2.4.20+ (the version this library is built and verified against). The transitive SwiftPM resolution is a Kotlin 2.4 feature. On an older Kotlin no SwiftPM package is generated, nothing resolves the native SDK, and the build fails with
ld: framework 'FirebaseCore' not foundinstead of a message naming the cause. -
Recommended: apply
id("io.github.mobilebytelabs.firebase")(same version as cmp-firebase, published to Maven Central) — it forces static Apple frameworks and enforces the Kotlin floor for you, so the two manual steps below are handled. -
Build your shared framework static:
iosArm64().binaries.framework { isStatic = true }(Firebase's SwiftPM products are static libraries; a dynamic framework crashes at runtime). -
In Xcode use direct integration — add the
embedAndSignAppleFrameworkForXcoderun-script build phase. This replacespod install. -
Set the deployment target to iOS 15.0 / macOS 10.15 / tvOS 15.0 (
firebase-ios-sdk12.x minimum).
Full steps: cmp-firebase/README.md.
Pass a web FirebaseOptions via FirebaseKit.initialize(config) (the cleaner library-level path); Firebase.initialize(options=...) from GitLive also works as a lower-level alternative.
// jsMain — typically in your app entry point
import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseKit
import io.github.mobilebytelabs.kmptoolkit.firebase.FirebaseConfig
import dev.gitlive.firebase.FirebaseOptions
FirebaseKit.initialize(
FirebaseConfig.builder()
.web(FirebaseOptions(
apiKey = "YOUR_API_KEY",
applicationId = "YOUR_APP_ID",
projectId = "your-firebase-project",
// ... see Firebase Console → Project settings → Your web app
))
.build()
)From GitLive
3.0.0-alpha02(KmpToolkit 3.5.21+),wasmJsis no longer a Measurement-Protocol target. Upstream PR #832 gives wasmJs full parity with the JS target, backed by the same Firebase JS SDK, socmp-firebasepromoteswasmJsMainontofirebaseMain.What you must change: a wasmJs app that previously supplied only
measurementProtocolmust now also supplyweb(apiKey,applicationId,projectId,authDomain).wasmJsreads the sameFirebaseConfig.webentry asjs— there is no separatewasmJsentry.If you skip it: native init is silently skipped and analytics NoOps — you will not get an exception at config time, so this fails quietly. Supply
web(or keep the target off the tier by pinninggitliveFirebasebelow3.0.0-alpha02).Unchanged: Crashlytics.
firebase-crashlyticsdid not gainwasmjsupstream, so wasmJs stays oncrashlyticsFallbackMain(LoggingCrashReporter).measurementProtocolis still required for jvm / linux / mingw.Every section of this guide has been updated for this change (2026-09-03).
GitLive doesn't ship usable native Firebase Analytics for jvm, linuxX64, linuxArm64, or mingwX64. For event capture on these 4 targets, use Firebase Measurement Protocol — see §7 below.
wasmJswas on this tier before GitLive3.0.0-alpha02; it is now a native Firebase target and readsFirebaseConfig.webinstead.
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.AnalyticsHelper
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.PerformanceTracker
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.di.AnalyticsModule
import io.github.mobilebytelabs.kmptoolkit.firebase.crash.di.CrashReporterModule
val firebaseModule = module {
single<AnalyticsHelper> {
AnalyticsModule.analyticsHelper(
mode = if (BuildConfig.DEBUG) AnalyticsModule.Mode.Stub
else AnalyticsModule.Mode.Firebase,
config = null, // pass MpConfig here for non-Firebase targets (see §7)
)
}
single { AnalyticsModule.performanceTracker(get()) }
single {
CrashReporterModule.crashReporter(
mode = if (BuildConfig.DEBUG) CrashReporterModule.Mode.Stub
else CrashReporterModule.Mode.Firebase
)
}
}
// Add to startKoin
startKoin {
modules(firebaseModule, /* ... */)
}AnalyticsModule.analyticsHelper() is a shared process singleton via provideAnalyticsHelper() — returns FirebaseAnalyticsHelper on firebaseMain platforms (Android, iOS, macOS, tvOS, JS), NoOpAnalyticsHelper on the non-Firebase tier (until you wire MpConfig — see §7).
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.*
class SettingsViewModel(private val analytics: AnalyticsHelper) {
init {
analytics.logScreenView("settings", sourceScreen = "home")
}
fun onSaveClick() {
analytics.logButtonClick("save", screenName = "settings")
}
fun onError(msg: String) {
analytics.logError(msg, errorCode = "E001", screen = "settings")
}
}analytics.logEvent(EventTypes.BUTTON_CLICK,
ParamKeys.BUTTON_NAME to "save",
ParamKeys.SCREEN_NAME to "settings",
)
analytics.logStateTransition("settings", from = "loading", to = "content")analytics.log(EventTypes.FORM_COMPLETED) {
param(ParamKeys.FORM_NAME, "registration")
param(ParamKeys.COMPLETION_TIME, 45) // numbers auto-stringified
}val tracker: PerformanceTracker = koinInject()
tracker.measure("settings_screen_render") {
// render work — emits loading_time event with duration_ms
}// User attributes for segmentation
analytics.setUserProperty("user_type", "premium")
analytics.setUserProperty("preferred_language", "en")
// User ID — MUST be hashed/obfuscated. NEVER raw email/phone.
analytics.setUserId(hashedUserId)
// Clear on logout
analytics.setUserId("")Firebase constraints (auto-truncated):
- User property name: ≤ 24 chars
- User property value: ≤ 36 chars
- User ID: ≤ 256 chars
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.TestAnalyticsHelper
@Test fun `clicking save logs button_click event`() {
val analytics = TestAnalyticsHelper()
val viewModel = SettingsViewModel(analytics)
viewModel.onSaveClick()
val event = analytics.events.single()
assertEquals(EventTypes.BUTTON_CLICK, event.type)
assertEquals("save", event.extras.first { it.key == ParamKeys.BUTTON_NAME }.value)
// Convenience assertions
assertEquals(1, analytics.countOf(EventTypes.BUTTON_CLICK))
assertEquals("save", analytics.lastOf(EventTypes.BUTTON_CLICK)?.extras?.first()?.value)
}GitLive doesn't ship usable native Firebase Analytics on these 4 targets: jvm, linuxX64, linuxArm64, mingwX64. Use Firebase Measurement Protocol over HTTP to land events in the same Firebase property + same BigQuery export.
wasmJsis no longer in this list. GitLive3.0.0-alpha02gave it full JS parity, so it runs the native Firebase JS SDK and needsFirebaseConfig.web. Crashlytics is the exception — upstream did not addwasmjsthere, so wasmJs still uses the logging fallback for crash reporting.
A short string token (typically 22 chars, looks like abc1d2e3f4-XYZ_a8B7c6D5e4F3g2H) that authenticates HTTP POSTs to Google's Measurement Protocol endpoint. It is the only Firebase credential that authorizes write-events-via-HTTP to a specific GA4 data stream — and it is the least-privileged Firebase credential (can't read analytics, can't admin, can't access other Firebase services).
Native Firebase SDKs (Android/iOS/JS) authenticate via google-services.json / GoogleService-Info.plist / Firebase Web Config. Those credentials don't reach MeasurementProtocolAnalyticsHelper — the HTTP path needs its own token, which is the MP API secret.
Step-by-step (Firebase Console UI):
- Open Firebase Console → click your project
- Click the gear icon (top-left, next to "Project Overview") → Project Settings
- Click the Integrations tab
- Find the Google Analytics card → click Manage (or Open in GA4)
- In the GA4 admin pane, navigate: Admin → Data Streams
- Click your stream — pick the one that matches the platform:
-
Web stream → for browser deploys (
js; andwasmJspre-alpha02) - Android stream → not relevant here (Android uses native SDK)
- For
jvm/linuxX64/linuxArm64/mingwX64, use whichever stream represents your "desktop" presence (often Web)
-
Web stream → for browser deploys (
- Scroll to Measurement Protocol API secrets (near the bottom of the page)
- Click Create
- Give it a descriptive name (e.g.,
jvm-prod,linux-staging) - Copy the secret value immediately — it is shown ONCE. If you lose it, you create a new one.
Recommended (vault-first): load from the org vault via /secrets pull — never commit raw values.
# Materialize org-scoped secrets locally (per RULE-SECRETS-VAULT-001 — no .env, no gh secret set)
/secrets pull
# Vault aliases used by cmp-firebase (org-global analytics, see §2 table):
# mbs-firebase-sa → Firebase / GCP service-account JSON (ORG / workspace)
# mbs-ga4-property-id → shared GA4 PROPERTY id, numeric e.g. 473327398 (ORG / workspace)
# <proj>-ga4-measurement-id → this app's GA4 stream MEASUREMENT id, G-XXXXXXXX (PROJECT)
# <proj>-mp-api-secret → this app's Measurement-Protocol API secret (PROJECT)The SAME GA4 property the library writes to (via native SDK or MP) is what the framework's growth dashboard reads via the GA4 Data API — so
app_crashevents, retention, and per-platform engagement all appear in one BigQuery export once the stream is enabled.
Fallback (e.g., initial local dev before vault onboarding):
# release-layer/.env (gitignored — verify it is in .gitignore)
MP_API_SECRET=abc123...# idea-layer/PROJECT_CONFIG.yaml
analytics:
envs:
prod:
property_id: G-XXXXXXXX # GA4 measurement ID (not a secret)
measurement_protocol:
api_secret_secret_ref: MP_API_SECRET # env var name; resolved at runtimeimport com.russhwolf.settings.Settings
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.AnalyticsHelper
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MeasurementProtocolAnalyticsHelper
import io.github.mobilebytelabs.kmptoolkit.firebase.analytics.mp.MpConfig
val analyticsModule = module {
single<AnalyticsHelper> {
// Pick per-platform helper:
// - firebaseMain platforms: provideAnalyticsHelper() returns FirebaseAnalyticsHelper
// - nonFirebaseMain platforms (jvm/linux/mingw): wire MP explicitly
if (BuildConfig.DEBUG) {
StubAnalyticsHelper()
} else {
MeasurementProtocolAnalyticsHelper(
config = MpConfig(
measurementId = "G-XXXXXXXX", // GA4 measurement ID
apiSecret = SecureStore.read("MP_API_SECRET"), // your secrets store
),
settings = Settings(), // multiplatform-settings
)
}
}
}MeasurementProtocolAnalyticsHelper accepts the SAME AnalyticsHelper interface — your ViewModels don't change. Only the DI wiring differs per platform.
-
Use the vault: load via
/secrets pull(aliasesmbs-firebase-sa,mbs-ga4-property-id,<proj>-mp-api-secret). Never.envas primary, nevergh secret set(RULE-SECRETS-VAULT-001). -
Never commit the secret value.
release-layer/.envmust be in.gitignore(fallback only). - Never log it. The library never prints it; you shouldn't either.
-
Rotate if it leaks: Firebase Console → revoke the old secret → create a new one →
/secrets pull→ redeploy. - Per environment: create separate secrets for prod/staging/dev. Don't share across environments.
-
Per project: each Firebase project has its own MP secrets.
mood-movies's secret won't work forreels-downloader. -
CI:
/secrets pullin CI via the vault. Never hard-code inbuild.gradle.ktsor YAML.
| Credential | Used by | Where it lives | What it does |
|---|---|---|---|
google-services.json |
Native Android Firebase SDK | androidApp/ |
Auto-config: API key, app ID, project ID, sender ID |
GoogleService-Info.plist |
Native iOS/macOS/tvOS Firebase SDK | Xcode project | Same, for Apple platforms |
| Firebase Web Config object | Firebase JS SDK |
jsMain init |
Same, for browser |
| Firebase Service Account JSON | Server-side admin (Crashlytics fetch via /idea firebase-crash, etc.) |
secrets/firebaseAppDistributionServiceCredentialsFile.json |
High-privilege; signs JWTs for any Firebase API |
| MP API secret | HTTP POST /mp/collect only |
release-layer/.env |
Only authorizes writing events to one specific GA4 data stream |
The MP secret is the least powerful credential in this list. That's intentional — least-privilege for an HTTP fallback.
| Feature | firebaseMain (GitLive) | nonFirebaseMain (MP HTTP) |
|---|---|---|
| Custom event capture | ✅ | ✅ |
| User properties + user ID | ✅ | ✅ |
| Persistent client_id | ✅ from GitLive | ✅ via multiplatform-settings (Apple/JS); in-memory on Linux/mingw |
| Async batching | ✅ native | ✅ 5s/25-event debounce |
| BigQuery export | ✅ | ✅ same dataset |
| DebugView | ✅ | ❌ |
Automatic events (first_open, session_start, in_app_purchase) |
✅ | ❌ — manual log if needed |
| A/B Testing tie-in | ✅ | ❌ |
| Demographics inference | ✅ | ❌ |
| Latency to BigQuery | ~1h | ~1h |
The library does NOT auto-redact, but provides primitives to enforce privacy:
-
pii: truein screen YAML (claude-product-cycle framework) → codegen NEVER auto-instruments -
EventValidator— debug-build regex check for email/phone/SSN/credit-card patterns in param values -
setUserId(hashedUserId)— never pass raw PII; hash client-side first
Recommended boundaries:
// User ID — always hashed
analytics.setUserId(sha256(rawUserId).take(16))
// Event params — never raw user input. Categorize first.
analytics.logEvent(EventTypes.SEARCH_PERFORMED,
ParamKeys.RESULT_COUNT to results.size.toString(),
// DO NOT: ParamKeys.SEARCH_TERM to userInput ← raw user input may include PII
)For apps with strict event taxonomy, install a registry to reject unregistered events at runtime:
val SettingsRegistry = EventRegistry(
scope = "settings",
events = setOf(
"settings_screen_viewed",
"settings_save_clicked",
),
)
val analytics: AnalyticsHelper = if (BuildConfig.DEBUG) {
RegistryValidatingHelper(realHelper, SettingsRegistry) { violation ->
Logger.w { "Analytics: $violation" }
}
} else {
realHelper
}Production builds skip the wrapper for zero overhead.
** Partials**
App Intents
Bubble
Clipboard
Cookbook
- Clipboard Copy Text
- Clipboard Read Text
- Consumer Anon Key Setup
- Crashlytics Attribution Per Library
- Ifonline Block
- Index
- Index
- Index
- Index
- Open Url Compose
- Pick And Share Image
- React To Offline
- Register Firebase Hooks
- Share Pdf Android
- Share Text
- Wifi Vs Cellular
Firebase
In App Update
Intent Launcher
Inter App Comms
Modules
- Cmp App Intents
- Cmp App Intents Compose
- Cmp Bubble
- Cmp Clipboard
- Cmp Deep Link
- Cmp Firebase
- Cmp In App Update
- Cmp Intent Launcher
- Cmp Intent Launcher Compose
- Cmp Library
- Cmp Network Monitor
- Cmp Network Monitor Compose
- Cmp Observe
- Cmp Observe Koin
- Cmp Open Url
- Cmp Pdf Generator
- Cmp Product Tickets
- Cmp Remote Config
- Cmp Share
- Cmp Share Compose
- Cmp Toast
Network Monitor
Open Url
Pdf Generator
Remote Config
Share
Superpowers
Toast
User Tickets
General