<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged firebase</title>
<link>https://coderlegion.com/tag/firebase</link>
<description>Powered by Question2Answer</description>
<item>
<title>Telemetry: Shared Analytics and Crash Reporting in Kotlin Multiplatform</title>
<link>https://coderlegion.com/30479/telemetry-shared-analytics-and-crash-reporting-in-kotlin-multiplatform</link>
<description>&lt;p&gt;In a modern endurance car, every sensor talks to the same box. Tyre pressure, brake temperature, fuel flow, throttle trace. They all end up in one data stream that goes to the pit wall over a single radio link. The engineer on the wall doesn't care which supplier made the brake sensor. He cares that the channel is called &lt;code&gt;BRAKE_TEMP_FL&lt;/code&gt; and that the number arrives every 100 milliseconds.&lt;/p&gt;
&lt;p&gt;Swap a sensor, the channel name stays. That's the whole point of the standard.&lt;/p&gt;
&lt;p&gt;Analytics and crash reporting in a KMP app should work the same way. The sensors are the SDKs, one per platform, made by different teams with different APIs. The channel names are yours.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-had-before&quot;&gt;What I had before&lt;/h3&gt;
&lt;p&gt;My first KMP app logged analytics twice. Once in Kotlin for Android, once in Swift for iOS. Same events, same names, in theory.&lt;/p&gt;
&lt;p&gt;In practice, the Android side sent &lt;code&gt;screen_view&lt;/code&gt; with a &lt;code&gt;screen_name&lt;/code&gt; parameter. The iOS side sent &lt;code&gt;screenView&lt;/code&gt; with &lt;code&gt;screenName&lt;/code&gt;. Nobody noticed for six weeks, until I tried to build a funnel in the dashboard and half the users were missing from it. The numbers weren't wrong. They were split across two event names that meant the same thing.&lt;/p&gt;
&lt;p&gt;That's the real cost of leaving telemetry in the platform layer. Not the duplicated code, which is small. The drift. Two implementations of the same contract, with no compiler checking that they agree.&lt;/p&gt;
&lt;p&gt;If you read &lt;a rel=&quot;nofollow&quot; href=&quot;/posts/feature-flags-kmp&quot;&gt;Change the Map&lt;/a&gt;, this is the same move applied to a different problem: the contract lives in &lt;code&gt;commonMain&lt;/code&gt;, the SDKs sit underneath. It also pairs with flags directly, because the first event worth logging is which variant a user was actually shown.&lt;/p&gt;
&lt;p&gt;Crash reporting has a quieter version of the same problem. A Kotlin exception that crashes the Android app shows up in Crashlytics with a readable stack trace. The same exception on iOS arrives as a native crash with frames that look like &lt;code&gt;kfun:...&lt;/code&gt;, and the Kotlin type is buried in the message. I'll get to that.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-interface-lives-in-commonmain&quot;&gt;The interface lives in commonMain&lt;/h3&gt;
&lt;p&gt;The fix is to stop treating analytics as a platform feature and start treating it as a domain contract.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain
interface Analytics {
    fun track(event: AnalyticsEvent)
    fun setUserId(id: String?)
}

sealed interface AnalyticsEvent {
    val name: String
    val params: Map&amp;lt;String, String&amp;gt;

    data class ScreenView(val screen: String) : AnalyticsEvent {
        override val name = &quot;screen_view&quot;
        override val params = mapOf(&quot;screen_name&quot; to screen)
    }

    data class Purchase(val sku: String, val amountCents: Long) : AnalyticsEvent {
        override val name = &quot;purchase&quot;
        override val params = mapOf(
            &quot;sku&quot; to sku,
            &quot;amount_cents&quot; to amountCents.toString(),
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A sealed interface for events is the part that pays for itself. The event name and the parameter keys exist in exactly one place. A new event is a new class, and the compiler tells every &lt;code&gt;when&lt;/code&gt; that cares about it. A typo in &lt;code&gt;&quot;screen_name&quot;&lt;/code&gt; is no longer possible in two files, because it's only written once.&lt;/p&gt;
&lt;p&gt;I keep params as &lt;code&gt;Map&amp;lt;String, String&amp;gt;&lt;/code&gt; on purpose. Firebase, Mixpanel, and Amplitude all accept richer types, but they disagree about which ones. Strings are the lowest common denominator, and the conversion cost is trivial.&lt;/p&gt;
&lt;p&gt;The crash side is a second, separate interface.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain
interface CrashReporter {
    fun log(message: String)
    fun recordNonFatal(throwable: Throwable)
    fun setUserId(id: String?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two interfaces, not one. Analytics is about what users did. Crash reporting is about what broke. They often go to different vendors, and you'll want to swap one without touching the other.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;android-the-easy-side&quot;&gt;Android: the easy side&lt;/h3&gt;
&lt;p&gt;On Android the implementation is a thin wrapper. Firebase is the obvious choice, though nothing here is Firebase-specific.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// androidMain
class FirebaseAnalyticsImpl(
    private val firebase: FirebaseAnalytics,
) : Analytics {

    override fun track(event: AnalyticsEvent) {
        val bundle = Bundle().apply {
            event.params.forEach { (k, v) -&amp;gt; putString(k, v) }
        }
        firebase.logEvent(event.name, bundle)
    }

    override fun setUserId(id: String?) {
        firebase.setUserId(id)
    }
}

class CrashlyticsReporter(
    private val crashlytics: FirebaseCrashlytics,
) : CrashReporter {

    override fun log(message: String) = crashlytics.log(message)

    override fun recordNonFatal(throwable: Throwable) =
        crashlytics.recordException(throwable)

    override fun setUserId(id: String?) {
        crashlytics.setUserId(id.orEmpty())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing clever. If the Android side is the only one you ever shipped, this is the code you already had.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ios-the-swift-bridge&quot;&gt;iOS: the Swift bridge&lt;/h3&gt;
&lt;p&gt;The simplest iOS implementation doesn't call the Firebase iOS SDK from Kotlin at all. No &lt;code&gt;expect&lt;/code&gt;/&lt;code&gt;actual&lt;/code&gt; in this layer, no cinterop bindings, no CocoaPods dependency in the shared module.&lt;/p&gt;
&lt;p&gt;The shared module defines the interface, and the iOS app passes in a Swift implementation at startup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// iosApp
final class FirebaseAnalyticsBridge: Analytics {

    func track(event: AnalyticsEvent) {
        FirebaseAnalytics.Analytics.logEvent(event.name, parameters: event.params)
    }

    func setUserId(id: String?) {
        FirebaseAnalytics.Analytics.setUserID(id)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swift sees the Kotlin interface as a protocol, so conforming to it is ordinary Swift. The one snag is the name: Firebase's Swift class is also called &lt;code&gt;Analytics&lt;/code&gt;, so inside this file the bare name is ambiguous. I qualify the Firebase one with its module. Renaming the Kotlin interface would also work, but I'd rather not let a vendor pick my names. The app registers it with the DI container before the first screen renders.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/Telemetry.kt
fun setUpTelemetry(analytics: Analytics, crashReporter: CrashReporter) {
    // register both in your DI graph of choice
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// iosApp
@main
struct IosApp: App {
    init() {
        TelemetryKt.setUpTelemetry(
            analytics: FirebaseAnalyticsBridge(),
            crashReporter: CrashlyticsBridge()
        )
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don't start that function name with &lt;code&gt;init&lt;/code&gt;. The Objective-C export reserves &lt;code&gt;init&lt;/code&gt; for initializers, so a Kotlin function called &lt;code&gt;initTelemetry&lt;/code&gt; shows up in Swift as &lt;code&gt;doInitTelemetry&lt;/code&gt;. It works, but it's the kind of thing that costs ten minutes the first time you see it.&lt;/p&gt;
&lt;p&gt;The Firebase SDK stays where Xcode manages it. The shared module never learns it exists.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;two-other-ways-to-bring-the-sdk-in&quot;&gt;Two other ways to bring the SDK in&lt;/h3&gt;
&lt;p&gt;That Swift bridge is how I wrote this in my first KMP apps, and it's the version I'd explain to anyone starting out because every moving part is visible. It isn't what I use today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitLive's Firebase Kotlin SDK.&lt;/strong&gt; &lt;code&gt;firebase-kotlin-sdk&lt;/code&gt; from GitLive wraps Firebase so you can call it from &lt;code&gt;commonMain&lt;/code&gt;. I haven't shipped it myself. If you use it, your &lt;code&gt;Analytics&lt;/code&gt; interface still sits in front of it, otherwise you've just moved the vendor lock-in into shared code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CocoaPods through Gradle, with the call in &lt;code&gt;iosMain&lt;/code&gt;.&lt;/strong&gt; This is what I use now. The Kotlin CocoaPods plugin lets the shared module declare the Firebase pod itself, and Kotlin generates the bindings so &lt;code&gt;iosMain&lt;/code&gt; can call the iOS SDK directly.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// build.gradle.kts (shared module)
plugins {
    kotlin(&quot;multiplatform&quot;)
    kotlin(&quot;native.cocoapods&quot;)
}

kotlin {
    cocoapods {
        version = &quot;1.0&quot;
        ios.deploymentTarget = &quot;16.0&quot;
        pod(&quot;FirebaseAnalytics&quot;)
        pod(&quot;FirebaseCrashlytics&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can pin a version with &lt;code&gt;pod(&quot;FirebaseAnalytics&quot;, &quot;~&amp;gt; 12.0&quot;)&lt;/code&gt;, and &lt;code&gt;linkOnly = true&lt;/code&gt; exists for pods you only need to link and not bind. The Kotlin docs cover both.&lt;/p&gt;
&lt;p&gt;Then the iOS implementation is Kotlin, in the same module as the interface.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// iosMain
import cocoapods.FirebaseAnalytics.FIRAnalytics
import kotlinx.cinterop.ExperimentalForeignApi

@OptIn(ExperimentalForeignApi::class)
class IosAnalytics : Analytics {

    @Suppress(&quot;UNCHECKED_CAST&quot;)
    override fun track(event: AnalyticsEvent) {
        FIRAnalytics.logEventWithName(event.name, event.params as Map&amp;lt;Any?, *&amp;gt;)
    }

    override fun setUserId(id: String?) {
        FIRAnalytics.setUserID(id)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The bindings come from the Objective-C headers, not the Swift API, so the class is &lt;code&gt;FIRAnalytics&lt;/code&gt; and the method is &lt;code&gt;logEventWithName&lt;/code&gt;. The &lt;code&gt;parameters&lt;/code&gt; dictionary arrives as &lt;code&gt;Map&amp;lt;Any?, *&amp;gt;?&lt;/code&gt;. A &lt;code&gt;Map&amp;lt;String, String&amp;gt;&lt;/code&gt; doesn't fit that type as-is, because map keys are invariant in Kotlin, hence the cast.&lt;/p&gt;
&lt;p&gt;I switched because the Swift bridge had a cost I only noticed over time: two files per interface, in two languages, in two repos' worth of mental context. With the pod declared in Gradle, the iOS implementation sits next to the Android one, in Kotlin, and a new method on &lt;code&gt;Analytics&lt;/code&gt; fails the build in the same module.&lt;/p&gt;
&lt;p&gt;It has its own cost. Pod dependencies and cinterop bindings are now part of your shared module's build, so Gradle sync and iOS builds feel it, and a Firebase SDK bump can break the generated bindings. It's also the option with an expiry date: the Kotlin docs now carry a CocoaPods-to-SwiftPM migration guide, and the new &lt;code&gt;swiftPMDependencies { }&lt;/code&gt; block replaces &lt;code&gt;cocoapods { }&lt;/code&gt;. The docs I read put the minimum Kotlin Gradle plugin at 2.4.20-RC3 for it, so check your version before planning the move. That's the trade I accept for keeping everything telemetry-related in one language.&lt;/p&gt;
&lt;p&gt;I'm showing three options on purpose, and Firebase is only the example. A KMP project has more than one way to bring a native dependency in: a bridge the app injects, a KMP wrapper library, or a native dependency declared in Gradle. Which one fits depends on how much of the native SDK surface you need, and who maintains the glue.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;kotlin-exceptions-on-ios-are-not-kotlin-exceptions&quot;&gt;Kotlin exceptions on iOS are not Kotlin exceptions&lt;/h3&gt;
&lt;p&gt;This is the part nobody mentions until you're staring at a dashboard.&lt;/p&gt;
&lt;p&gt;When an uncaught Kotlin exception terminates the app on iOS, the crash is reported by the native crash handler. The stack trace is the native one. The Kotlin frames are there, but unsymbolicated Kotlin frames read as noise, and the exception type is flattened. Touchlab's CrashKiOS exists for exactly this: it sends symbolicated Kotlin crashes and handled exceptions to Crashlytics, with breadcrumbs and custom keys. Its docs page was last updated in January 2024, so check the repo for recent activity before you rely on it.&lt;/p&gt;
&lt;p&gt;For non-fatal errors you have a better option. Catch the exception at a boundary you control, and hand it to &lt;code&gt;recordNonFatal&lt;/code&gt;. On iOS that means converting it to an &lt;code&gt;NSError&lt;/code&gt;, and that's where it gets fiddly.&lt;/p&gt;
&lt;p&gt;Swift can't get much out of a &lt;code&gt;KotlinThrowable&lt;/code&gt; by itself. The Kotlin stack trace accessor on Native, &lt;code&gt;getStackTrace()&lt;/code&gt;, is a stdlib extension marked &lt;code&gt;@ExperimentalNativeApi&lt;/code&gt;, so I don't count on it being reachable from Swift. I let Kotlin do the work instead, with two small extensions in &lt;code&gt;iosMain&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// iosMain
fun Throwable.kotlinTypeName(): String =
    this::class.qualifiedName ?: &quot;UnknownKotlinThrowable&quot;

fun Throwable.kotlinStackTrace(): String = stackTraceToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They extend a Kotlin class, so the export turns them into methods on &lt;code&gt;KotlinThrowable&lt;/code&gt; and Swift calls them like any other method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// iosApp
final class CrashlyticsBridge: CrashReporter {

    func recordNonFatal(throwable: KotlinThrowable) {
        let error = NSError(
            domain: throwable.kotlinTypeName(),
            code: 0,
            userInfo: [
                NSLocalizedDescriptionKey: throwable.message ?? &quot;unknown&quot;,
                &quot;kotlin_stacktrace&quot;: throwable.kotlinStackTrace(),
            ]
        )
        Crashlytics.crashlytics().record(error: error)
    }

    func log(message: String) {
        Crashlytics.crashlytics().log(message)
    }

    func setUserId(id: String?) {
        Crashlytics.crashlytics().setUserID(id ?? &quot;&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Crashlytics turns &lt;code&gt;userInfo&lt;/code&gt; into key-value pairs in the keys section of the issue, so the full Kotlin trace sits right there. I put it there on purpose rather than hoping the dashboard parses the message, because it won't.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;domain&lt;/code&gt; choice matters more than it looks. For fatal crashes Crashlytics groups by stack trace, but the Firebase docs say logged errors are grouped by &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. A fixed domain like &lt;code&gt;&quot;kotlin&quot;&lt;/code&gt; with code &lt;code&gt;0&lt;/code&gt; would collapse every non-fatal into one issue. With the Kotlin type there, an &lt;code&gt;IllegalStateException&lt;/code&gt; and a network timeout stay separate. Keep unique values like user IDs or timestamps out of both fields, or you'll fragment the dashboard the other way. Crashlytics also only stores the most recent eight exceptions in a session, so &lt;code&gt;recordNonFatal&lt;/code&gt; is not a log.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-the-calls-go&quot;&gt;Where the calls go&lt;/h3&gt;
&lt;p&gt;The interface is only worth having if people use it in the same place. I let ViewModels own analytics events and nothing else.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain
class CheckoutViewModel(
    private val analytics: Analytics,
    private val crashReporter: CrashReporter,
    private val repository: CheckoutRepository,
) : ViewModel() {

    fun onPay(sku: String, amountCents: Long) {
        viewModelScope.launch {
            runCatching { repository.pay(sku, amountCents) }
                .onSuccess {
                    analytics.track(AnalyticsEvent.Purchase(sku, amountCents))
                }
                .onFailure {
                    if (it is CancellationException) throw it
                    crashReporter.recordNonFatal(it)
                }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;CancellationException&lt;/code&gt; check is there because &lt;code&gt;runCatching&lt;/code&gt; catches it too. Without it, every user who leaves the screen mid-payment becomes a non-fatal in Crashlytics.&lt;/p&gt;
&lt;p&gt;Composables and SwiftUI views don't call &lt;code&gt;track&lt;/code&gt;. Screen views are the exception, and I fire them from a single navigation observer, so there's one line of code that produces every &lt;code&gt;screen_view&lt;/code&gt; in the app.&lt;/p&gt;
&lt;p&gt;The other benefit shows up in tests. A &lt;code&gt;FakeAnalytics&lt;/code&gt; that stores events in a list takes ten lines, and asserting on it in &lt;code&gt;commonTest&lt;/code&gt; is trivial. I used to skip testing analytics because it meant mocking an SDK. Now I assert that tapping Pay produces exactly one &lt;code&gt;Purchase&lt;/code&gt; with the right amount, and it runs on every target.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Don't init the SDKs inside commonMain, whichever option you pick.&lt;/strong&gt; Firebase on Android wants a &lt;code&gt;Context&lt;/code&gt;. On iOS it wants &lt;code&gt;FirebaseApp.configure()&lt;/code&gt; before anything else touches it. Those calls belong in &lt;code&gt;Application.onCreate&lt;/code&gt; and the iOS &lt;code&gt;App.init&lt;/code&gt;, not behind an abstraction that pretends the platforms are the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User IDs are a privacy decision, not a plumbing one.&lt;/strong&gt; &lt;code&gt;setUserId&lt;/code&gt; is easy to wire and easy to get wrong. I pass an internal opaque ID and never an email. Deciding that once, in the interface docs, beats hoping every caller remembers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consent has to be a gate, not a flag.&lt;/strong&gt; If your users can opt out, wrap the implementation, not the call sites. A &lt;code&gt;ConsentAwareAnalytics&lt;/code&gt; decorator that drops events when consent is off keeps the rule in one class. Crash reporting is different. &lt;code&gt;setCrashlyticsCollectionEnabled&lt;/code&gt; can be called at runtime, but the Firebase docs say the override applies from the next launch, so a user who opts out mid-session is still reporting until the app restarts. Say that in your consent screen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A debug build should not talk to production.&lt;/strong&gt; I give the debug variant a &lt;code&gt;LoggingAnalytics&lt;/code&gt; that prints to the console. It's another implementation of the same interface, which is the quiet argument for having the interface at all.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Treat telemetry as a contract you own and a set of sensors you rent. The event names, the parameter keys, and the rule for what counts as a non-fatal live in &lt;code&gt;commonMain&lt;/code&gt;, in code your compiler can check. The SDKs sit underneath, one per platform, and nobody outside the platform layer should know which ones.&lt;/p&gt;
&lt;p&gt;I would still start with the interface even if the app only ships on one platform today. Adding iOS later then costs one Swift file instead of an audit of every event.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Same channel names, whatever the sensor. 🏁&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The KMP Bits app is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/pt/app/bitsreader/id6755438235&quot;&gt;App Store&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.joel.bitsreader&quot;&gt;Google Play&lt;/a&gt; — built entirely with KMP.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30479/telemetry-shared-analytics-and-crash-reporting-in-kotlin-multiplatform</guid>
<pubDate>Fri, 09 Oct 2026 14:00:00 +0000</pubDate>
</item>
<item>
<title>How to Fix Firebase `PERMISSION_DENIED` Errors in React Native</title>
<link>https://coderlegion.com/28157/how-to-fix-firebase-permissiondenied-errors-in-react-native</link>
<description>&lt;p&gt;If you are using Firebase with a React Native app, sooner or later you will probably run into something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: PERMISSION_DENIED: Permission denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the annoying part is that the error does not tell you much.&lt;/p&gt;
&lt;p&gt;It does not say:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Your Firestore rule on this collection is blocking the request.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It just tells you that Firebase rejected it.&lt;/p&gt;
&lt;p&gt;That can happen for several different reasons. Your Firestore rules might be doing exactly what you told them to do, your user might not actually be authenticated, App Check might be involved, or your app might be connected to a different Firebase project than you think.&lt;/p&gt;
&lt;p&gt;So before changing random rules until the error disappears, it is better to figure out &lt;strong&gt;why Firebase is rejecting the request&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-find-out-which-firebase-service-is-rejecting-the-request&quot;&gt;First, find out which Firebase service is rejecting the request&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PERMISSION_DENIED&lt;/code&gt; is not limited to Firestore.&lt;/p&gt;
&lt;p&gt;You can see permission-related errors with services such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud Firestore&lt;/li&gt;
&lt;li&gt;Realtime Database&lt;/li&gt;
&lt;li&gt;Firebase Storage&lt;/li&gt;
&lt;li&gt;Firebase App Check&lt;/li&gt;
&lt;li&gt;Firebase Authentication-related operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So start by looking at the full error.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the stack trace shows a Firestore operation such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;firestore()
  .collection(&quot;users&quot;)
  .doc(userId)
  .get();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then your first place to look is Firestore security rules.&lt;/p&gt;
&lt;p&gt;If the error happens while uploading a file, Storage rules are more relevant.&lt;/p&gt;
&lt;p&gt;This sounds obvious, but a long React Native stack trace can make it easy to focus on the wrong part.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-your-firestore-security-rules&quot;&gt;Check your Firestore security rules&lt;/h3&gt;
&lt;p&gt;If the error comes from Firestore, check your rules first.&lt;/p&gt;
&lt;p&gt;For example, you might have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {

    match /users/{userId} {
      allow read, write: if request.auth != null;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This rule only allows authenticated users to access documents under:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/{userId}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your app makes the request before the user has signed in, Firebase will reject it.&lt;/p&gt;
&lt;p&gt;That can produce:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;permission-denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is that Firebase is not necessarily malfunctioning here.&lt;/p&gt;
&lt;p&gt;Your rule may simply be doing exactly what it was written to do.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-whether-the-user-is-actually-authenticated&quot;&gt;Check whether the user is actually authenticated&lt;/h3&gt;
&lt;p&gt;One of the easiest mistakes is assuming that the user is signed in when the request runs.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const user = auth().currentUser;

console.log(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that prints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then there is no authenticated Firebase user at that moment.&lt;/p&gt;
&lt;p&gt;A rule such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if request.auth != null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;will reject the request.&lt;/p&gt;
&lt;p&gt;This can happen when your app starts up and immediately tries to read Firestore before Firebase Authentication has restored the user's session.&lt;/p&gt;
&lt;p&gt;For example, you might have code that effectively does this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const user = auth().currentUser;

firestore()
  .collection(&quot;users&quot;)
  .doc(user.uid)
  .get();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;user&lt;/code&gt; is not available yet, the request can fail.&lt;/p&gt;
&lt;p&gt;Instead, make sure your application waits for the authentication state before making requests that require authentication.&lt;/p&gt;
&lt;p&gt;With React Native Firebase, you can listen for authentication changes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;useEffect(() =&amp;gt; {
  const unsubscribe = auth().onAuthStateChanged(user =&amp;gt; {
    if (user) {
      // Safe to make requests that require authentication
    }
  });

  return unsubscribe;
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact structure depends on your app, but the idea is the same:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not assume authentication has finished restoring just because the app has started.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-that-the-rule-matches-the-path-you-are-actually-using&quot;&gt;Check that the rule matches the path you are actually using&lt;/h3&gt;
&lt;p&gt;This is another very common one.&lt;/p&gt;
&lt;p&gt;Imagine your rules contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;match /users/{userId} {
  allow read, write: if request.auth != null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But your app actually accesses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/profiles/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That rule does not apply to &lt;code&gt;/profiles/123&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You need a matching rule for the path being accessed.&lt;/p&gt;
&lt;p&gt;This becomes especially confusing when you have nested collections.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/123/settings/theme
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is not the same path as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your rules need to match the actual document path.&lt;/p&gt;
&lt;p&gt;If you are getting &lt;code&gt;PERMISSION_DENIED&lt;/code&gt;, look at the exact collection and document your code is trying to access.&lt;/p&gt;
&lt;p&gt;Do not assume the rule you are looking at is the one Firebase is evaluating.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;be-careful-with-nested-firestore-documents&quot;&gt;Be careful with nested Firestore documents&lt;/h3&gt;
&lt;p&gt;Suppose you have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;users/{userId}/settings/{settingId}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;match /users/{userId} {
  allow read, write: if request.auth != null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and expect that to cover everything underneath the user.&lt;/p&gt;
&lt;p&gt;It does not automatically mean that every nested document is covered.&lt;/p&gt;
&lt;p&gt;You may need a recursive wildcard depending on the structure and rules you want.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;match /users/{userId}/{document=**} {
  allow read, write: if request.auth != null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be careful with this kind of rule, though.&lt;/p&gt;
&lt;p&gt;It can give access to much more data than you intended.&lt;/p&gt;
&lt;p&gt;Do not add a broad wildcard just because it makes the error disappear.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;do-not-use-allow-read-write-if-true-as-the-final-fix&quot;&gt;Do not use &lt;code&gt;allow read, write: if true&lt;/code&gt; as the final fix&lt;/h3&gt;
&lt;p&gt;You will sometimes see people recommend this while debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, it can make the permission error disappear.&lt;/p&gt;
&lt;p&gt;It also allows anyone who can access your Firebase project from the client to read and write the matching data.&lt;/p&gt;
&lt;p&gt;That is not a real fix.&lt;/p&gt;
&lt;p&gt;If you temporarily use an open rule to confirm that the rules are the cause, put the secure rule back immediately afterward.&lt;/p&gt;
&lt;p&gt;For example, if the app should only allow authenticated users:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if request.auth != null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is very different from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal is not simply to make Firebase stop complaining.&lt;/p&gt;
&lt;p&gt;The goal is to make the rule match what your application is actually supposed to allow.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-whether-you-are-using-the-correct-firebase-project&quot;&gt;Check whether you are using the correct Firebase project&lt;/h3&gt;
&lt;p&gt;This one can waste a lot of time.&lt;/p&gt;
&lt;p&gt;Your React Native app might be connected to a different Firebase project than the one you are looking at in the Firebase Console.&lt;/p&gt;
&lt;p&gt;You think you are checking the rules for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-production-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but the app is actually connected to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;my-development-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you change rules in the console and nothing happens.&lt;/p&gt;
&lt;p&gt;The app keeps returning:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;permission-denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;because you are fixing the wrong project.&lt;/p&gt;
&lt;p&gt;Check your Firebase configuration and make sure the project ID matches the Firebase project you are inspecting.&lt;/p&gt;
&lt;p&gt;For Android, check the Firebase configuration used by your Android app.&lt;/p&gt;
&lt;p&gt;For iOS, check the corresponding iOS Firebase configuration.&lt;/p&gt;
&lt;p&gt;If you have development, staging, and production projects, this becomes especially important.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-your-android-and-ios-firebase-configuration&quot;&gt;Check your Android and iOS Firebase configuration&lt;/h3&gt;
&lt;p&gt;If the problem only happens on one platform, compare the Firebase configuration between them.&lt;/p&gt;
&lt;p&gt;For Android, make sure the correct Firebase configuration is being used.&lt;/p&gt;
&lt;p&gt;For iOS, make sure the correct configuration is being used there as well.&lt;/p&gt;
&lt;p&gt;A situation like this is possible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Android → Firebase project A
iOS     → Firebase project B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then spend hours changing Firestore rules in project A while testing an iOS build that is actually talking to project B.&lt;/p&gt;
&lt;p&gt;If the error happens only on Android or only on iOS, platform-specific Firebase configuration should be on your checklist.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-firebase-app-check&quot;&gt;Check Firebase App Check&lt;/h3&gt;
&lt;p&gt;App Check can also be involved when Firebase rejects requests.&lt;/p&gt;
&lt;p&gt;App Check is designed to help verify that requests are coming from your legitimate app rather than an unauthorized client.&lt;/p&gt;
&lt;p&gt;If you recently enabled App Check or changed its configuration, check whether the failing request is related to App Check.&lt;/p&gt;
&lt;p&gt;This becomes especially important if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the app worked before enabling App Check&lt;/li&gt;
&lt;li&gt;requests work in one environment but not another&lt;/li&gt;
&lt;li&gt;you are testing a development build&lt;/li&gt;
&lt;li&gt;you changed App Check enforcement settings&lt;/li&gt;
&lt;li&gt;you changed the app's Android or iOS configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not immediately disable App Check just because it is involved.&lt;/p&gt;
&lt;p&gt;First determine whether the request is failing because of App Check configuration or because of your Firestore/Storage rules.&lt;/p&gt;
&lt;p&gt;Those are different problems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;development-and-production-can-behave-differently&quot;&gt;Development and production can behave differently&lt;/h3&gt;
&lt;p&gt;A common source of confusion is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;It works in development, but my production build gets &lt;code&gt;PERMISSION_DENIED&lt;/code&gt;.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That does not necessarily mean the Firebase rules randomly changed.&lt;/p&gt;
&lt;p&gt;Your development and production builds may be using different:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firebase projects&lt;/li&gt;
&lt;li&gt;application IDs&lt;/li&gt;
&lt;li&gt;bundle identifiers&lt;/li&gt;
&lt;li&gt;configuration files&lt;/li&gt;
&lt;li&gt;App Check settings&lt;/li&gt;
&lt;li&gt;authentication settings&lt;/li&gt;
&lt;li&gt;environment variables&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the problem only happens in a release build, compare the Firebase configuration between the working and failing builds.&lt;/p&gt;
&lt;p&gt;This is usually more useful than changing your Firestore rules blindly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-the-exact-user-id-being-used&quot;&gt;Check the exact user ID being used&lt;/h3&gt;
&lt;p&gt;Suppose your rule is intended to allow users to access their own documents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;match /users/{userId} {
  allow read, write: if request.auth.uid == userId;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means this request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;will only work if:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;request.auth.uid == &quot;123&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your application accidentally requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/456
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while the logged-in user is actually &lt;code&gt;123&lt;/code&gt;, Firebase will reject it.&lt;/p&gt;
&lt;p&gt;So log the values you are using while debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;console.log(&quot;Current user:&quot;, auth().currentUser?.uid);
console.log(&quot;Requested user:&quot;, userId);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If they do not match, you have found the problem.&lt;/p&gt;
&lt;p&gt;This is much more useful than changing the security rule to allow everyone.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-your-storage-rules-if-the-error-happens-during-uploads&quot;&gt;Check your Storage rules if the error happens during uploads&lt;/h3&gt;
&lt;p&gt;If the problem happens while uploading an image, PDF, or another file, you may actually be dealing with Firebase Storage rather than Firestore.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;FirebaseError: [storage/unauthorized] User is not authorized to perform the desired action.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Storage has its own security rules.&lt;/p&gt;
&lt;p&gt;A Firestore rule like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if request.auth != null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;does not control Firebase Storage.&lt;/p&gt;
&lt;p&gt;If the error happens during:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;storage()
  .ref(&quot;profile-images/avatar.jpg&quot;)
  .putFile(filePath);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;look at your Storage rules instead.&lt;/p&gt;
&lt;p&gt;The service producing the error matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;check-realtime-database-rules-separately&quot;&gt;Check Realtime Database rules separately&lt;/h3&gt;
&lt;p&gt;The same idea applies to Realtime Database.&lt;/p&gt;
&lt;p&gt;Realtime Database has its own rules structure, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;rules&quot;: {
    &quot;.read&quot;: &quot;auth != null&quot;,
    &quot;.write&quot;: &quot;auth != null&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Changing Firestore rules will not fix a Realtime Database permission problem.&lt;/p&gt;
&lt;p&gt;If your application uses several Firebase services, make sure you know which one is rejecting the request before changing anything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;use-the-firebase-emulator-when-appropriate&quot;&gt;Use the Firebase Emulator when appropriate&lt;/h3&gt;
&lt;p&gt;If you are working on complicated security rules, the Firebase Emulator Suite can make debugging much easier.&lt;/p&gt;
&lt;p&gt;Instead of repeatedly testing rules against production data, you can test your rules locally.&lt;/p&gt;
&lt;p&gt;This is especially useful when your rules depend on things like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;request.auth
request.auth.uid
request.resource.data
resource.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, a rule might look correct at first glance but fail because the incoming document does not contain the field your rule expects.&lt;/p&gt;
&lt;p&gt;Testing the rule with known authentication and data values makes these problems much easier to see.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;look-at-what-changed-right-before-the-error&quot;&gt;Look at what changed right before the error&lt;/h3&gt;
&lt;p&gt;This is one of the most useful debugging habits for Firebase errors.&lt;/p&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What changed immediately before &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; appeared?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Maybe you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;changed Firestore rules&lt;/li&gt;
&lt;li&gt;added authentication&lt;/li&gt;
&lt;li&gt;changed the user document structure&lt;/li&gt;
&lt;li&gt;switched Firebase projects&lt;/li&gt;
&lt;li&gt;enabled App Check&lt;/li&gt;
&lt;li&gt;changed an Android package name&lt;/li&gt;
&lt;li&gt;changed an iOS bundle identifier&lt;/li&gt;
&lt;li&gt;updated a Firebase dependency&lt;/li&gt;
&lt;li&gt;moved a Firestore collection&lt;/li&gt;
&lt;li&gt;changed the document ID&lt;/li&gt;
&lt;li&gt;created a new production build&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The timing does not prove what caused the error, but it gives you a very good place to start.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-simple-debugging-order&quot;&gt;A simple debugging order&lt;/h3&gt;
&lt;p&gt;When I get a Firebase &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; error, I usually go through these checks:&lt;/p&gt;
&lt;h4 id=&quot;1-identify-the-firebase-service&quot;&gt;1. Identify the Firebase service&lt;/h4&gt;
&lt;p&gt;Is it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firestore?&lt;/li&gt;
&lt;li&gt;Storage?&lt;/li&gt;
&lt;li&gt;Realtime Database?&lt;/li&gt;
&lt;li&gt;App Check?&lt;/li&gt;
&lt;li&gt;something else?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-read-the-exact-path&quot;&gt;2. Read the exact path&lt;/h4&gt;
&lt;p&gt;What collection, document, or storage path is the app trying to access?&lt;/p&gt;
&lt;h4 id=&quot;3-check-authentication&quot;&gt;3. Check authentication&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;console.log(auth().currentUser?.uid);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is there actually a logged-in user?&lt;/p&gt;
&lt;h4 id=&quot;4-check-the-security-rule&quot;&gt;4. Check the security rule&lt;/h4&gt;
&lt;p&gt;Does the rule actually match the path being accessed?&lt;/p&gt;
&lt;h4 id=&quot;5-check-the-values-used-by-the-rule&quot;&gt;5. Check the values used by the rule&lt;/h4&gt;
&lt;p&gt;If the rule compares user IDs, roles, or document fields, verify those values.&lt;/p&gt;
&lt;h4 id=&quot;6-check-the-firebase-project&quot;&gt;6. Check the Firebase project&lt;/h4&gt;
&lt;p&gt;Make sure the app and Firebase Console are using the same project.&lt;/p&gt;
&lt;h4 id=&quot;7-check-app-check&quot;&gt;7. Check App Check&lt;/h4&gt;
&lt;p&gt;Especially if the problem started after enabling or enforcing it.&lt;/p&gt;
&lt;h4 id=&quot;8-check-platform-specific-configuration&quot;&gt;8. Check platform-specific configuration&lt;/h4&gt;
&lt;p&gt;If only Android or iOS is affected, compare the configuration for that platform.&lt;/p&gt;
&lt;h4 id=&quot;9-reproduce-the-problem-again&quot;&gt;9. Reproduce the problem again&lt;/h4&gt;
&lt;p&gt;After making one change, test again.&lt;/p&gt;
&lt;p&gt;This gives you a much clearer debugging trail.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-finding-a-simple-rules-mistake&quot;&gt;Example: finding a simple rules mistake&lt;/h3&gt;
&lt;p&gt;Imagine your Firestore rule says:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;match /users/{userId} {
  allow read: if request.auth != null
               &amp;amp;&amp;amp; request.auth.uid == userId;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your app is logged in as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;abc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but your code requests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/users/undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Firebase sees:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;request.auth.uid == &quot;abc123&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;userId == &quot;undefined&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The condition is false.&lt;/p&gt;
&lt;p&gt;So Firebase returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;permission-denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is not Firebase.&lt;/p&gt;
&lt;p&gt;The problem is that your application generated the wrong document path.&lt;/p&gt;
&lt;p&gt;This is why checking the exact request is so important.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PERMISSION_DENIED&lt;/code&gt; can be frustrating because the error itself is often very short while the number of possible causes is not.&lt;/p&gt;
&lt;p&gt;The important thing is to avoid treating it as simply:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Firebase permissions are broken.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Start by finding out &lt;strong&gt;which Firebase service rejected the request&lt;/strong&gt;, then check the exact path, authentication state, security rules, and Firebase project.&lt;/p&gt;
&lt;p&gt;If the error appeared after a recent change, use that as a clue.&lt;/p&gt;
&lt;p&gt;And avoid the temptation to solve everything with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;allow read, write: if true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That only hides the real problem and can leave your data exposed.&lt;/p&gt;
&lt;p&gt;If your error is buried inside a much larger React Native/Firebase log and it is difficult to tell whether the problem is authentication, rules, App Check, configuration, or something else, you can paste the full error into &lt;strong&gt;FixMyError&lt;/strong&gt; and use it to narrow down what is actually going wrong.&lt;/p&gt;
&lt;p&gt;Try it at &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fixmyerrorapp.com&quot;&gt;https://www.fixmyerrorapp.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/28157/how-to-fix-firebase-permissiondenied-errors-in-react-native</guid>
<pubDate>Tue, 22 Sep 2026 12:05:03 +0000</pubDate>
</item>
<item>
<title>The Firebase Auth bug that took me 3 days to find (it wasn't Firebase's fault)</title>
<link>https://coderlegion.com/27717/the-firebase-auth-bug-that-took-me-3-days-to-find-it-wasnt-firebases-fault</link>
<description>&lt;p&gt;&lt;strong&gt;The symptom&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;signInWithRedirect would fire fine — the Google popup flow completed, the redirect came back, and then... nothing. auth.currentUser was null. No error in the console. No failed network request. The user object had just vanished somewhere between the redirect and my app reading it.&lt;/p&gt;
&lt;p&gt;The frustrating part: it worked perfectly on localhost. It only broke in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The wrong turns&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First guess: async timing bug. I wrapped the auth-state check in every useEffect variation I could think of, added loading states, tried onAuthStateChanged instead of reading currentUser directly. No change.&lt;/p&gt;
&lt;p&gt;Second guess: a config mismatch. I diffed my Firebase config between local and prod character by character. Identical.&lt;/p&gt;
&lt;p&gt;Third guess: CORS. Checked network tab, no CORS errors, no failed requests at all — which in hindsight should have been the clue. A CORS problem fails loudly. This was failing silently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What was actually happening&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Firebase's redirect-based auth flow works by briefly using an iframe on your authDomain (typically your-project.firebaseapp.com) to relay the session back to your app. That relay depends on third-party storage being accessible inside that iframe — cookies/localStorage set on firebaseapp.com, read while your actual site is on a different domain.&lt;/p&gt;
&lt;p&gt;Browsers have been steadily tightening third-party storage partitioning (Safari's been strict about this for years, Chrome's been catching up). Production was on my real custom domain. Local dev was hitting localhost, which a lot of browsers still treat more permissively. That's why it &quot;worked&quot; locally and silently died in prod — the iframe's storage access was just being partitioned away, and Firebase had no clean way to surface that as an error since, from its perspective, the browser just... didn't return anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Switch to signInWithPopup instead of signInWithRedirect. Popup-based auth doesn't rely on that same cross-domain iframe storage relay — the popup and the opener communicate directly, sidestepping the whole partitioning problem.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Before: silently broke in production
await signInWithRedirect(auth, provider);

// After: works consistently across browsers
await signInWithPopup(auth, provider);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trade-off: popups get blocked more aggressively on mobile and by some ad blockers, so it's not a strictly-better default — just a better fit for what was actually failing here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When something works locally and silently fails in production with zero error output, my new first suspect is cross-origin storage behavior, not application logic. Silent failure is often the browser deciding not to tell you it blocked something, rather than your code doing something wrong.&lt;/p&gt;
&lt;p&gt;Anyone else hit this exact wall with Firebase (or any auth provider using a similar iframe-relay pattern)? Curious if others landed on popup, or found a way to keep redirect working reliably.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27717/the-firebase-auth-bug-that-took-me-3-days-to-find-it-wasnt-firebases-fault</guid>
<pubDate>Sat, 19 Sep 2026 08:24:54 +0000</pubDate>
</item>
<item>
<title>How I Structured My Firestore Data Model for an Anti-Proxy Attendance System</title>
<link>https://coderlegion.com/26120/how-i-structured-my-firestore-data-model-for-an-anti-proxy-attendance-system</link>
<description>&lt;p&gt;The database architecture behind students, meetings, attendance, authentication, and controlling unnecessary Firestore reads.*&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-i-structured-my-firestore-data-model-for-an-anti-proxy-attendance-system&quot;&gt;How I Structured My Firestore Data Model for an Anti-Proxy Attendance System&lt;/h2&gt;
&lt;p&gt;When I began building my attendance system, database architecture wasn't really my first concern.&lt;/p&gt;
&lt;p&gt;I had one straightforward objective:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Make the MVP work.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I needed a place to store students.&lt;/p&gt;
&lt;p&gt;I needed a way to create meetings.&lt;/p&gt;
&lt;p&gt;I needed to save attendance records.&lt;/p&gt;
&lt;p&gt;So I started creating Firestore data structures based on whatever the application needed at that moment.&lt;/p&gt;
&lt;p&gt;Initially, that approach worked.&lt;/p&gt;
&lt;p&gt;Then the application started expanding.&lt;/p&gt;
&lt;p&gt;Authentication was introduced.&lt;/p&gt;
&lt;p&gt;Dynamic QR authentication was introduced.&lt;/p&gt;
&lt;p&gt;Device verification was introduced.&lt;/p&gt;
&lt;p&gt;Attendance history was introduced.&lt;/p&gt;
&lt;p&gt;Offline synchronization was introduced.&lt;/p&gt;
&lt;p&gt;And suddenly, Firestore was doing much more than simply holding attendance records.&lt;/p&gt;
&lt;p&gt;It was turning into the foundation that the rest of the application depended on.&lt;/p&gt;
&lt;p&gt;That's when an important realization hit me:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A schema that is perfectly fine for an MVP may not remain suitable once the application starts growing.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-first-question-what-really-needs-to-live-in-the-database-&quot;&gt;The First Question: What Really Needs to Live in the Database?&lt;/h2&gt;
&lt;p&gt;An attendance system sounds straightforward until you actually list everything it needs to keep track of.&lt;/p&gt;
&lt;p&gt;For every attendance event, I may need details such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The student&lt;/li&gt;
&lt;li&gt;The meeting&lt;/li&gt;
&lt;li&gt;The attendance status&lt;/li&gt;
&lt;li&gt;The time the attendance was recorded&lt;/li&gt;
&lt;li&gt;Authentication information&lt;/li&gt;
&lt;li&gt;Device information&lt;/li&gt;
&lt;li&gt;Synchronization state&lt;/li&gt;
&lt;li&gt;Administrative information&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difficult part wasn't simply finding a place to store all of these things.&lt;/p&gt;
&lt;p&gt;The harder problem was determining &lt;strong&gt;how all of these pieces should connect with one another&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I started looking at the database through the entities that actually existed inside the system.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student
   │
   ├── Identity
   ├── Device
   └── Attendance
          │
          ▼
        Meeting
          │
          ├── QR Session
          └── Attendance Records
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database needed to represent these relationships without making the application repeatedly retrieve the same information.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;firestore-forces-a-different-way-of-thinking&quot;&gt;Firestore Forces a Different Way of Thinking&lt;/h2&gt;
&lt;p&gt;Firestore doesn't work like a traditional relational database.&lt;/p&gt;
&lt;p&gt;There are no conventional SQL joins where I can simply write something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT ...
FROM students
JOIN attendance ...
JOIN meetings ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead, I need to think about a different question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Which pieces of data does the application need to access together?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That question became one of the most important considerations while designing the schema.&lt;/p&gt;
&lt;p&gt;In Firestore, the structure of your data directly influences how much information the application has to retrieve.&lt;/p&gt;
&lt;p&gt;And I had already experienced the consequences of getting that wrong.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;i-still-remember-the-50-000-read-problem&quot;&gt;I Still Remember the 50,000 Read Problem&lt;/h2&gt;
&lt;p&gt;Earlier during development, I accidentally burned through my Firestore free-tier reads while testing and debugging the application.&lt;/p&gt;
&lt;p&gt;That experience completely changed the way I looked at database operations.&lt;/p&gt;
&lt;p&gt;Before that happened, my thinking was basically:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;It's just one additional read.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Afterwards, my question became:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Does the application actually need this read?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That shift in mindset started influencing the database architecture as the project continued to evolve.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;designing-around-actual-access-patterns&quot;&gt;Designing Around Actual Access Patterns&lt;/h2&gt;
&lt;p&gt;Rather than beginning with:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Which collections should I create?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I started approaching the problem from the opposite direction:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;What information does the application actually need to retrieve?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example, when an attendance session is running, the system needs enough information to validate the request and then record the attendance correctly.&lt;/p&gt;
&lt;p&gt;So the database has to support operations such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create Meeting
      ↓
Generate Attendance Session
      ↓
Validate Student
      ↓
Validate Device
      ↓
Validate QR
      ↓
Record Attendance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database should not become the thing slowing down every stage of the process.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;keeping-attendance-as-a-separate-record&quot;&gt;Keeping Attendance as a Separate Record&lt;/h2&gt;
&lt;p&gt;One of the key decisions I made was to treat attendance as its own record rather than repeatedly changing a student's profile whenever they attend a meeting.&lt;/p&gt;
&lt;p&gt;Conceptually, the relationship looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student
   │
   └──────────────┐
                  │
                  ▼
              Attendance
                  │
                  ▼
                Meeting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows attendance history to exist separately from the student's core profile.&lt;/p&gt;
&lt;p&gt;Instead of only asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Is this student present?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;the system can answer a much more useful question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Was this student present for this particular meeting, and when exactly was that attendance recorded?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distinction is important.&lt;/p&gt;
&lt;p&gt;Attendance represents an &lt;strong&gt;event&lt;/strong&gt;, rather than simply being a permanent property attached to a student.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;more-data-does-not-automatically-mean-a-better-database&quot;&gt;More Data Does Not Automatically Mean a Better Database&lt;/h2&gt;
&lt;p&gt;A common trap during database design is duplicating information everywhere because it makes individual queries feel easier.&lt;/p&gt;
&lt;p&gt;At first, that can seem convenient.&lt;/p&gt;
&lt;p&gt;The problem appears later when those duplicated values need to stay consistent.&lt;/p&gt;
&lt;p&gt;For example, copying the same information into several documents may simplify one specific query, but now the application has to maintain multiple versions of the same data.&lt;/p&gt;
&lt;p&gt;So I constantly found myself balancing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fast reads&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;against&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data duplication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;against&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write complexity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;against&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Firestore usage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There isn't one universal answer that solves all of these concerns.&lt;/p&gt;
&lt;p&gt;The right decision depends on how the application actually reads and writes the data.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-an-attendance-request-actually-contains&quot;&gt;What an Attendance Request Actually Contains&lt;/h2&gt;
&lt;p&gt;At a high level, an attendance request now carries several different pieces of context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Attendance Request
       │
       ├── Student Identity
       │
       ├── Meeting
       │
       ├── QR Validation
       │
       ├── Device Verification
       │
       └── Attendance Record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So attendance isn't really just:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;student → present
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's closer to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;student
   +
meeting
   +
valid session
   +
trusted device
   +
validated request
   =
attendance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means the database is not merely a storage layer.&lt;/p&gt;
&lt;p&gt;It becomes part of the security architecture as well.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;thinking-carefully-about-reads-and-writes&quot;&gt;Thinking Carefully About Reads and Writes&lt;/h2&gt;
&lt;p&gt;After experiencing the 50,000-read issue, I started paying much more attention to the difference between reads and writes.&lt;/p&gt;
&lt;p&gt;A system can function correctly and still perform far more database operations than it actually needs.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Page Load
   ↓
Read Student
   ↓
Read Meeting
   ↓
Read Configuration
   ↓
Read Student Again
   ↓
Read Meeting Again
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing is technically failing here.&lt;/p&gt;
&lt;p&gt;But the application is doing unnecessary work.&lt;/p&gt;
&lt;p&gt;So I started looking for ways to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reuse information that the application already has&lt;/li&gt;
&lt;li&gt;Eliminate repeated reads&lt;/li&gt;
&lt;li&gt;Cache frequently accessed information&lt;/li&gt;
&lt;li&gt;Minimize unnecessary writes&lt;/li&gt;
&lt;li&gt;Fetch only the data that is actually required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This eventually became part of the larger optimization work I covered in Part 3.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;firestore-security-goes-beyond-security-rules&quot;&gt;Firestore Security Goes Beyond Security Rules&lt;/h2&gt;
&lt;p&gt;Another lesson from the project was that database security cannot be reduced to Firestore Security Rules alone.&lt;/p&gt;
&lt;p&gt;Rules are important.&lt;/p&gt;
&lt;p&gt;But they represent only one layer of the overall security model.&lt;/p&gt;
&lt;p&gt;In my attendance system, validation happens across several stages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User Authentication
        ↓
Dynamic QR Validation
        ↓
Device Verification
        ↓
Backend Validation
        ↓
Firestore
        ↓
Attendance Record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database should never blindly trust whatever information the frontend submits.&lt;/p&gt;
&lt;p&gt;The backend needs to validate the request before writing sensitive attendance information.&lt;/p&gt;
&lt;p&gt;This separation became even more important as more anti-proxy mechanisms were introduced into the system.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;offline-support-changes-the-data-lifecycle&quot;&gt;Offline Support Changes the Data Lifecycle&lt;/h2&gt;
&lt;p&gt;Then another challenge appeared.&lt;/p&gt;
&lt;p&gt;Offline support.&lt;/p&gt;
&lt;p&gt;Once the system could record attendance without immediately communicating with Firestore, the data model needed to distinguish between two different states:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Attendance created locally
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Attendance successfully synchronized
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Conceptually, the lifecycle became:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student
   ↓
Attendance Event
   ↓
Local Storage
   ↓
Pending Sync
   ↓
Firestore
   ↓
Synchronized
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That meant the lifecycle of an attendance record was no longer simply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create → Save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It became:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create
  ↓
Store locally
  ↓
Wait
  ↓
Synchronize
  ↓
Confirm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That changed the way I thought about reliability as well.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-would-avoid-doing&quot;&gt;What I Would Avoid Doing&lt;/h2&gt;
&lt;p&gt;One of the clearest lessons from this project is that Firestore schemas should not be designed by blindly copying relational database patterns.&lt;/p&gt;
&lt;p&gt;Firestore has its own strengths, limitations, and way of handling data.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;How would I structure this in MySQL?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I found it much more useful to ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;What are the operations my application performs most often?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then I could design the data model around those access patterns.&lt;/p&gt;
&lt;p&gt;That single shift in perspective made the database design much easier to reason about.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-bigger-lesson-from-the-data-model&quot;&gt;The Bigger Lesson From the Data Model&lt;/h2&gt;
&lt;p&gt;At the beginning, the database felt like infrastructure.&lt;/p&gt;
&lt;p&gt;Something beneath the actual application.&lt;/p&gt;
&lt;p&gt;But as the system became more complex, I realized that the data model affects almost every major part of the application:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Cost&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;li&gt;Offline synchronization&lt;/li&gt;
&lt;li&gt;Backend architecture&lt;/li&gt;
&lt;li&gt;Query complexity&lt;/li&gt;
&lt;li&gt;Scalability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A poor data model doesn't necessarily cause an obvious failure on day one.&lt;/p&gt;
&lt;p&gt;Sometimes the problem is much quieter.&lt;/p&gt;
&lt;p&gt;It simply makes every new feature a little harder to build.&lt;/p&gt;
&lt;p&gt;And that can become a much bigger problem over time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-would-change-today&quot;&gt;What I Would Change Today&lt;/h2&gt;
&lt;p&gt;If I were starting this system again today, I would spend considerably more time thinking about the data model before implementing features.&lt;/p&gt;
&lt;p&gt;I'd define:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Entities
   ↓
Relationships
   ↓
Access Patterns
   ↓
Read / Write Frequency
   ↓
Caching Strategy
   ↓
Security Boundaries
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;before writing the first database query.&lt;/p&gt;
&lt;p&gt;I learned this lesson after the system was already functioning.&lt;/p&gt;
&lt;p&gt;Building first showed me what the application genuinely needed.&lt;/p&gt;
&lt;p&gt;But if I rebuilt it from scratch, I'd bring those lessons into the design phase much earlier.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The original requirement sounded simple:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;I just need somewhere to store attendance.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Over time, that requirement evolved into something much larger:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;I need a data architecture that can support authentication, dynamic QR validation, device verification, offline synchronization, and efficient reads and writes.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those are two completely different problems.&lt;/p&gt;
&lt;p&gt;And probably one of the biggest lessons this project gave me is that &lt;strong&gt;database design isn't simply about deciding where data should be stored.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's about defining how the entire application communicates with that data.&lt;/p&gt;
&lt;p&gt;The database isn't simply sitting underneath the architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is part of the architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;The database was only one part of the overall system.&lt;/p&gt;
&lt;p&gt;The next challenge was infrastructure.&lt;/p&gt;
&lt;p&gt;My backend was running on a free-tier service, and that introduced another constraint that became difficult to ignore:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cold starts.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Which raised another question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How do you make a slow backend feel fast from the user's perspective?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's the problem I'll explore next.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-project&quot;&gt;🔗 Project&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/siddarthpatelkama/UBA-veltech-attendance-system&quot;&gt;https://github.com/siddarthpatelkama/UBA-veltech-attendance-system&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-your-turn&quot;&gt;💬 Your turn&lt;/h4&gt;
&lt;p&gt;If you've built a Firestore application before, what's one database decision you made early that later saved you from dealing with a much bigger problem?&lt;/p&gt;
&lt;p&gt;I'd love to hear about it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26120/how-i-structured-my-firestore-data-model-for-an-anti-proxy-attendance-system</guid>
<pubDate>Thu, 03 Sep 2026 04:12:28 +0000</pubDate>
</item>
<item>
<title>Supabase Auth vs Firebase Auth: Which One Should You Choose in 2026?</title>
<link>https://coderlegion.com/24980/supabase-auth-vs-firebase-auth-which-one-should-you-choose-in-2026</link>
<description>&lt;p&gt;You're starting a new Next.js project, and before you write a single feature, you have to answer one boring but critical question: &lt;strong&gt;who's going to handle login?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Two names keep showing up at the top of every list — &lt;strong&gt;Supabase Auth&lt;/strong&gt; and &lt;strong&gt;Firebase Auth&lt;/strong&gt;. Both are free to start, both support Google/GitHub/email login, and both promise to save you from writing your own password-hashing logic. But they're built on completely different foundations, and picking the wrong one can mean months of awkward workarounds later — like discovering you need SQL joins for a permissions system, but you're stuck with Firestore's document model.&lt;/p&gt;
&lt;p&gt;This guide breaks down exactly how Supabase Auth and Firebase Auth differ in 2026 — architecture, pricing, security, and real Next.js 16 App Router code for both — so you can pick the right one &lt;em&gt;before&lt;/em&gt; you've built half your app on top of it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-comparison-is-tricky&quot;&gt;Why This Comparison Is Tricky&lt;/h3&gt;
&lt;p&gt;Most &quot;Supabase vs Firebase&quot; posts compare marketing pages, not real implementation experience. The truth is these two products solve authentication very differently under the hood:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Supabase Auth&lt;/strong&gt; is a layer on top of &lt;strong&gt;PostgreSQL&lt;/strong&gt; — every user lives in a real SQL table (&lt;code&gt;auth.users&lt;/code&gt;), and permissions are enforced with &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; policies written in SQL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Auth&lt;/strong&gt; is a layer on top of &lt;strong&gt;Firestore/Google Cloud&lt;/strong&gt; — users live in Google's own auth system, and permissions are enforced with &lt;strong&gt;Firestore Security Rules&lt;/strong&gt;, a completely different (non-SQL) rules language.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That single architectural difference is what actually decides which one fits your project — not the login form UI, which looks nearly identical on both.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-answer&quot;&gt;Quick Answer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Supabase Auth&lt;/strong&gt; if you want a real Postgres database, SQL-based permissions (RLS), open-source flexibility, and the option to self-host later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose Firebase Auth&lt;/strong&gt; if you're already invested in Firebase or Google Cloud services, or if Firebase's managed identity infrastructure fits your application's architecture.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let's get into the details.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;supabase-auth-vs-firebase-auth-which-is-better-for-next-js-&quot;&gt;Supabase Auth vs Firebase Auth: Which Is Better for Next.js?&lt;/h3&gt;
&lt;p&gt;If you're building a Next.js SaaS application with PostgreSQL, Supabase Auth is usually the more natural choice because authentication, database access, and authorization can be designed around the same Postgres/RLS model.&lt;/p&gt;
&lt;p&gt;Firebase Auth is a better fit when your application already uses Firebase/Google Cloud services or your data model naturally fits Firestore.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;supabase-auth-vs-firebase-auth-feature-comparison&quot;&gt;Supabase Auth vs Firebase Auth: Feature Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature &lt;/th&gt;&lt;th&gt;Supabase Auth &lt;/th&gt;&lt;th&gt;Firebase Auth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Authentication architecture &lt;/td&gt;&lt;td&gt;Integrated with Supabase/PostgreSQL ecosystem &lt;/td&gt;&lt;td&gt;Managed identity service&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Common database pairing     &lt;/td&gt;&lt;td&gt;PostgreSQL                                    &lt;/td&gt;&lt;td&gt;Firestore (NoSQL)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Free/no-cost auth tier &lt;/td&gt;&lt;td&gt;50,000 monthly active users (MAU) &lt;/td&gt;&lt;td&gt;Depends on Firebase Auth / Identity Platform configuration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Paid pricing &lt;/td&gt;&lt;td&gt;Included in Supabase plan quota; $0.00325/MAU over Pro quota &lt;/td&gt;&lt;td&gt;Identity Platform: 50,000 MAU no-cost tier on Blaze, then usage-based pricing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Email/password &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Magic links &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ (email link sign-in)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Social OAuth (Google, GitHub, etc.) &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Phone/SMS OTP &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Permissions model &lt;/td&gt;&lt;td&gt;Row Level Security (SQL policies) &lt;/td&gt;&lt;td&gt;Firestore Security Rules (custom rules language)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting &lt;/td&gt;&lt;td&gt;✅ Yes (fully open source) &lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Open source &lt;/td&gt;&lt;td&gt;✅ Yes &lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Next.js SSR package &lt;/td&gt;&lt;td&gt;&lt;code&gt;@supabase/ssr&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Firebase JS SDK + &lt;code&gt;initializeServerApp&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Best paired with &lt;/td&gt;&lt;td&gt;Postgres-based SaaS apps &lt;/td&gt;&lt;td&gt;Google Cloud / Firestore apps&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Both platforms' free tiers are generous enough to cover most side projects and early-stage SaaS apps completely — pricing usually isn't the deciding factor until you're well past MVP stage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-the-real-difference&quot;&gt;Architecture: The Real Difference&lt;/h3&gt;
&lt;p&gt;This is the part most comparison articles skip, and it's the one that actually matters.&lt;/p&gt;
&lt;h4 id=&quot;supabase-auth-runs-on-postgres&quot;&gt;Supabase Auth Runs on Postgres&lt;/h4&gt;
&lt;p&gt;When someone signs up with Supabase Auth, a row gets created in a special &lt;code&gt;auth.users&lt;/code&gt; table inside your actual Postgres database. Every other table you create — &lt;code&gt;profiles&lt;/code&gt;, &lt;code&gt;posts&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt; — can have a foreign key pointing straight at &lt;code&gt;auth.users.id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That means your permissions logic is just SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Only let a user see their own orders
create policy &quot;Users can view own orders&quot;
on orders
for select
using (auth.uid() = user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; if your app has relational data — a user has many orders, an order has many line items, a team has many members — Postgres joins handle that naturally. You're not fighting the database to model relationships.&lt;/p&gt;
&lt;h4 id=&quot;firebase-auth-and-firestore&quot;&gt;Firebase Auth And Firestore&lt;/h4&gt;
&lt;p&gt;Firebase Auth manages identity separately from your application data. When paired with Firestore, authorization is commonly enforced using &lt;strong&gt;Firestore Security Rules&lt;/strong&gt; — a rules language, not SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /orders/{orderId} {
      allow read: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == resource.data.userId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Firestore is a document database. There's no native &lt;code&gt;JOIN&lt;/code&gt;. If your data is deeply relational (think: multi-tenant SaaS with teams, roles, and shared resources), you'll end up duplicating data across documents or running multiple round-trip queries to stitch things together client-side.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Neither model is &quot;wrong&quot; — Firestore's document model is genuinely great for chat apps, real-time feeds, and simple per-user data. Postgres shines once your data has real relationships between tables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-supabase-auth-in-next-js-16&quot;&gt;Setting Up Supabase Auth in Next.js 16&lt;/h3&gt;
&lt;p&gt;This guide assumes &lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router, &lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode, and &lt;strong&gt;Tailwind CSS&lt;/strong&gt; — the current defaults for any new Next.js project.&lt;/p&gt;
&lt;h4 id=&quot;step-1-install-dependencies&quot;&gt;Step 1: Install Dependencies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @supabase/supabase-js @supabase/ssr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;@supabase/supabase-js&lt;/code&gt; is the core client library. &lt;code&gt;@supabase/ssr&lt;/code&gt; is the package built specifically for frameworks like Next.js — it handles the tricky part of syncing auth cookies between the browser and the server so your session works correctly in Server Components, Server Actions, and the browser alike.&lt;/p&gt;
&lt;h4 id=&quot;step-2-add-environment-variables&quot;&gt;Step 2: Add Environment Variables&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your_publishable_key
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ Never expose a Supabase secret key (&lt;code&gt;sb_secret_...&lt;/code&gt;) or legacy &lt;code&gt;service_role&lt;/code&gt; key in client-side code. Both bypass RLS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; For new Supabase projects, use the publishable key rather than the legacy anon key. Supabase is transitioning away from the legacy API keys during 2026.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-3-create-the-browser-client&quot;&gt;Step 3: Create the Browser Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/supabase/client.ts
import { createBrowserClient } from &quot;@supabase/ssr&quot;;

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this inside &lt;strong&gt;Client Components&lt;/strong&gt; — anywhere you need &lt;code&gt;&quot;use client&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;step-4-create-the-server-client&quot;&gt;Step 4: Create the Server Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/supabase/server.ts
import { createServerClient } from &quot;@supabase/ssr&quot;;
import { cookies } from &quot;next/headers&quot;;

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
              cookieStore.set(name, value, options)
            );
          } catch {
            // Safe to ignore when called from a Server Component
          }
        },
      },
    }
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; this client reads and writes auth cookies through Next.js's &lt;code&gt;cookies()&lt;/code&gt; API, so a session created during login is visible to Server Components and Server Actions immediately — no manual token passing needed.&lt;/p&gt;
&lt;h4 id=&quot;step-5-add-the-auth-proxy-middleware-&quot;&gt;Step 5: Add the Auth Proxy (Middleware)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// utils/supabase/proxy.ts
import { createServerClient } from &quot;@supabase/ssr&quot;;
import { NextResponse, type NextRequest } from &quot;next/server&quot;;

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet, headers) {
          cookiesToSet.forEach(({ name, value }) =&amp;gt;
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
            supabaseResponse.cookies.set(name, value, options)
          );
          Object.entries(headers).forEach(([key, value]) =&amp;gt;
            supabaseResponse.headers.set(key, value)
          );
        },
      },
    }
  );

  await supabase.auth.getClaims();

  return supabaseResponse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// proxy.ts (root of project)
import { type NextRequest } from &quot;next/server&quot;;
import { updateSession } from &quot;@/utils/supabase/proxy&quot;;

export async function proxy(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    &quot;/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)&quot;,
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Skipping this proxy file. Without it, Supabase's refresh token doesn't get renewed on every request, and users get randomly logged out mid-session — one of the most common Supabase Auth bugs beginners hit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-6-sign-in-with-email-and-password&quot;&gt;Step 6: Sign In With Email and Password&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/auth.ts
&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { createClient } from &quot;@/lib/supabase/server&quot;;

export async function login(prev: unknown, formData: FormData) {
  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword({
    email: formData.get(&quot;email&quot;) as string,
    password: formData.get(&quot;password&quot;) as string,
  });

  if (error) {
    return { error: error.message };
  }

  redirect(&quot;/dashboard&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-7-protect-data-with-row-level-security&quot;&gt;Step 7: Protect Data with Row Level Security&lt;/h4&gt;
&lt;p&gt;This is Supabase Auth's biggest advantage — security lives at the &lt;strong&gt;database level&lt;/strong&gt;, not just in your app code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;alter table profiles enable row level security;

create policy &quot;Users can view own profile&quot;
on profiles for select
using ((select auth.uid()) = id);

create policy &quot;Users can update own profile&quot;
on profiles for update
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; even if someone bypasses your Next.js app entirely and hits the Supabase API directly, RLS still blocks them from seeing data that isn't theirs. Your frontend code stops being the only line of defense.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 If you're using Supabase Auth with multiple tables, writing RLS policies manually can become repetitive. DevStacked's &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; can generate strict policies based on your access pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-firebase-auth-in-next-js-16&quot;&gt;Setting Up Firebase Auth in Next.js 16&lt;/h3&gt;
&lt;h4 id=&quot;step-1-install-dependencies-1&quot;&gt;Step 1: Install Dependencies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install firebase firebase-admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;firebase&lt;/code&gt; is the client SDK (runs in the browser and in Server Components via Firebase's newer server app support). &lt;code&gt;firebase-admin&lt;/code&gt; is for privileged, server-only operations like verifying tokens.&lt;/p&gt;
&lt;h4 id=&quot;step-2-add-environment-variables-1&quot;&gt;Step 2: Add Environment Variables&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=yourproject.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=yourproject
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-initialize-firebase-client-side-&quot;&gt;Step 3: Initialize Firebase (Client Side)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/firebase/client.ts
import { initializeApp, getApps, getApp } from &quot;firebase/app&quot;;
import { getAuth } from &quot;firebase/auth&quot;;

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;getApps().length ? getApp() : initializeApp(...)&lt;/code&gt; prevents Firebase from being re-initialized on every hot reload in development — a classic Firebase + Next.js gotcha if you skip this check.&lt;/p&gt;
&lt;h4 id=&quot;step-4-sign-in-with-email-and-password&quot;&gt;Step 4: Sign In With Email and Password&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/login-form.tsx
&quot;use client&quot;;

import { signInWithEmailAndPassword } from &quot;firebase/auth&quot;;
import { auth } from &quot;@/lib/firebase/client&quot;;
import { useRouter } from &quot;next/navigation&quot;;
import { useState } from &quot;react&quot;;

export function LoginForm() {
  const router = useRouter();
  const [error, setError] = useState(&quot;&quot;);

  const handleSubmit = async (e: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();
    const form = new FormData(e.currentTarget);

    try {
      await signInWithEmailAndPassword(
        auth,
        form.get(&quot;email&quot;) as string,
        form.get(&quot;password&quot;) as string
      );
      router.push(&quot;/dashboard&quot;);
    } catch (err) {
      setError(err instanceof Error ? err.message : &quot;Login failed&quot;);
    }
  };

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input name=&quot;email&quot; type=&quot;email&quot; required /&amp;gt;
      &amp;lt;input name=&quot;password&quot; type=&quot;password&quot; required /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Sign in&amp;lt;/button&amp;gt;
      {error &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Doing all your auth logic client-side and assuming that's enough for SSR. Firebase's client SDK doesn't automatically make the logged-in user visible to your Next.js Server Components — you need a separate step to bridge that gap (next).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-5-bridge-the-session-to-the-server&quot;&gt;Step 5: Bridge the Session to the Server&lt;/h4&gt;
&lt;p&gt;Firebase's newer &lt;strong&gt;Firebase Server App&lt;/strong&gt; feature lets you read the authenticated user inside Server Components:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/firebase/server.ts
import { cookies } from &quot;next/headers&quot;;
import { initializeApp, initializeServerApp } from &quot;firebase/app&quot;;
import { getAuth } from &quot;firebase/auth&quot;;

export async function getAuthenticatedAppForUser() {
  const authIdToken = (await cookies()).get(&quot;__session&quot;)?.value;

  const firebaseServerApp = initializeServerApp(initializeApp(), {
    authIdToken,
  });

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Learn more &lt;code&gt;Firebase Server App&lt;/code&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://firebase.google.com/codelabs/firebase-nextjs#5&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the client sets a &lt;code&gt;__session&lt;/code&gt; cookie containing the Firebase ID token after login (you write this cookie yourself, typically via a small API route or &lt;code&gt;onIdTokenChanged&lt;/code&gt; listener). The server then rebuilds an authenticated Firebase app instance from that cookie — this is meaningfully more manual than Supabase's &lt;code&gt;@supabase/ssr&lt;/code&gt; package, which handles cookie syncing for you out of the box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This example demonstrates the server-side bridge rather than a complete production session-cookie implementation. In production, use a secure HTTP-only session cookie and validate it server-side.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Assuming Firebase Auth &quot;just works&quot; with Server Components the way it works with a client-only SPA. The App Router's server/client split means you need this extra bridging step — it's the single biggest source of Firebase + Next.js App Router complaints online.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-6-protect-data-with-firestore-security-rules&quot;&gt;Step 6: Protect Data with Firestore Security Rules&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /profiles/{userId} {
      allow read, update: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Firestore rules are enforced server-side by Google's infrastructure, similar in spirit to RLS — but written in a proprietary rules language instead of SQL, so there's a real learning curve if your team already knows SQL but not Firestore's rule syntax.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pricing-comparison&quot;&gt;Pricing Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Supabase Auth &lt;/th&gt;&lt;th&gt;Firebase Auth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Free tier &lt;/td&gt;&lt;td&gt;50,000 MAU &lt;/td&gt;&lt;td&gt;Depends on Firebase Auth / Identity Platform configuration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Paid tier &lt;/td&gt;&lt;td&gt;Included in Supabase plan quota; additional MAU billed after quota &lt;/td&gt;&lt;td&gt;Usage-based pricing with Identity Platform&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting &lt;/td&gt;&lt;td&gt;✅ Available &lt;/td&gt;&lt;td&gt;❌ Not available&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both platforms offer generous options for early-stage projects, although the exact limits and pricing depend on the plan and services you use. The real cost difference shows up later: Supabase gives you a self-hosting escape hatch if you ever want to leave the MAU pricing model entirely; Firebase does not.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;security-considerations-for-both&quot;&gt;Security Considerations for Both&lt;/h3&gt;
&lt;p&gt;Regardless of which you choose, keep these in mind for Next.js 16:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never trust client-side auth checks alone.&lt;/strong&gt; Whether you're using Supabase or Firebase, always re-verify the session inside Server Actions and Route Handlers — a hidden button in the UI doesn't stop a direct API call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;proxy.ts&lt;/code&gt; (formerly &lt;code&gt;middleware.ts&lt;/code&gt;) is a UX nicety, not your security boundary.&lt;/strong&gt; Both Supabase's session refresh and any Firebase token check you add there can be bypassed by calling your backend directly — the real gate is the check inside your Server Action or database rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default new users to the lowest-privilege role.&lt;/strong&gt; Never let a signup form set its own &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;is_admin&lt;/code&gt; field — assign it server-side.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-should-you-use-supabase-auth-&quot;&gt;When Should You Use Supabase Auth?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You want a real relational database (Postgres) under your app, not just auth.&lt;/li&gt;
&lt;li&gt;Your app has complex relationships — teams, roles, shared resources, multi-tenancy.&lt;/li&gt;
&lt;li&gt;You want the option to self-host later, or you like knowing you &lt;em&gt;could&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;You're already comfortable with SQL, or want to learn it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-should-you-use-firebase-auth-&quot;&gt;When Should You Use Firebase Auth?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You're already using Firestore, Cloud Functions, Firebase Cloud Messaging, or Google Analytics.&lt;/li&gt;
&lt;li&gt;Your data model is simple and document-shaped (a user has a profile, a handful of settings — no deep relational joins).&lt;/li&gt;
&lt;li&gt;You want Google's massive scale and long production track record specifically for mobile + web apps together.&lt;/li&gt;
&lt;li&gt;Your team already knows Firestore Security Rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;decision-table&quot;&gt;Decision Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;If you need...              &lt;/th&gt;&lt;th&gt;Better choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Next.js + PostgreSQL        &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SQL + relational data       &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;RLS-based authorization     &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting                &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Firestore                   &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Firebase ecosystem          &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Google Cloud integration    &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mobile + web Firebase stack &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complex relational SaaS     &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Document-oriented data      &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-switch-from-firebase-auth-to-supabase-auth-later-&quot;&gt;Can I switch from Firebase Auth to Supabase Auth later?&lt;/h4&gt;
&lt;p&gt;Yes, but it's real migration work. You'll need to migrate users and identities, account for password-hash compatibility and provider configuration, and rebuild authorization rules from your Firebase/Security Rules model into Supabase RLS.&lt;/p&gt;
&lt;h4 id=&quot;which-one-is-easier-for-a-complete-beginner-&quot;&gt;Which one is easier for a complete beginner?&lt;/h4&gt;
&lt;p&gt;Firebase Auth's client-only setup (no SSR bridging) is slightly faster to get a basic login working. But Supabase Auth's &lt;code&gt;@supabase/ssr&lt;/code&gt; package makes Next.js App Router integration smoother once you add server-side checks, which almost every real app eventually needs.&lt;/p&gt;
&lt;h4 id=&quot;does-supabase-auth-support-social-login-like-google-and-github-&quot;&gt;Does Supabase Auth support social login like Google and GitHub?&lt;/h4&gt;
&lt;p&gt;Yes — Supabase Auth supports Google, GitHub, Apple, Discord, and dozens of other OAuth providers, configured directly from the Supabase dashboard with no extra backend code required.&lt;/p&gt;
&lt;h4 id=&quot;is-firebase-auth-open-source-&quot;&gt;Is Firebase Auth open source?&lt;/h4&gt;
&lt;p&gt;No. Firebase Auth is a fully managed Google Cloud service — you can't self-host it or inspect its internals. Supabase Auth (part of the broader Supabase platform, built on the open-source GoTrue project) is open source and can be self-hosted.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-row-level-security-if-i-m-already-checking-auth-in-my-next-js-code-&quot;&gt;Do I need Row Level Security if I'm already checking auth in my Next.js code?&lt;/h4&gt;
&lt;p&gt;RLS protects your database from application-level mistakes and direct client access, but it does not protect against misuse of a secret/service-role key because those elevated keys bypass RLS.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-1&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Supabase Auth and Firebase Auth both get you a working login system fast — the real decision comes down to what's underneath: a relational Postgres database with SQL-based security, or Firestore's document model with Google's rules language. If your app has real relationships between users, teams, and data, Supabase's Postgres foundation will save you pain down the road. If you're already deep in the Google Cloud ecosystem, Firebase Auth keeps everything under one roof.&lt;/p&gt;
&lt;p&gt;Whichever you pick, remember the same rule applies to both: UI checks are for user experience, and server-side/database-level checks are your actual security boundary.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;helpful-free-tools&quot;&gt;Helpful Free Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — generate strict, pattern-specific Row Level Security policies in seconds instead of hand-writing SQL.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate validated schemas for your auth forms straight from JSON or TypeScript, including a ready-to-use Server Action.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24980/supabase-auth-vs-firebase-auth-which-one-should-you-choose-in-2026</guid>
<pubDate>Thu, 20 Aug 2026 10:26:22 +0000</pubDate>
</item>
<item>
<title>When 50,000 Firestore Reads Forced Me to Rethink My Attendance System</title>
<link>https://coderlegion.com/23407/when-50-000-firestore-reads-forced-me-to-rethink-my-attendance-system</link>
<description>&lt;p&gt;How an unexpected database limit led me to build an offline-first anti-proxy attendance system.&lt;/p&gt;
&lt;p&gt;Introduction&lt;/p&gt;
&lt;p&gt;When I started building an attendance system for my university's UBA (Unnat Bharat Abhiyan) club, I had one clear instruction from our coordinator:&lt;/p&gt;
&lt;p&gt;&quot;Build an MVP as soon as possible.&quot;&lt;/p&gt;
&lt;p&gt;So I did what most student developers would do. I focused on shipping features instead of watching dashboards.&lt;/p&gt;
&lt;p&gt;Every new feature meant refreshing pages, testing authentication, creating meetings, scanning QR codes, fixing bugs, and repeating the same workflow again and again.&lt;/p&gt;
&lt;p&gt;I was so focused on making the system work that I never noticed something else happening in the background.&lt;/p&gt;
&lt;p&gt;Every debug session was consuming Firestore reads.&lt;/p&gt;
&lt;p&gt;The Day Everything Stopped Working&lt;/p&gt;
&lt;p&gt;After several hours of development, it was finally time to demonstrate the MVP.&lt;/p&gt;
&lt;p&gt;Everything looked ready.&lt;/p&gt;
&lt;p&gt;The QR authentication worked.&lt;/p&gt;
&lt;p&gt;Meetings could be created.&lt;/p&gt;
&lt;p&gt;Students could scan codes.&lt;/p&gt;
&lt;p&gt;Then the application suddenly stopped communicating with Firestore.&lt;/p&gt;
&lt;p&gt;At first, I thought I had introduced a bug.&lt;/p&gt;
&lt;p&gt;I checked my code.&lt;/p&gt;
&lt;p&gt;Nothing seemed wrong.&lt;/p&gt;
&lt;p&gt;A few minutes later, I found the real culprit.&lt;/p&gt;
&lt;p&gt;I had unknowingly exhausted Firestore's free-tier quota of 50,000 document reads.&lt;/p&gt;
&lt;p&gt;Not because of production traffic.&lt;/p&gt;
&lt;p&gt;Not because thousands of students were using it.&lt;/p&gt;
&lt;p&gt;Simply because I had been debugging the application so much.&lt;/p&gt;
&lt;p&gt;That moment completely changed how I thought about the project.&lt;/p&gt;
&lt;p&gt;Waiting Wasn't an Option&lt;/p&gt;
&lt;p&gt;The easiest solution would have been to wait for the quota to reset.&lt;/p&gt;
&lt;p&gt;But this wasn't just another side project.&lt;/p&gt;
&lt;p&gt;The attendance system had to work during meetings.&lt;/p&gt;
&lt;p&gt;If the database became unavailable, attendance couldn't simply stop.&lt;/p&gt;
&lt;p&gt;So instead of asking:&lt;/p&gt;
&lt;p&gt;&quot;How do I reduce Firestore reads?&quot;&lt;/p&gt;
&lt;p&gt;I asked a different question.&lt;/p&gt;
&lt;p&gt;&quot;How can the application continue working even if Firestore becomes unavailable?&quot;&lt;/p&gt;
&lt;p&gt;That single question changed the architecture of the project.&lt;/p&gt;
&lt;p&gt;Building an Offline-First Attendance System&lt;/p&gt;
&lt;p&gt;The obvious answer was offline support.&lt;/p&gt;
&lt;p&gt;But attendance systems aren't ordinary CRUD applications.&lt;/p&gt;
&lt;p&gt;They also have to prevent proxy attendance.&lt;/p&gt;
&lt;p&gt;Moving offline couldn't mean compromising security.&lt;/p&gt;
&lt;p&gt;So the offline mode had to preserve the same guarantees as the online system.&lt;/p&gt;
&lt;p&gt;The application needed to:&lt;/p&gt;
&lt;p&gt;Continue recording attendance without Firestore.&lt;br&gt;
Prevent duplicate submissions.&lt;br&gt;
Maintain anti-proxy validation.&lt;br&gt;
Store attendance securely until synchronization.&lt;br&gt;
Upload records once connectivity or database access returned.&lt;/p&gt;
&lt;p&gt;Instead of treating offline mode as a backup feature, it became a core part of the system's architecture.&lt;/p&gt;
&lt;p&gt;Solving One Problem Revealed Another&lt;/p&gt;
&lt;p&gt;The offline architecture solved the immediate issue.&lt;/p&gt;
&lt;p&gt;But I still wasn't happy.&lt;/p&gt;
&lt;p&gt;The original problem hadn't really been &quot;no internet.&quot;&lt;/p&gt;
&lt;p&gt;It was inefficient database usage.&lt;/p&gt;
&lt;p&gt;So I started reviewing every interaction with Firestore.&lt;/p&gt;
&lt;p&gt;Questions I hadn't asked before suddenly became important.&lt;/p&gt;
&lt;p&gt;Do I really need this read?&lt;br&gt;
Can this data be cached?&lt;br&gt;
Can multiple requests be combined?&lt;br&gt;
Why am I fetching the same document repeatedly?&lt;/p&gt;
&lt;p&gt;I realized that improving resilience wasn't enough.&lt;/p&gt;
&lt;p&gt;The system also needed to become more efficient.&lt;/p&gt;
&lt;p&gt;Reducing Firestore Reads&lt;/p&gt;
&lt;p&gt;Once the MVP was stable again, I began optimizing the backend.&lt;/p&gt;
&lt;p&gt;Some of the improvements included:&lt;/p&gt;
&lt;p&gt;Reducing unnecessary document reads.&lt;br&gt;
Avoiding repeated requests for unchanged data.&lt;br&gt;
Caching frequently accessed information using Render's free cache.&lt;br&gt;
Optimizing database queries.&lt;br&gt;
Reducing redundant writes wherever possible.&lt;/p&gt;
&lt;p&gt;None of these optimizations changed the features users saw.&lt;/p&gt;
&lt;p&gt;But together, they significantly reduced the load on Firestore and made the application far more sustainable on the free tier.&lt;/p&gt;
&lt;p&gt;What This Experience Taught Me&lt;/p&gt;
&lt;p&gt;Looking back, exhausting Firestore's free-tier quota was frustrating.&lt;/p&gt;
&lt;p&gt;But it also forced me to think like a software engineer rather than just a programmer.&lt;/p&gt;
&lt;p&gt;I stopped asking:&lt;/p&gt;
&lt;p&gt;&quot;How do I make this feature work?&quot;&lt;/p&gt;
&lt;p&gt;and started asking:&lt;/p&gt;
&lt;p&gt;&quot;How do I make this system keep working when one of its dependencies fails?&quot;&lt;/p&gt;
&lt;p&gt;That shift in mindset led to a better architecture than the one I originally planned.&lt;/p&gt;
&lt;p&gt;Sometimes the best engineering decisions aren't made during planning.&lt;/p&gt;
&lt;p&gt;They're made when reality breaks your assumptions.&lt;/p&gt;
&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;p&gt;The attendance system I set out to build was meant to prevent proxy attendance.&lt;/p&gt;
&lt;p&gt;The system I ended up building also learned how to survive database limits, continue operating offline, and make smarter use of backend resources.&lt;/p&gt;
&lt;p&gt;Running out of Firestore reads became one of the most valuable debugging sessions of the entire project.&lt;/p&gt;
&lt;p&gt;💬 I'd love to hear your thoughts.&lt;/p&gt;
&lt;p&gt;Have you ever hit a free-tier limit or an unexpected infrastructure constraint that completely changed your system design? Share your experience in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23407/when-50-000-firestore-reads-forced-me-to-rethink-my-attendance-system</guid>
<pubDate>Fri, 24 Jul 2026 18:32:03 +0000</pubDate>
</item>
<item>
<title>Demystifying the Ghost Failure in Firebase Hosting GitHub Actions</title>
<link>https://coderlegion.com/21748/demystifying-the-ghost-failure-in-firebase-hosting-github-actions</link>
<description>&lt;p&gt;Imagine your pipeline throwing a red error badge, yet your production application reflects fresh updates perfectly. This exact scenario confuses countless developers utilizing the official Firebase Extended GitHub Action. The workflow crashes with a cryptic warning message stating that the supplied version is the current active version, casting doubt on your continuous deployment pipeline reliability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redundant Release Triggers:&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;The pipeline triggers file hashing and bundles content assets accurately, uploading them to Firebase storage infrastructure seamlessly.&lt;br&gt;
Idempotent State Conflicts: When consecutive builds contain zero file modifications, Firebase optimization logic flags the release bundle as structurally identical to the live production build.&lt;br&gt;
Aggressive Exit Code Flags: The deployment utility interprets this identical state as an operational failure, returning a non-zero exit status that forcefully breaks your workflow chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🛠️ The Modern Implementation Strategy:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Conditional Deployment Guards: Introduce a dynamic file change detection mechanism directly inside your continuous integration step definitions.&lt;br&gt;
Idempotent Workflow Controls: Implement custom workflow scripts leveraging the core Firebase CLI directly instead of using abstract, rigid pre-built actions.&lt;br&gt;
Graceful Exit Optimization: Append standard bash execution overrides to capture identical version notices and rewrite exit statuses cleanly to neutral status levels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔗 visit our official site: www.nextbigcreative.com&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21748/demystifying-the-ghost-failure-in-firebase-hosting-github-actions</guid>
<pubDate>Thu, 02 Jul 2026 05:48:04 +0000</pubDate>
</item>
<item>
<title>How to Fix FCM &quot;MismatchSenderId&quot; in Multi-Environment Expo (EAS) Builds</title>
<link>https://coderlegion.com/21444/how-to-fix-fcm-mismatchsenderid-in-multi-environment-expo-eas-builds</link>
<description>&lt;h2 id=&quot;how-to-fix-mismatchsenderid-in-expo-eas-push-notifications-across-multiple-environments&quot;&gt;How to Fix &lt;code&gt;MismatchSenderId&lt;/code&gt; in Expo EAS Push Notifications Across Multiple Environments&lt;/h2&gt;
&lt;p&gt;If you are managing multiple environments (Staging, Preview, Production) in a React Native Expo app, setting up Firebase Cloud Messaging (FCM) can quickly turn into a credential nightmare.&lt;/p&gt;
&lt;p&gt;You set up separate Firebase projects for each environment, download your config files, and configure your service accounts. Everything looks right. But when you test push notifications on your staging build, you hit a wall:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: MismatchSenderId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This error doesn't mean your code is broken. It means you've hit a structural limitation in how Expo EAS handles push credentials.&lt;/p&gt;
&lt;p&gt;Here is exactly why this happens and how to architect a clean solution.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-the-mismatch-happens&quot;&gt;Why the Mismatch Happens&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;MismatchSenderId&lt;/code&gt; error occurs because of a strict identity mismatch:&lt;/p&gt;
&lt;p&gt;The device token was generated by Firebase Project A, but the notification payload was dispatched using credentials from Firebase Project B.&lt;/p&gt;
&lt;p&gt;In a standard web or backend setup, this is easy to manage. But in the Expo ecosystem, you run into the EAS Project Limit:&lt;/p&gt;
&lt;p&gt;Expo maps push notification service accounts per EAS Project, not per EAS Build Profile (&lt;code&gt;development&lt;/code&gt;, &lt;code&gt;preview&lt;/code&gt;, &lt;code&gt;production&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;When you run &lt;code&gt;eas credentials&lt;/code&gt; or use the automated CLI prompts, EAS expects one default push notification Service Account (SA) key for the entire project.&lt;/p&gt;
&lt;p&gt;If you try to reuse or attach the same service account across different Google Cloud/Firebase projects via IAM permissions, FCM will reject the cross-project token request, throwing &lt;code&gt;MismatchSenderId&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;solution-1-split-your-eas-projects-recommended-for-clean-automation-&quot;&gt;Solution 1: Split Your EAS Projects (Recommended for Clean Automation)&lt;/h2&gt;
&lt;p&gt;If you want to rely on Expo's automated push notification infrastructure without overriding credentials constantly, the cleanest architectural fix is to separate your environments at the EAS project level.&lt;/p&gt;
&lt;p&gt;Instead of one EAS project with multiple build profiles, initialize separate EAS projects in your ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;your-app-staging&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;your-app-production&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;implementation-steps&quot;&gt;Implementation Steps&lt;/h3&gt;
&lt;p&gt;In your &lt;code&gt;app.json&lt;/code&gt;, dynamically switch the &lt;code&gt;expo.projectId&lt;/code&gt; and &lt;code&gt;expo.slug&lt;/code&gt; based on an environment variable during your build step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;Your App&quot;,
    &quot;slug&quot;: &quot;your-app-production&quot;,
    &quot;extra&quot;: {
      &quot;eas&quot;: {
        &quot;projectId&quot;: &quot;YOUR-PROD-PROJECT-ID&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Link your staging build profile to your staging EAS project.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;eas credentials&lt;/code&gt; for each project separately. This allows you to upload the staging FCM Service Account to your staging EAS project, and the production FCM Service Account to your production EAS project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;solution-2-bypass-eas-and-route-tokens-on-your-backend&quot;&gt;Solution 2: Bypass EAS and Route Tokens on Your Backend&lt;/h2&gt;
&lt;p&gt;If separating your EAS projects isn't an option and you need to keep everything under a single Expo App ID, you have to stop relying on Expo's unified push notification server and handle routing downstream.&lt;/p&gt;
&lt;h3 id=&quot;implementation-steps-1&quot;&gt;Implementation Steps&lt;/h3&gt;
&lt;h4 id=&quot;tag-tokens-by-environment&quot;&gt;Tag Tokens by Environment&lt;/h4&gt;
&lt;p&gt;When your app requests a push notification token using &lt;code&gt;Expo.getExpoPushTokenAsync()&lt;/code&gt;, append the current build environment metadata before sending it to your database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const token = await Expo.getExpoPushTokenAsync();

// Save to backend alongside the environment flag
await saveTokenToBackend({
  token: token.data,
  environment: process.env.EXPO_PUBLIC_APP_ENV // 'staging' or 'production'
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;handle-routing-on-your-server&quot;&gt;Handle Routing on Your Server&lt;/h4&gt;
&lt;p&gt;On your backend infrastructure, do not use a single global Firebase initialization instance.&lt;/p&gt;
&lt;p&gt;Initialize multiple Firebase Admin SDK instances using the respective service account JSON keys for each environment.&lt;/p&gt;
&lt;p&gt;When triggering a notification, check the token's environment tag and dispatch it explicitly through the matching Firebase Admin instance.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;verification-checklist-before-your-next-build&quot;&gt;Verification Checklist Before Your Next Build&lt;/h2&gt;
&lt;p&gt;Before you trigger your next EAS build, double-check that your native configuration files aren't bleeding into each other:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Config Plugins:&lt;/strong&gt; Ensure your &lt;code&gt;app.json&lt;/code&gt; or &lt;code&gt;app.config.js&lt;/code&gt; uses a dynamic config plugin to load &lt;code&gt;google-services.json&lt;/code&gt; (Android) and &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; (iOS) based on the target build profile. If your staging build accidentally bundles the production &lt;code&gt;google-services.json&lt;/code&gt;, your token identities will instantly mismatch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sender IDs:&lt;/strong&gt; Verify that the sender ID inside your bundled client-side Firebase config explicitly matches the project ID of the service account used by your notification server.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;I actually built a quick web tool called &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fixmyerrorapp.com&quot;&gt;Fix My Error&lt;/a&gt; to automate troubleshooting these kinds of React Native, Expo, and Gradle build traps. If you're stuck on a cryptic error log, feel free to drop it in there to grab the configuration fix.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/21444/how-to-fix-fcm-mismatchsenderid-in-multi-environment-expo-eas-builds</guid>
<pubDate>Sat, 27 Jun 2026 09:03:38 +0000</pubDate>
</item>
<item>
<title>The Weekend I Finally Finished 17 Projects</title>
<link>https://coderlegion.com/20345/the-weekend-i-finally-finished-17-projects</link>
<description>&lt;p&gt;You know that feeling when you open a folder you haven't touched in months and find... yourself?&lt;/p&gt;
&lt;p&gt;Not metaphorically. I mean literally — old projects you don't even remember writing, with code that's somehow &lt;em&gt;good&lt;/em&gt;? And you're sitting there like, &quot;wait, &lt;em&gt;I&lt;/em&gt; wrote this?&quot;&lt;/p&gt;
&lt;p&gt;That was my Saturday.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mess&quot;&gt;The Mess&lt;/h3&gt;
&lt;p&gt;I decided it was time. My portfolio was a ghost town. My GitHub was full of half-finished dreams. You know the drill, you start a project, get 80% there, then a shinier thing comes along and suddenly it's gathering dust for a year.&lt;/p&gt;
&lt;p&gt;I had 17 of those.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LIS&lt;/strong&gt; — An AI tutoring app for Cameroonian students. Works offline. Has a &lt;em&gt;Learning DNA&lt;/em&gt; that adapts to how you study. I literally submitted this for a social impact prize and then... just left it there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mbende Stay&lt;/strong&gt; — A booking platform with mobile money payments. Fully functional. Zero visitors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;418 I'm a Teapot&quot;&lt;/strong&gt; — A game where you play as a teapot that can never win and an AI &lt;em&gt;roasts you&lt;/em&gt; every time you fail. I'm not kidding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;elementTouch&lt;/strong&gt; — A JavaScript library I built from scratch. Drag and drop. Typewriter effects. A whole code editor. Just... sitting there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And I'm looking at all of this thinking: &lt;em&gt;bro, you really just built things and then forgot to tell anyone?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So I made a decision: polish them all, put them in my portfolio, and actually show the work.&lt;/p&gt;
&lt;p&gt;First step: build an admin import tool. One click, seed 17 projects into Firestore. Felt like a genius.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;then-the-universe-laughed&quot;&gt;Then the Universe Laughed&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ReferenceError: location is not defined
    at v (.next/server/app/admin/blog/new/page.js:1:11128)
    at y (.next/server/app/admin/projects/import/page.js:1:16875)
    at y (.next/server/app/admin/projects/new/page.js:1:8262)
    at j (.next/server/app/admin/resumes/new/page.js:1:4056)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four pages. Dead. And I have &lt;em&gt;no idea&lt;/em&gt; where &lt;code&gt;location&lt;/code&gt; is coming from.&lt;/p&gt;
&lt;p&gt;You ever debug a Firebase + Next.js app and feel like you're chasing a ghost? Because I was definitely chasing a ghost.&lt;/p&gt;
&lt;p&gt;Turns out, Firebase's SDK references &lt;code&gt;location&lt;/code&gt; (as in &lt;code&gt;window.location&lt;/code&gt;) at the module level. Not inside a function you call. At &lt;em&gt;import time&lt;/em&gt;. So when Next.js tries to server-render your admin page, Node.js is like &quot;what's &lt;code&gt;location&lt;/code&gt;? never heard of her.&quot;&lt;/p&gt;
&lt;p&gt;The fix?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import dynamic from &quot;next/dynamic&quot;
export default dynamic(() =&amp;gt; import(&quot;./page.client&quot;), { ssr: false })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrapped each page in &lt;code&gt;next/dynamic&lt;/code&gt; with &lt;code&gt;ssr: false&lt;/code&gt;. Basically told Next.js: &lt;em&gt;&quot;Don't even look at this page on the server. Not once. Not ever. Just send the JS and let the browser figure it out.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Moved all the actual code to &lt;code&gt;page.client.tsx&lt;/code&gt;. Build passed. I felt like a god for approximately 12 minutes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-betrayal&quot;&gt;The Betrayal&lt;/h3&gt;
&lt;p&gt;Then I looked at my blog.&lt;/p&gt;
&lt;p&gt;You see, my blog posts are stored in Firestore as HTML. And somewhere along the way, I'd written them with beautiful Tailwind dark mode classes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;not-dark:bg-white dark:bg-gray-900 dark:shadow-lg&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's the problem: &lt;strong&gt;Tailwind v4's JIT compiler has never heard of your database.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It scans your source files — your &lt;code&gt;app/&lt;/code&gt;, your &lt;code&gt;components/&lt;/code&gt;, your &lt;code&gt;lib/&lt;/code&gt;. It does NOT make a Firebase query to find class names.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;dark:bg-gray-900&lt;/code&gt; in a blog post? Never compiled. It renders as plain text with zero styles. My &quot;beautiful blog content&quot; was just... floating in the void. No background. No shadow. Nothing.&lt;/p&gt;
&lt;p&gt;I sat there staring at the screen feeling personally betrayed by a CSS framework.&lt;/p&gt;
&lt;p&gt;But hey, I fixed it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function prepareHtml(html: string): string {
  return html.replace(/\b(?:not-)?dark:[^\s&quot;'&amp;gt;]+/g, &quot;&quot;).trim()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Strip the prefixes. Then override everything with CSS variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.blog-content .text-gray-900 { color: inherit !important; }
.blog-content .bg-white, .blog-content .bg-gray-900 { background: transparent !important; }
.blog-content .shadow-md, .blog-content .shadow-lg { box-shadow: none !important; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now every hardcoded class respects the theme. Dark mode, light mode, it just works. The blog content is finally alive.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;then-there-were-17&quot;&gt;Then There Were 17&lt;/h3&gt;
&lt;p&gt;So now I've got 17 projects in my portfolio. On one page. One. Grid. Screen. You see the problem.&lt;/p&gt;
&lt;p&gt;I needed pagination. Not the boring kind — something that actually looks good:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chevrons that bounce when you hover&lt;/li&gt;
&lt;li&gt;Page numbers with the accent color on active&lt;/li&gt;
&lt;li&gt;Ellipsis that only show when there's actually a gap&lt;/li&gt;
&lt;li&gt;A quiet little &quot;Page X of Y · N projects&quot; at the bottom&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The button logic nearly broke me. You try handling &quot;first page, last page, exactly 5 pages, exactly 6 pages, ellipsis-or-no-ellipsis&quot; without your brain melting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const pages = useMemo(() =&amp;gt; {
  const p: (number | &quot;...&quot;)[] = []
  if (totalPages &amp;lt;= 5) {
    for (let i = 0; i &amp;lt; totalPages; i++) p.push(i)
  } else {
    p.push(0)
    if (safePage &amp;gt; 2) p.push(&quot;...&quot;)
    // ... more edge cases
  }
  return p
}, [safePage, totalPages])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. It's clean. I'm proud of it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-actually-learned&quot;&gt;What I Actually Learned&lt;/h3&gt;
&lt;h4 id=&quot;1-you-probably-have-better-code-than-you-think&quot;&gt;1. You probably have better code than you think&lt;/h4&gt;
&lt;p&gt;Open that old project. The one you're embarrassed about. I bet it's not as bad as you remember. We're all way too hard on ourselves.&lt;/p&gt;
&lt;h4 id=&quot;2-admin-pages-don-t-need-ssr&quot;&gt;2. Admin pages don't need SSR&lt;/h4&gt;
&lt;p&gt;If your page is behind a login wall, there's zero SEO value in server-rendering it. Just use &lt;code&gt;ssr: false&lt;/code&gt; and save yourself the headache. Your admin panel will survive without being indexed by Google.&lt;/p&gt;
&lt;h4 id=&quot;3-tailwind-v4-jit-is-amazing-until-your-content-is-dynamic&quot;&gt;3. Tailwind v4 JIT is amazing until your content is dynamic&lt;/h4&gt;
&lt;p&gt;If your HTML lives in a database, plan ahead. CSS variables are your escape hatch. Hardcoded utility classes in data strings will come back to haunt you.&lt;/p&gt;
&lt;h4 id=&quot;4-a-portfolio-is-never-done&quot;&gt;4. A portfolio is never done&lt;/h4&gt;
&lt;p&gt;Every time I thought &quot;okay this is final,&quot; I found another project, another bug, another thing to improve. That's not failure. That's just what building in public looks like.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-left&quot;&gt;What's Left&lt;/h3&gt;
&lt;p&gt;elementTouch still has 2 unfinished tasks out of 7. I'll get to them.&lt;/p&gt;
&lt;p&gt;And there's a missing &lt;code&gt;admin/blog/edit/[id]&lt;/code&gt; page that keeps crashing the build. One day.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Anyway, that's my weekend. What's the most embarrassing bug you've fought with Next.js + Firebase? Drop it below so I don't feel alone.&lt;br&gt;
Here's my portfolio btw: &lt;a rel=&quot;nofollow&quot; href=&quot;https://adodanielnj.vercel.app/&quot;&gt;Portfolio&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20345/the-weekend-i-finally-finished-17-projects</guid>
<pubDate>Fri, 12 Jun 2026 00:14:34 +0000</pubDate>
</item>
<item>
<title>I Built an AI-Powered Agricultural Logistics Platform for Farmers Worldwide — Here's How</title>
<link>https://coderlegion.com/19388/i-built-an-ai-powered-agricultural-logistics-platform-for-farmers-worldwide-heres-how</link>
<description>&lt;p&gt;16% of agricultural produce globally is lost every year before it reaches the buyer. Not because the crops are bad. Because the supply chain is broken.&lt;br&gt;
Farmers have no digital way to list surplus crops. Buyers can't discover what's available nearby. Drivers get no coordination layer. Everyone loses.&lt;br&gt;
I built Micro-Harvest to fix all three — at the same time — using a single AI conversation.&lt;/p&gt;
&lt;p&gt;What Is Micro-Harvest?&lt;br&gt;
Micro-Harvest is a three-sided agricultural logistics platform built for the Google Cloud Rapid Agent Hackathon 2026 (Elastic Track). It connects:&lt;/p&gt;
&lt;p&gt; Farmers (Growers) — who list surplus crops via an AI chat agent&lt;br&gt;
 Buyers (Producers) — who discover listings through smart geo-search&lt;br&gt;
 Drivers (Transporters) — who get auto-assigned hauls and handle delivery&lt;/p&gt;
&lt;p&gt;One conversation from the farmer triggers the entire chain — AI reasoning, buyer matching, driver dispatch, and Stripe payment settlement. All automated.&lt;br&gt;
 Watch the full demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/0ZdplkglDBo&quot;&gt;https://youtu.be/0ZdplkglDBo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The Core Idea: One Message Does Everything&lt;br&gt;
Here's what happens when a farmer speaks or types:&lt;/p&gt;
&lt;p&gt;&quot;I have 20 macro bins of Merlot ready to go today, asking $4000 per ton&quot;&lt;/p&gt;
&lt;p&gt;Under the hood, in under 30 seconds:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Gemini 3.1 Flash extracts → crop type, quantity, price, urgency&lt;/li&gt;
&lt;li&gt;AI Reasoning Engine scores → urgency level, weather risk, vehicle recommendation&lt;/li&gt;
&lt;li&gt;Elastic MCP (24 tools) → geo-matches nearest available buyer + transporter&lt;/li&gt;
&lt;li&gt;FCM push notification → driver gets haul assignment&lt;/li&gt;
&lt;li&gt;Driver completes → Gate 1 pickup + Gate 2 delivery confirmation&lt;/li&gt;
&lt;li&gt;Stripe → 80% to farmer, 15% to driver, 5% platform fee. Same day.&lt;br&gt;
No phone calls. No middlemen. No 3-day delays.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Tech Stack-&lt;br&gt;
AI Agent - Google Agent Platform, LLM - Gemini 3.1 Flash Lite Preview, Search - Elastic MCP — 24 tools, Backend - Firebase (Firestore, Auth, Cloud Functions) Payments - Stripe, Mobile - Flutter (3 Android apps) Admin - Flutter Web&lt;/p&gt;
&lt;p&gt;The Elastic MCP server is the heart of the matching layer. It exposes 24 tools covering semantic crop search, geo-distance queries, transporter availability matching, index health monitoring, and BigQuery analytics. Every search the buyer runs fires a real geo-distance query against the listings index in real time — you can watch it in the Elastic search profiler.&lt;/p&gt;
&lt;p&gt;The Feature I'm Most Proud Of: Offline-First Listing&lt;br&gt;
Farmers often operate in areas with poor connectivity. Someone out in a field with no signal shouldn't lose their listing.&lt;br&gt;
So I built the Harvest Agent to be offline-first:&lt;/p&gt;
&lt;p&gt;Farmer speaks or types their crop details — no internet detected&lt;br&gt;
App shows an offline badge and queues the message locally&lt;br&gt;
The moment connectivity returns — listing syncs and goes live automatically&lt;br&gt;
Zero farmer action needed&lt;/p&gt;
&lt;p&gt;This was the hardest part to get right. The local queue needed to survive app restarts, handle partial sync failures, and not create duplicate listings on reconnect. It took more debugging than the entire AI integration.&lt;/p&gt;
&lt;p&gt;Three Apps, One Platform&lt;br&gt;
All three apps are built in Flutter and share the same Firebase project:&lt;br&gt;
Farmer App (Grower)&lt;/p&gt;
&lt;p&gt;Voice + text AI listing creation&lt;br&gt;
Offline message queue with auto-sync&lt;br&gt;
Real-time listing status tracking&lt;br&gt;
Earnings dashboard&lt;/p&gt;
&lt;p&gt;Buyer App (Producer)&lt;/p&gt;
&lt;p&gt;Natural language surplus search&lt;br&gt;
Geo-filtered results with radius control&lt;br&gt;
Claim and reserve listings&lt;br&gt;
Live transport tracking&lt;/p&gt;
&lt;p&gt;Driver App (Transporter)&lt;/p&gt;
&lt;p&gt;FCM haul notifications&lt;br&gt;
GPS route guidance to farm&lt;br&gt;
Gate 1 + Gate 2 photo capture&lt;br&gt;
Instant payment on delivery confirmation&lt;/p&gt;
&lt;p&gt;You can download and install all three APKs from the GitHub release and walk through the complete end-to-end flow yourself. APK links are in the Links section at the bottom.&lt;br&gt;
(Enable &quot;Install from unknown sources&quot; in Android settings first)&lt;/p&gt;
&lt;p&gt;What I'd Build Next&lt;br&gt;
A few things I want to add after the hackathon:&lt;/p&gt;
&lt;p&gt;Price intelligence — Gemini currently recommends a price band but doesn't pull live commodity market rates; integrating real-time agri pricing APIs would make this genuinely powerful&lt;br&gt;
iOS builds — currently Android only; the Flutter code is cross-platform but Apple provisioning didn't make the deadline&lt;br&gt;
Multi-language AI agent — supporting regional languages so the Harvest Agent works natively for farmers regardless of where they are in the world&lt;/p&gt;
&lt;p&gt;Links&lt;br&gt;
 Demo Video&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/0ZdplkglDBo&quot;&gt;https://youtu.be/0ZdplkglDBo&lt;/a&gt;&lt;br&gt;
 Landing Page&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://micro-harvest-landing.web.app&quot;&gt;https://micro-harvest-landing.web.app&lt;/a&gt;&lt;br&gt;
️ Admin Panel&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://micro-harvest.web.app&quot;&gt;https://micro-harvest.web.app&lt;/a&gt;&lt;br&gt;
 GitHub&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/micro-harvest&quot;&gt;https://github.com/ssurekumar01111-hue/micro-harvest&lt;/a&gt;&lt;br&gt;
 Farmer APK&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/grower-release.apk&quot;&gt;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/grower-release.apk&lt;/a&gt;&lt;br&gt;
 Buyer APK&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/producer-release.apk&quot;&gt;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/producer-release.apk&lt;/a&gt;&lt;br&gt;
 Driver APK&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/transporter-release.apk&quot;&gt;https://github.com/ssurekumar01111-hue/micro-harvest/releases/download/v1.0.0/transporter-release.apk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Building this solo in Flutter + Firebase was genuinely fun — the Google Agent Platform + Elastic MCP combination is seriously powerful for this kind of multi-party matching problem. Happy to answer questions about any part of the architecture in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19388/i-built-an-ai-powered-agricultural-logistics-platform-for-farmers-worldwide-heres-how</guid>
<pubDate>Mon, 01 Jun 2026 06:04:51 +0000</pubDate>
</item>
<item>
<title>Building a Modern Real-Time Collaborative Document Editor — Full Stack Engineering Journey</title>
<link>https://coderlegion.com/19196/building-modern-real-time-collaborative-document-editor-full-stack-engineering-journey</link>
<description>&lt;p&gt;In today’s digital world, real-time collaboration platforms have become one of the most important categories of modern web applications. From team collaboration tools to cloud-based editors, users now expect instant synchronization, secure authentication, responsive interfaces and seamless teamwork experiences.&lt;/p&gt;
&lt;p&gt;To strengthen my full-stack engineering skills and gain deeper practical experience in scalable real-time systems, I started working on a &lt;strong&gt;Modern Real-Time Collaborative Document Editor&lt;/strong&gt; during my internship journey at TEYZIX CORE.&lt;/p&gt;
&lt;p&gt;This project was designed as a modern collaborative platform where multiple users can work together on documents in real time while maintaining synchronization, authentication, responsiveness and structured backend architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;project-overview&quot;&gt;Project Overview&lt;/h2&gt;
&lt;p&gt;The main goal of this project was to build a scalable and modern collaborative editing platform using a complete full-stack architecture.&lt;/p&gt;
&lt;p&gt;The application allows users to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create and manage documents&lt;/li&gt;
&lt;li&gt;Collaborate with multiple users simultaneously&lt;/li&gt;
&lt;li&gt;Experience real-time synchronization&lt;/li&gt;
&lt;li&gt;Use secure authentication systems&lt;/li&gt;
&lt;li&gt;Access a responsive and modern UI/UX&lt;/li&gt;
&lt;li&gt;Work with live socket communication&lt;/li&gt;
&lt;li&gt;Handle scalable backend APIs&lt;/li&gt;
&lt;li&gt;Maintain structured project architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than creating a beginner-level CRUD application, this project focused more on solving real-world engineering challenges such as real-time communication, state synchronization, authentication workflows, API structuring, socket event management and scalable architecture planning.&lt;/p&gt;
&lt;p&gt;This project falls between an intermediate and advanced level full-stack engineering system.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;technologies-used&quot;&gt;Technologies Used&lt;/h2&gt;
&lt;h3 id=&quot;frontend-technologies&quot;&gt;Frontend Technologies&lt;/h3&gt;
&lt;p&gt;The frontend of the application was developed using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React.js&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Context API&lt;/li&gt;
&lt;li&gt;Custom Hooks&lt;/li&gt;
&lt;li&gt;Socket.IO Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;React.js was used to create reusable and scalable UI components, while Vite provided a fast and optimized development environment.&lt;/p&gt;
&lt;p&gt;Tailwind CSS helped in building a modern responsive interface with clean layouts and consistent styling.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;backend-technologies&quot;&gt;Backend Technologies&lt;/h3&gt;
&lt;p&gt;The backend architecture was built using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Express.js&lt;/li&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;Socket.IO&lt;/li&gt;
&lt;li&gt;Middleware Architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Express.js handled routing, middleware management, API handling and backend scalability.&lt;/p&gt;
&lt;p&gt;Socket.IO played a major role in enabling real-time communication between users connected to the same document.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;database-amp-authentication&quot;&gt;Database &amp;amp; Authentication&lt;/h3&gt;
&lt;p&gt;For backend services and cloud storage integration, Firebase services were used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firebase Authentication&lt;/li&gt;
&lt;li&gt;Firestore Database&lt;/li&gt;
&lt;li&gt;Firebase Admin SDK&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Firestore was selected because of its real-time capabilities, flexibility, scalability and easy integration with modern JavaScript applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-time-collaboration-system&quot;&gt;Real-Time Collaboration System&lt;/h2&gt;
&lt;p&gt;One of the most important parts of this project was implementing real-time synchronization.&lt;/p&gt;
&lt;p&gt;When multiple users edit the same document simultaneously, the system must instantly sync updates between connected clients without refreshing the page.&lt;/p&gt;
&lt;p&gt;This functionality was implemented using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Socket.IO event communication&lt;/li&gt;
&lt;li&gt;Real-time listeners&lt;/li&gt;
&lt;li&gt;Shared document state management&lt;/li&gt;
&lt;li&gt;Live synchronization handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The application was designed to handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time document updates&lt;/li&gt;
&lt;li&gt;Live user presence&lt;/li&gt;
&lt;li&gt;Connection/disconnection events&lt;/li&gt;
&lt;li&gt;Collaborative editing workflows&lt;/li&gt;
&lt;li&gt;Sync reliability between multiple users&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;project-architecture&quot;&gt;Project Architecture&lt;/h2&gt;
&lt;p&gt;The project followed a structured full-stack architecture with separate client and server environments.&lt;/p&gt;
&lt;h3 id=&quot;frontend-structure&quot;&gt;Frontend Structure&lt;/h3&gt;
&lt;p&gt;The frontend included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reusable components&lt;/li&gt;
&lt;li&gt;Route protection&lt;/li&gt;
&lt;li&gt;Context management&lt;/li&gt;
&lt;li&gt;Socket services&lt;/li&gt;
&lt;li&gt;Authentication services&lt;/li&gt;
&lt;li&gt;API service layer&lt;/li&gt;
&lt;li&gt;Custom hooks&lt;/li&gt;
&lt;li&gt;Theme management&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;backend-structure&quot;&gt;Backend Structure&lt;/h3&gt;
&lt;p&gt;The backend included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controllers&lt;/li&gt;
&lt;li&gt;Services&lt;/li&gt;
&lt;li&gt;Middleware&lt;/li&gt;
&lt;li&gt;Routes&lt;/li&gt;
&lt;li&gt;Socket handlers&lt;/li&gt;
&lt;li&gt;Validators&lt;/li&gt;
&lt;li&gt;Utilities&lt;/li&gt;
&lt;li&gt;Firebase configuration&lt;/li&gt;
&lt;li&gt;Authentication handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project structure was designed to remain scalable, maintainable, and production-ready.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;major-challenges-faced-during-development&quot;&gt;Major Challenges Faced During Development&lt;/h2&gt;
&lt;p&gt;Every real-world project comes with technical challenges and this project was no exception.&lt;/p&gt;
&lt;p&gt;Although most parts of the system worked smoothly two major technical issues became the biggest learning experiences during development.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-firebase-api-amp-backend-configuration&quot;&gt;1. Firebase API &amp;amp; Backend Configuration&lt;/h3&gt;
&lt;p&gt;The first major challenge was configuring the Firebase Admin SDK correctly with the backend environment.&lt;/p&gt;
&lt;p&gt;At first, the backend was running successfully but Firebase services were not connecting properly because the required environment variables and service account credentials were missing or incorrectly formatted.&lt;/p&gt;
&lt;p&gt;The main issues included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Firebase private key formatting&lt;/li&gt;
&lt;li&gt;Environment variable handling&lt;/li&gt;
&lt;li&gt;Admin SDK initialization&lt;/li&gt;
&lt;li&gt;Secure credential management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After debugging the configuration and properly connecting the Firebase service account credentials the backend started communicating successfully with Firebase services.&lt;/p&gt;
&lt;p&gt;This experience helped me understand how important environment configuration and secure credential handling are in production-level applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-authentication-data-not-appearing-in-fire-store&quot;&gt;2. Authentication Data Not Appearing in Fire store&lt;/h3&gt;
&lt;p&gt;The second major issue involved authentication data storage.&lt;/p&gt;
&lt;p&gt;Users were able to sign up and log in successfully using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Authentication&lt;/li&gt;
&lt;li&gt;Email &amp;amp; Password Authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, the user information was not appearing inside Firestore collections as expected.&lt;/p&gt;
&lt;p&gt;Initially, this created confusion because authentication itself was working correctly.&lt;/p&gt;
&lt;p&gt;Later I understood that:&lt;/p&gt;
&lt;p&gt;Firebase Authentication and Fire store Database are separate systems.&lt;/p&gt;
&lt;p&gt;Authentication stores user accounts inside the Firebase Authentication panel, while Fire store stores custom database documents separately.&lt;/p&gt;
&lt;p&gt;To save user information in Fire store, additional database logic must be written manually using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;addDoc()&lt;/li&gt;
&lt;li&gt;setDoc()&lt;/li&gt;
&lt;li&gt;collection()&lt;/li&gt;
&lt;li&gt;updateDoc()&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This debugging process improved my understanding of how authentication systems and NoSQL databases work together in modern cloud applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;technical-engineering-considerations&quot;&gt;Technical Engineering Considerations&lt;/h2&gt;
&lt;p&gt;While developing this system, several important software engineering practices were considered.&lt;/p&gt;
&lt;h3 id=&quot;real-time-reliability&quot;&gt;Real-Time Reliability&lt;/h3&gt;
&lt;p&gt;The synchronization system needed to remain stable and responsive between multiple connected users.&lt;/p&gt;
&lt;h3 id=&quot;scalable-code-structure&quot;&gt;Scalable Code Structure&lt;/h3&gt;
&lt;p&gt;The project was organized using a modular architecture for maintainability and scalability.&lt;/p&gt;
&lt;h3 id=&quot;clean-api-naming&quot;&gt;Clean API Naming&lt;/h3&gt;
&lt;p&gt;REST APIs and socket events followed structured naming conventions for readability and debugging.&lt;/p&gt;
&lt;h3 id=&quot;environment-security&quot;&gt;Environment Security&lt;/h3&gt;
&lt;p&gt;Sensitive Firebase credentials and environment variables were managed securely through &lt;code&gt;.env&lt;/code&gt; configuration files.&lt;/p&gt;
&lt;h3 id=&quot;deployment-readiness&quot;&gt;Deployment Readiness&lt;/h3&gt;
&lt;p&gt;The architecture was designed in a way that the project could later be deployed without major restructuring.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;deliverables-amp-documentation&quot;&gt;Deliverables &amp;amp; Documentation&lt;/h2&gt;
&lt;p&gt;The project also included proper engineering documentation and structured deliverables such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Source code repository&lt;/li&gt;
&lt;li&gt;README documentation&lt;/li&gt;
&lt;li&gt;API documentation&lt;/li&gt;
&lt;li&gt;Architecture overview&lt;/li&gt;
&lt;li&gt;Environment setup guides&lt;/li&gt;
&lt;li&gt;Screenshots &amp;amp; walkthroughs&lt;/li&gt;
&lt;li&gt;Socket event documentation&lt;/li&gt;
&lt;li&gt;Deployment preparation&lt;/li&gt;
&lt;li&gt;Database structure planning&lt;/li&gt;
&lt;li&gt;Real-time collaboration explanation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-learned-from-this-project&quot;&gt;What I Learned From This Project&lt;/h2&gt;
&lt;p&gt;This project became a major practical learning experience for me.&lt;/p&gt;
&lt;p&gt;Instead of only building frontend interfaces, I learned how modern full-stack systems work internally.&lt;/p&gt;
&lt;p&gt;Some of the biggest learning outcomes included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time application architecture&lt;/li&gt;
&lt;li&gt;Socket communication workflows&lt;/li&gt;
&lt;li&gt;Firebase integration&lt;/li&gt;
&lt;li&gt;Authentication systems&lt;/li&gt;
&lt;li&gt;Backend structuring&lt;/li&gt;
&lt;li&gt;Environment management&lt;/li&gt;
&lt;li&gt;API handling&lt;/li&gt;
&lt;li&gt;Debugging real-world issues&lt;/li&gt;
&lt;li&gt;Scalable project organization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most importantly, this project improved my confidence in working with modern full-stack technologies, develop a SaaS project and solving practical engineering challenges.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt; Repository Link:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/YasirAwan4831/rt-collaborative-doc-editor-fs3&quot;&gt;https://github.com/YasirAwan4831/rt-collaborative-doc-editor-fs3&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Building this Modern Real-Time Collaborative Document Editor was an exciting and valuable experience.&lt;/p&gt;
&lt;p&gt;The project helped me understand how modern collaboration platforms function behind the scenes and how full-stack technologies interact together to create scalable web applications.&lt;/p&gt;
&lt;p&gt;Although the system still has room for future improvements such as advanced permissions, document version recovery and conflict resolution enhancements, the current implementation successfully demonstrates real-world collaboration workflows and modern engineering practices.&lt;/p&gt;
&lt;p&gt;Projects like this are not only about writing code — they are about understanding architecture, debugging systems, solving engineering problems and continuously learning throughout the development journey.&lt;/p&gt;
&lt;p&gt;And honestly, that is what makes software engineering exciting.&lt;/p&gt;
&lt;p&gt;— &lt;a rel=&quot;nofollow&quot; href=&quot;https://yasirawan4831.github.io/futuristic-links-dashboard/&quot;&gt;Muhammad Yasir&lt;/a&gt;&lt;br&gt;
Full Stack Developer&lt;/p&gt;
&lt;p&gt;&lt;em&gt;#FullStackDevelopment #ReactJS #NodeJS #Firebase #SocketIO #WebDevelopment #RealtimeApps #JavaScript #OpenSource #SoftwareEngineering #FrontendDevelopment #BackendDevelopment #Firestore #TailwindCSS #Vite #CollaborativeEditor #TEYZIXCORE #LinkedInTech #Developers #CodingJourney #MuhammadYasir #yasirTech #YasirAwan4831&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19196/building-modern-real-time-collaborative-document-editor-full-stack-engineering-journey</guid>
<pubDate>Fri, 29 May 2026 05:56:02 +0000</pubDate>
</item>
<item>
<title>Persistence Honesty: Guests, Firebase, Cache, and User-Owned Sync</title>
<link>https://coderlegion.com/18703/persistence-honesty-guests-firebase-cache-and-user-owned-sync</link>
<description>&lt;h2 id=&quot;persistence-honesty-guests-firebase-cache-and-user-owned-sync&quot;&gt;Persistence Honesty: Guests, Firebase, Cache, and User-Owned Sync&lt;/h2&gt;
&lt;p&gt;“Local-first” in our prose means &lt;strong&gt;interaction and ingestion default to the device&lt;/strong&gt; — not “no server for signed-in users.” This part is the &lt;strong&gt;mode matrix&lt;/strong&gt; every writer and growth campaign must internalise. Deeper guest/auth detail also lives in &lt;code&gt;content/coderlegion-sovereign-engineering-serial/05-local-first-browser-vault.md&lt;/code&gt;; here we align with the &lt;strong&gt;2026 calibration ledger&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ground-truth-table&quot;&gt;Ground truth table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope=&quot;col&quot;&gt;User mode&lt;/th&gt;
&lt;th scope=&quot;col&quot;&gt;Authoritative trades&lt;/th&gt;
&lt;th scope=&quot;col&quot;&gt;Code touchpoints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Guest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser &lt;code&gt;localStorage&lt;/code&gt; (namespaced keys, quota handling)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localPortfolioStore&lt;/code&gt; patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authenticated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Firebase&lt;/strong&gt; cloud — authoritative history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useTrades&lt;/code&gt; → &lt;code&gt;TradeService.getTrades&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI preferences&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zustand &lt;code&gt;persist&lt;/code&gt; with &lt;code&gt;partialize&lt;/code&gt; — chart/filters only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;portfolioStore&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IndexedDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primarily &lt;strong&gt;Firestore client SDK cache&lt;/strong&gt;; cleared on logout where implemented&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useAuth&lt;/code&gt; teardown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Drive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User-owned file (&lt;code&gt;pocket_portfolio_db.json&lt;/code&gt;) — dumb sync lane&lt;/td&gt;
&lt;td&gt;Sovereign Sync docs / architecture page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-indexeddb-is-our-database-is-wrong&quot;&gt;Why “IndexedDB is our database” is wrong&lt;/h3&gt;
&lt;p&gt;Guests do not use IndexedDB as the primary trade vault — they use &lt;strong&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;. Authenticated users’ authority is &lt;strong&gt;Firebase&lt;/strong&gt;, with IndexedDB as &lt;strong&gt;offline/cache&lt;/strong&gt; for the SDK. Claiming IndexedDB is “the portfolio vault for everyone” fails procurement diligence in one grep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;local-first-no-cloud&quot;&gt;Local-first ≠ no cloud&lt;/h3&gt;
&lt;p&gt;Approved phrases (from &lt;code&gt;docs/command/claims-vs-codebase-calibration.md&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client-side aggregation by construction&lt;/strong&gt; for LLM context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateless inference handler&lt;/strong&gt; for portfolio text on &lt;code&gt;/api/ai/chat&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prohibited without sign-off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Zero server footprint” / “fully local portfolio” for auth workflows.&lt;/li&gt;
&lt;li&gt;“No cloud” — Firebase, Vercel, LLM APIs, optional Redis/KV are all cloud-adjacent.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;google-drive-sovereign-sync-not-app-logic&quot;&gt;Google Drive: sovereign sync, not app logic&lt;/h3&gt;
&lt;p&gt;Drive holds a &lt;strong&gt;user-owned JSON file&lt;/strong&gt;. The app does not execute portfolio business rules on Google’s servers — it treats Drive as &lt;strong&gt;dumb storage&lt;/strong&gt; for export/sync. That is a credible “user-owned lane” story &lt;strong&gt;without&lt;/strong&gt; claiming we eliminated Firebase for signed-in users.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineering-takeaway-for-part-12&quot;&gt;Engineering takeaway for Part 12&lt;/h3&gt;
&lt;p&gt;Honest persistence makes the &lt;strong&gt;inference boundary&lt;/strong&gt; (Parts 2–3) believable: we are not pretending the full ledger never existed; we are proving the &lt;strong&gt;LLM hop&lt;/strong&gt; does not require re-uploading it row-by-row.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Part 4 of &lt;strong&gt;Sovereign Ingestion &amp;amp; Stateless Inference&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/book/sovereign-intelligence&quot;&gt;Sovereign Intelligence&lt;/a&gt; book, explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.openportfolio.co.uk&quot;&gt;Open Portfolio&lt;/a&gt;, or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app&quot;&gt;try Pocket Portfolio&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18703/persistence-honesty-guests-firebase-cache-and-user-owned-sync</guid>
<pubDate>Thu, 28 May 2026 08:00:00 +0000</pubDate>
</item>
<item>
<title>What's the most annoying problem in your daily life that doesn't have a good mobile app yet?</title>
<link>https://coderlegion.com/17845/whats-the-most-annoying-problem-in-your-daily-life-that-doesnt-have-a-good-mobile-app-yet</link>
<description>&lt;p&gt;Hey CoderLegion community &lt;br&gt;
I'm a Flutter &amp;amp; Firebase developer currently looking for my next build idea — and instead of guessing, I want to ask the people who actually feel the pain.&lt;br&gt;
What's a real problem you face regularly that you wish had a better mobile app solution?&lt;br&gt;
Could be anything:&lt;/p&gt;
&lt;p&gt;Something at work that still runs on WhatsApp groups or spreadsheets&lt;br&gt;
A local service that has no app (booking, delivery, scheduling)&lt;br&gt;
A tool you use daily that feels clunky or outdated&lt;br&gt;
Something you've thought &quot;why doesn't this app exist yet?&quot;&lt;/p&gt;
&lt;p&gt;I build full-stack mobile apps using Flutter + Firebase — so I'm looking for ideas that involve real-time data, multi-user roles, bookings, payments, or location tracking. But drop anything — I want to hear what actually frustrates people.&lt;br&gt;
Drop your pain point in the comments  Even half-formed ideas are welcome. If enough people share the same problem, that's a signal worth building for.&lt;br&gt;
Let's see what's missing out there.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/17845/whats-the-most-annoying-problem-in-your-daily-life-that-doesnt-have-a-good-mobile-app-yet</guid>
<pubDate>Thu, 14 May 2026 13:33:13 +0000</pubDate>
</item>
<item>
<title>Building a Zero-Cost AI Feature in Flutter with Gemma 4 + Firebase</title>
<link>https://coderlegion.com/17447/building-a-zero-cost-ai-feature-in-flutter-with-gemma-4-firebase</link>
<description>&lt;p&gt;How to combine on-device inference with cloud sync — without paying a cent in API fees&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bworo35ns7cm6xwfa0wm.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-unspoken-reality-of-ai-api-costs&quot;&gt;The Unspoken Reality of AI API Costs&lt;/h3&gt;
&lt;p&gt;Here’s the moment every indie developer dreads. You’ve shipped your AI feature. Users love it. It’s working. Then you open your billing dashboard.&lt;/p&gt;
&lt;p&gt;Every question your users ask costs you money. Every summary generated, every classification run, you’re paying for it. You have created a successful product, but its popularity is now draining your resources.&lt;/p&gt;
&lt;p&gt;What if there was a way to run powerful AI in your Flutter app with zero ongoing API costs? No per-request charges. No server bills. No privacy risk from data leaving the device.&lt;/p&gt;
&lt;p&gt;That’s exactly what the combination of Gemma 4 and Firebase unlocks. Gemma does the thinking, entirely on the user’s phone. Firebase handles persistence and sync. The result is an AI-powered app that scales to thousands of users at near-zero marginal cost.&lt;/p&gt;
&lt;p&gt;I’m building my own product on this exact stack. Here’s how it works.&lt;/p&gt;
&lt;h3 id=&quot;why-cloud-ai-is-a-trap-for-indie-developers&quot;&gt;Why cloud AI is a trap for indie developers&lt;/h3&gt;
&lt;p&gt;Cloud AI APIs are seductive. You make one API call, get a response, ship the feature. But the cost model is brutal for bootstrapped products:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Costs scale directly with usage — success punishes you&lt;/li&gt;
&lt;li&gt;User data leaves the device — a privacy and trust problem&lt;/li&gt;
&lt;li&gt;No connection means broken features — poor offline experience&lt;/li&gt;
&lt;li&gt;You’re dependent on a third-party API that can change pricing or availability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On-device AI solves all four of these problems at once. The model runs locally, the user’s data never hits a network, it works offline, and you own the stack.&lt;/p&gt;
&lt;h3 id=&quot;the-stack-gemma-4-flutter-gemma-firebase&quot;&gt;The stack: Gemma 4 + flutter_gemma + Firebase&lt;/h3&gt;
&lt;p&gt;Three components, each with a clear job:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gemma 4 (the brain)&lt;/strong&gt;&lt;br&gt;
Gemma 4 is Google’s latest edge-optimized open model, built on the same research as Gemini but designed to run efficiently on mobile hardware. The E2B variant (Effective 2B parameters) runs in under 2GB of RAM thanks to 4-bit quantization. It’s practical for real devices, not just flagship phones. Critically, Gemma 4 supports native function calling, which means you can wire it directly into your app’s logic without any prompt engineering hacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;flutter_gemma (the bridge)&lt;/strong&gt;&lt;br&gt;
The flutter_gemma package wraps the native LiteRT-LM inference engine in a clean Dart API. You get GPU acceleration, streaming responses, multimodal vision support, and function calling, all from Flutter. It supports Android, iOS, Web, and Desktop from a single codebase.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Firebase (the backbone)&lt;/strong&gt;&lt;br&gt;
Firebase handles everything that needs to live in the cloud: Firestore for storing and syncing AI outputs across devices, Firebase Auth for user identity, and optionally Firebase Storage if you need to persist larger assets. The key insight is that your AI inference never touches Firebase; only the results do.&lt;/p&gt;
&lt;h3 id=&quot;the-architecture-who-does-what&quot;&gt;The architecture: who does what&lt;/h3&gt;
&lt;p&gt;Here’s the principle that makes this work: AI never touches the network. Only the output does.&lt;/p&gt;
&lt;p&gt;The flow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User creates content in the app (a note, a photo, a voice memo)&lt;/li&gt;
&lt;li&gt;Gemma processes it on-device — tagging, summarizing, classifying&lt;/li&gt;
&lt;li&gt;The AI output (not the raw input) is written to Firestore&lt;/li&gt;
&lt;li&gt;Firebase syncs the output to the user’s other devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The user’s raw thoughts, photos, or voice never leave their phone. The model never makes a network call. But their data is still available everywhere they need it, because Firebase syncs the processed result.&lt;/p&gt;
&lt;h3 id=&quot;building-it-a-private-ai-notes-app&quot;&gt;Building it: a private AI notes app&lt;/h3&gt;
&lt;p&gt;Let’s make this concrete. We’ll build a simple notes app where Gemma automatically tags and summarizes each note on-device, then Firebase syncs the result across the user’s devices. Here’s the step-by-step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Set up flutter_gemma&lt;/strong&gt;&lt;br&gt;
Add the dependency to your pubspec.yaml:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dependencies:  flutter_gemma: ^0.13.1  cloud_firestore: ^5.0.0  firebase_auth: ^5.0.0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Download the Gemma 4 E2B model from Hugging Face (you’ll need to accept the license terms). Store your HuggingFace token securely; never hardcode it. On first launch, the app downloads the model to local storage. This is a one-time operation of around 1.5GB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Run on-device inference&lt;/strong&gt;&lt;br&gt;
Initialize the model and run a simple summarization:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final model = await FlutterGemma.getActiveModel(  maxTokens: 512,  preferredBackend: PreferredBackend.gpu,);
final chat = await model.createChat();
await chat.addQueryChunk(  Message.text(    text: ‘Summarize this note in one sentence and suggest 2-3 tags: $noteContent’,    isUser: true,  ),);
final response = await chat.generateChatResponse();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That’s it. The inference runs entirely on-device. No API key, no network call, no cost. The response gives you a summary and tags you can parse and store.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Write the output to Firestore&lt;/strong&gt;&lt;br&gt;
Take the AI output and persist it to Firestore:&lt;/p&gt;
&lt;p&gt;`await FirebaseFirestore.instance&lt;br&gt;
  .collection(’notes’)&lt;br&gt;
  .doc(noteId)&lt;br&gt;
  .set({‘originalContent’: noteContent, // stays on-device or stored per your preference    &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;‘summary’: parsedSummary,    
‘tags’: parsedTags,    
‘userId’: FirebaseAuth.instance.currentUser!.uid,    
‘createdAt’: FieldValue.serverTimestamp(),});`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice what goes to Firestore: the AI-generated summary and tags, not necessarily the raw note. You can store the original too if your app requires it — but the point is you’re in control of what leaves the device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Read it back and sync&lt;/strong&gt;&lt;br&gt;
Firestore’s real-time listeners handle the sync automatically. Open the app on a second device and your tagged, summarized notes appear instantly — without any polling logic on your part. Firebase does the heavy lifting.&lt;/p&gt;
&lt;h3 id=&quot;real-talk-the-gotchas&quot;&gt;Real talk: the gotchas&lt;/h3&gt;
&lt;p&gt;I won’t sell you a perfect picture. Here’s what you’ll actually run into:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model download size&lt;/strong&gt;&lt;br&gt;
The Gemma 4 E2B model is ~1.5GB. This is a one-time download, but you need to handle it gracefully. Show a clear progress indicator on first launch. Consider letting users choose to download over WiFi only. The flutter_gemma package provides download progress callbacks — use them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cold start time&lt;/strong&gt;&lt;br&gt;
Loading the model into memory takes a few seconds on the first inference of a session. Show a loading state. After the model is warm, subsequent responses are fast. Set user expectations early — frame it as the app ‘preparing your AI’ rather than ‘loading.’&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Know when to use cloud AI instead&lt;/strong&gt;&lt;br&gt;
On-device Gemma 4 is excellent for targeted tasks: summarization, classification, short generation, tagging. It’s not the right tool for complex multi-step reasoning, very long context windows, or tasks where you need GPT-4-level capability. Design your AI features to scope appropriately — use Gemma on-device for the 80% of tasks it handles well, and route complex queries to a cloud model if needed.&lt;/p&gt;
&lt;h3 id=&quot;the-payoff&quot;&gt;The payoff&lt;/h3&gt;
&lt;p&gt;Here’s what you get with this architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero ongoing AI cost.&lt;/strong&gt; No per-request charges, ever. The model is on the user’s device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full privacy.&lt;/strong&gt; User data is processed locally. You can credibly promise your users their data stays on their phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Works offline.&lt;/strong&gt; AI features work without a connection. Firebase syncs when connectivity returns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scales for free.&lt;/strong&gt; Your 1,000th user costs you no more in AI inference than your first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Firebase gives you the sync and persistence layer without building a backend. Gemma gives you the AI without paying per token. Together, they let you ship an AI-powered product that’s genuinely sustainable for an indie developer.&lt;/p&gt;
&lt;h3 id=&quot;what-i-m-building-with-this&quot;&gt;What I’m building with this&lt;/h3&gt;
&lt;p&gt;This is the exact architecture I’m using for my own product — built on Flutter, Gemma, and Firebase. I’ll be writing more about specific implementation decisions as I go, including how I’m using Gemma’s multimodal vision support and how I structure Firestore for AI-generated data.&lt;/p&gt;
&lt;p&gt;If you’re building something similar — or thinking about it — hit reply. I’d love to hear what you’re working on.&lt;/p&gt;
&lt;p&gt;Next up: Adding multimodal vision to your Flutter + Gemma app — letting the model read images directly, entirely on-device.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17447/building-a-zero-cost-ai-feature-in-flutter-with-gemma-4-firebase</guid>
<pubDate>Mon, 11 May 2026 16:39:31 +0000</pubDate>
</item>
<item>
<title>I Built a Complete Zomato-Style Food Delivery Platform in Flutter — Here's What I Learned</title>
<link>https://coderlegion.com/17269/i-built-a-complete-zomato-style-food-delivery-platform-in-flutter-heres-what-i-learned</link>
<description>&lt;p&gt;Just shipped Zesto — a full food delivery ecosystem with 4 Flutter&lt;br&gt;
apps and a Firebase backend. Here's what the build taught me.&lt;/p&gt;
&lt;p&gt;What's in the project&lt;/p&gt;
&lt;p&gt; Customer App — OTP login, restaurant discovery, cart,&lt;br&gt;
Razorpay payments, live driver tracking on Google Maps&lt;br&gt;
 Driver App — auto job assignment, step-by-step delivery&lt;br&gt;
slider, background GPS foreground service&lt;br&gt;
️ Restaurant App — real-time order dashboard, menu management&lt;br&gt;
️ Admin Panel — Flutter Web, multi-city management&lt;/p&gt;
&lt;p&gt;Backend: 20+Firebase Cloud Functions (Node.js 18)&lt;/p&gt;
&lt;p&gt;3 Things That Bit Me Hard&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Never trust client-side prices&lt;br&gt;
My original placeOrder Cloud Function accepted grand_total from&lt;br&gt;
the app. Anyone could send grand_total: 1 and get free food.&lt;br&gt;
Fixed by recalculating everything server-side from Firestore menu&lt;br&gt;
prices — never touching the client value.&lt;/li&gt;
&lt;li&gt;Status string consistency across 4 apps&lt;br&gt;
Flutter enums use camelCase by default (arrivedAtRestaurant).&lt;br&gt;
Firestore stored snake_case (arrived_at_restaurant). Silent&lt;br&gt;
mismatches everywhere. Lesson: standardize from day one and use&lt;br&gt;
a safe parser that never throws on unknown values.&lt;/li&gt;
&lt;li&gt;MobX reactions fire on every change — not just the first one&lt;br&gt;
My HomeScreen reaction was pushing a new delivery screen every time&lt;br&gt;
activeJob updated — including status changes while the driver was&lt;br&gt;
already on the delivery screen. Three screens deep by the time the&lt;br&gt;
order was delivered. A single boolean guard fixed it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tech Stack&lt;br&gt;
Flutter 3.11+ · Dart · Firebase · Razorpay&lt;br&gt;
Google Maps · MobX · Node.js 18 · GeoFlutterFire&lt;/p&gt;
&lt;p&gt;The source code is available&lt;br&gt;
If you're building something similar or want a real production&lt;br&gt;
Flutter/Firebase codebase to learn from:&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://morningstar47jb.gumroad.com/l/tnevss&quot;&gt;https://morningstar47jb.gumroad.com/l/tnevss&lt;/a&gt;&lt;br&gt;
Demo video: &lt;a rel=&quot;nofollow&quot; href=&quot;https://youtube.com/shorts/rL2M5O5Tk50&quot;&gt;https://youtube.com/shorts/rL2M5O5Tk50&lt;/a&gt;&lt;br&gt;
Happy to answer any architecture questions in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17269/i-built-a-complete-zomato-style-food-delivery-platform-in-flutter-heres-what-i-learned</guid>
<pubDate>Sun, 10 May 2026 10:40:13 +0000</pubDate>
</item>
<item>
<title>Measuring Loop Velocity Without Portfolio Rows</title>
<link>https://coderlegion.com/13908/measuring-loop-velocity-without-portfolio-rows</link>
<description>&lt;hr&gt;
&lt;h2 id=&quot;measuring-loop-velocity-without-portfolio-rows&quot;&gt;Measuring Loop Velocity Without Portfolio Rows&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/admin/analytics&lt;/code&gt; is an &lt;strong&gt;operator console&lt;/strong&gt;—revenue, referrals, npm packages, viral email blast stats, Pocket Analyst usage &lt;strong&gt;metadata&lt;/strong&gt;. It is &lt;strong&gt;not&lt;/strong&gt; a SQL console into user holdings.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;api-surface-get-api-admin-analytics&quot;&gt;API surface: &lt;code&gt;GET /api/admin/analytics&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;app/api/admin/analytics/route.ts&lt;/code&gt; aggregates many subsystems in one response (time range via &lt;code&gt;?range=&lt;/code&gt;). Notable bundles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stripe&lt;/strong&gt; — subscription / checkout signals keyed by price IDs (&lt;code&gt;SPONSOR_PRICE_IDS&lt;/code&gt;, &lt;code&gt;FOUNDERS_CLUB_PRICE_IDS&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Referrals&lt;/strong&gt; — &lt;code&gt;getReferralData(startDate)&lt;/code&gt; over &lt;code&gt;referralEvents&lt;/code&gt; (clicks, conversions, by campaign, by source).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Viral moment&lt;/strong&gt; — &lt;code&gt;getViralMomentBlastMetrics&lt;/code&gt; + &lt;code&gt;viralMomentEmailBlast&lt;/code&gt; object including &lt;code&gt;viral_moment_v1&lt;/code&gt; conversion rollups when campaign data exists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NPM&lt;/strong&gt; — download signals for scoped packages (&lt;code&gt;NPM_PACKAGES&lt;/code&gt; array).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pocket Analyst&lt;/strong&gt; — events come from &lt;code&gt;toolUsage&lt;/code&gt; writes (see Part 1) — flags like &lt;code&gt;hadAttachment&lt;/code&gt;, not prompt text.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;strong&gt;UI&lt;/strong&gt; is &lt;code&gt;app/admin/analytics/page.tsx&lt;/code&gt; — tables and cards for humans, not raw Firestore dumps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-zero-pii-analytics-actually-means-here&quot;&gt;What “zero-PII analytics” actually means here&lt;/h3&gt;
&lt;p&gt;We &lt;strong&gt;minimize&lt;/strong&gt; sensitive fields in product telemetry:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Referral events carry &lt;strong&gt;codes&lt;/strong&gt; and &lt;strong&gt;campaign tags&lt;/strong&gt;, not tickers.&lt;/li&gt;
&lt;li&gt;Analyst logs carry &lt;strong&gt;uid/tier/booleans&lt;/strong&gt;, not &lt;code&gt;context&lt;/code&gt; strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We &lt;strong&gt;do not&lt;/strong&gt; claim zero identifiers: &lt;strong&gt;UIDs&lt;/strong&gt; exist wherever Firebase auth does.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ga4-vs-admin&quot;&gt;GA4 vs admin&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GA4&lt;/strong&gt; answers &lt;strong&gt;marketing acquisition&lt;/strong&gt; (active users, sources). &lt;strong&gt;Admin API&lt;/strong&gt; answers &lt;strong&gt;product operations&lt;/strong&gt; (paid tiers, referral funnel, blast performance). Use both; &lt;strong&gt;never conflate&lt;/strong&gt; without labels (Part 4).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Part 9 of &lt;strong&gt;Sovereign Engineering&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/book/sovereign-intelligence&quot;&gt;Sovereign Intelligence&lt;/a&gt; book or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app&quot;&gt;try the app&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13908/measuring-loop-velocity-without-portfolio-rows</guid>
<pubDate>Mon, 04 May 2026 11:29:00 +0000</pubDate>
</item>
<item>
<title>Building a Zomato-Like Multi-City Food Delivery App with Flutter &amp; Firebase</title>
<link>https://coderlegion.com/16044/building-a-zomato-like-multi-city-food-delivery-app-with-flutter-firebase</link>
<description>&lt;p&gt;Hey everyone, Surendra Kumar here —Flutter/Firebase developer from Banda, India. I've been building production apps under Gfood Delivery Private Limited, and today I want to share the architecture behind my most ambitious project yet: Zesto, a multi-city food delivery platform built entirely with Flutter and Firebase.&lt;br&gt;
Zesto isn't a tutorial project. It's a real, working platform — 4 Flutter apps, a complete Firebase backend, and architecture decisions modeled after how production platforms like Zomato actually work.&lt;br&gt;
Want to try it before reading? Download the customer app APK and test it yourself:&lt;br&gt;
 Download Zesto Customer App &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/zesto-demo/releases/download/v1.0-beta/app-release.apk&quot;&gt;https://github.com/ssurekumar01111-hue/zesto-demo/releases/download/v1.0-beta/app-release.apk&lt;/a&gt;&lt;br&gt;
I'd love your feedback and improvement ideas after reading.&lt;/p&gt;
&lt;p&gt;The 4-App Ecosystem&lt;br&gt;
Zesto consists of four interconnected Flutter applications:&lt;/p&gt;
&lt;p&gt;Customer App — Multi-city restaurant discovery, cart, Razorpay + wallet payments, live GPS tracking, reviews&lt;br&gt;
Restaurant Partner App — Order management, menu CRUD, earnings dashboard, FCM order alerts&lt;br&gt;
Driver App — Order accept/reject, live delivery tracking, earnings, payout requests&lt;br&gt;
Flutter Web Admin Panel — 14 screens covering cities, zones, restaurants, drivers, orders, analytics, reviews, ads, config, and audit logs&lt;/p&gt;
&lt;p&gt;The Core Architecture Decision: City + Zone Scoping&lt;br&gt;
This is the most important architectural decision in Zesto — and the one that separates it from basic food delivery tutorials.&lt;br&gt;
Every single Firestore document has two fields:&lt;br&gt;
dart{&lt;br&gt;
  &quot;city_id&quot;: &quot;banda_up&quot;,&lt;br&gt;
  &quot;zone_id&quot;: &quot;banda_zone_1&quot;,&lt;br&gt;
  // ... rest of document&lt;br&gt;
}&lt;br&gt;
Every query filters by city_id first:&lt;br&gt;
dartFirebaseFirestore.instance&lt;br&gt;
  .collection('restaurants')&lt;br&gt;
  .where('city_id', isEqualTo: currentCityId)&lt;br&gt;
  .where('is_active', isEqualTo: true)&lt;br&gt;
  .snapshots();&lt;br&gt;
Why does this matter? Without city scoping, as you add cities your queries return restaurants from everywhere. Firestore has no built-in geographic filtering — you have to design it in from day one. Retrofitting this later would require rewriting every query and migrating every document.&lt;br&gt;
Zones sit inside cities and control driver assignment. When an order is placed, the Cloud Function assigns it to available drivers in the same zone first, then expands to the full city if no zone drivers are available.&lt;/p&gt;
&lt;p&gt;Firebase Custom Claims for RBAC&lt;br&gt;
Zesto has 5 user roles: customer, restaurant_owner, driver, admin, super_admin. Managing this with Firestore documents alone creates security holes — a user could theoretically write to their own document and escalate their role.&lt;br&gt;
The solution is Firebase custom claims set via Cloud Functions:&lt;br&gt;
javascript// functions/src/admin/setUserRole.js&lt;br&gt;
exports.setUserRole = functions.https.onCall(async (data, context) =&amp;gt; {&lt;br&gt;
  if (!context.auth.token.super_admin) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;throw new functions.https.HttpsError('permission-denied', 'Not authorized');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
  await admin.auth().setCustomUserClaims(data.uid, {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;role: data.role,
city_id: data.cityId,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
});&lt;br&gt;
On the Flutter side, the role is read from the ID token directly:&lt;br&gt;
dartfinal token = await FirebaseAuth.instance.currentUser?.getIdTokenResult();&lt;br&gt;
final role = token?.claims?['role'] as String?;&lt;br&gt;
This means even if someone modifies their Firestore document, the custom claim on the token is what gates access. Firestore rules enforce this:&lt;br&gt;
javascript// firestore.rules&lt;br&gt;
match /orders/{orderId} {&lt;br&gt;
  allow read: if request.auth.token.role == 'admin'&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|| request.auth.token.role == 'restaurant_owner'
|| request.auth.uid == resource.data.customer_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;MobX for State Management&lt;br&gt;
Zesto uses MobX across all 4 apps. Here's why I chose it over Riverpod or GetX for this project:&lt;br&gt;
MobX's reactive observables map perfectly to Firestore streams. When a Firestore stream emits a new value, MobX automatically rebuilds only the widgets that consume that observable — no manual setState, no context.watch boilerplate.&lt;br&gt;
dart// core/stores/order_store.dart&lt;br&gt;
class OrderStore = &lt;em&gt;OrderStore with &lt;/em&gt;$OrderStore;&lt;/p&gt;
&lt;p&gt;abstract class _OrderStore with Store {&lt;br&gt;
  @observable&lt;br&gt;
  ObservableList activeOrders = ObservableList();&lt;/p&gt;
&lt;p&gt;  @observable&lt;br&gt;
  OrderStatus currentStatus = OrderStatus.placed;&lt;/p&gt;
&lt;p&gt;  @action&lt;br&gt;
  void updateOrderStatus(String orderId, OrderStatus status) {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final idx = activeOrders.indexWhere((o) =&amp;gt; o.id == orderId);
if (idx != -1) {
  activeOrders[idx] = activeOrders[idx].copyWith(status: status);
}
currentStatus = status;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;br&gt;
The store subscribes to a Firestore stream once and updates observables. Every widget that reads activeOrders rebuilds automatically. Clean, testable, no rebuild storms.&lt;/p&gt;
&lt;p&gt;Dual Payment Path: Razorpay + In-App Wallet&lt;br&gt;
Zesto supports two payment methods and they interact with each other in interesting ways.&lt;br&gt;
Razorpay handles card/UPI payments. The flow is standard — create an order via Cloud Functions, open Razorpay checkout, handle the webhook callback.&lt;br&gt;
The wallet is more interesting. Each customer has a wallet_balance field on their user document. When they pay via wallet:&lt;br&gt;
javascript// functions/src/payments/walletPayment.js&lt;br&gt;
exports.processWalletPayment = functions.https.onCall(async (data, context) =&amp;gt; {&lt;br&gt;
  const uid = context.auth.uid;&lt;/p&gt;
&lt;p&gt;  return admin.firestore().runTransaction(async (transaction) =&amp;gt; {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const userRef = admin.firestore().collection('users').doc(uid);
const userDoc = await transaction.get(userRef);

const currentBalance = userDoc.data().wallet_balance;
if (currentBalance &amp;lt; data.amount) {
  throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');
}

transaction.update(userRef, {
  wallet_balance: admin.firestore.FieldValue.increment(-data.amount)
});

transaction.set(admin.firestore().collection('wallet_txns').doc(), {
  uid,
  amount: -data.amount,
  type: 'debit',
  order_id: data.orderId,
  created_at: admin.firestore.FieldValue.serverTimestamp(),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
});&lt;br&gt;
The Firestore transaction ensures the balance check and deduction happen atomically — no race condition where two simultaneous orders drain the wallet below zero.&lt;/p&gt;
&lt;p&gt;Cloud Functions: Domain-Split Architecture&lt;br&gt;
Instead of one massive index.js, Zesto's Cloud Functions are split by domain:&lt;br&gt;
functions/src/&lt;br&gt;
├── orders/&lt;br&gt;
│   ├── placeOrder.js       # Create order, notify restaurant&lt;br&gt;
│   ├── assignDriver.js     # Zone-based driver assignment&lt;br&gt;
│   └── onOrderDelivered.js # Earnings calculation, wallet credit&lt;br&gt;
├── payments/&lt;br&gt;
│   ├── razorpayWebhook.js  # Payment confirmation&lt;br&gt;
│   └── walletPayment.js    # Wallet debit/credit&lt;br&gt;
└── admin/&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── approveRestaurant.js
└── setUserRole.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each function is independently deployable. When I fix a bug in assignDriver.js, I deploy only that function — not the entire backend.&lt;/p&gt;
&lt;p&gt;The Driver Assignment Problem&lt;br&gt;
When an order is placed, the platform needs to assign a driver. Here's the simplified logic:&lt;br&gt;
javascriptexports.assignDriver = functions.firestore&lt;br&gt;
  .document('orders/{orderId}')&lt;br&gt;
  .onCreate(async (snap, context) =&amp;gt; {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const order = snap.data();

// 1. Find online drivers in the same zone
const driversSnap = await admin.firestore()
  .collection('drivers')
  .where('city_id', isEqualTo: order.city_id)
  .where('zone_id', isEqualTo: order.zone_id)
  .where('is_online', isEqualTo: true)
  .where('current_order_id', isEqualTo: null)
  .limit(5)
  .get();

if (driversSnap.empty) {
  // Expand to full city
  // ... same query without zone_id filter
}

// 2. Send FCM notification to available drivers
// First driver to accept wins (handled by transaction in Driver app)
const tokens = driversSnap.docs.map(d =&amp;gt; d.data().fcm_token);
await admin.messaging().sendMulticast({
  tokens,
  data: { type: 'NEW_ORDER', order_id: context.params.orderId }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  });&lt;br&gt;
On the driver side, accepting an order uses a Firestore transaction with three guards to prevent race conditions — only one driver can claim the order even if multiple tap Accept simultaneously.&lt;/p&gt;
&lt;p&gt;What's Coming&lt;br&gt;
Zesto is launching soon on Gumroad and Codester as commercial source code — complete, ready-to-customize. It includes all 4 Flutter apps, the complete Cloud Functions backend, Firestore security rules, and developer documentation.&lt;br&gt;
Try the customer app right now:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue/zesto-demo/releases/download/v1.0-beta/app-release.apk&quot;&gt;https://github.com/ssurekumar01111-hue/zesto-demo/releases/download/v1.0-beta/app-release.apk&lt;/a&gt;&lt;br&gt;
I'd genuinely love your feedback — what works well, what feels off, what you'd build differently. Drop a comment or reach me at &lt;em&gt;Emails are not allowed&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Surendra Kumar — Flutter/Firebase developer, Banda, India&lt;br&gt;
Portfolio: portfolio.gfood.in | GitHub: ssurekumar01111-hue&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16044/building-a-zomato-like-multi-city-food-delivery-app-with-flutter-firebase</guid>
<pubDate>Tue, 28 Apr 2026 05:29:56 +0000</pubDate>
</item>
<item>
<title>How I Built a Complete On-Demand Home Services App (ServeNow) with Flutter &amp; Firebase</title>
<link>https://coderlegion.com/16036/how-i-built-a-complete-on-demand-home-services-app-servenow-with-flutter-firebase</link>
<description>&lt;p&gt;Hey everyone, Surendra Kumar here, a self-taught Flutter developer from Banda, India. I've spent a good chunk of my time building and shipping production apps, like those I worked on for Gfood Delivery Private Limited. Today, I want to share the story behind one of my most ambitious projects: ServeNow.&lt;/p&gt;
&lt;p&gt;As an indie developer, I've always been driven to build &lt;em&gt;real&lt;/em&gt; solutions—apps that don't just look pretty in a tutorial but actually work robustly in the wild. That's exactly the philosophy I brought to ServeNow.&lt;/p&gt;
&lt;h4 id=&quot;what-is-servenow-crafting-a-full-fledged-marketplace&quot;&gt;What is ServeNow? Crafting a Full-Fledged Marketplace&lt;/h4&gt;
&lt;p&gt;ServeNow isn't just another demo app. It's a complete, production-ready on-demand home services marketplace built from the ground up with Flutter and Firebase. My goal was to create a comprehensive system that could handle everything from booking a beautician to fixing a leaky faucet, all while providing a smooth experience for customers, service providers, and administrators.&lt;/p&gt;
&lt;p&gt;To achieve this, I engineered ServeNow as a suite of four interconnected applications, each designed for a specific user role:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Customer App&lt;/strong&gt;: This is where users come to life. They can browse various services (beauty, cleaning, plumbing, electrical, etc.), book appointments, track their service provider in real-time, chat with them, make secure payments via Razorpay, and leave reviews.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider App&lt;/strong&gt;: For the businesses offering services. They can accept new bookings, manage their schedule and service offerings, track their real-time earnings, and request payouts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handyman App&lt;/strong&gt;: This is for the actual service professionals. It's a multi-service pro app featuring a broadcast job matching system, allowing handymen to pick up nearby jobs. It also includes an onboarding and approval flow for new pros.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flutter Web Admin Panel&lt;/strong&gt;: The control center. This robust web panel gives administrators full control over users, bookings, categories, service providers, payout requests, and provides crucial analytics to keep the marketplace running efficiently.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;why-flutter-amp-firebase-my-tech-stack-decisions&quot;&gt;Why Flutter &amp;amp; Firebase? My Tech Stack Decisions&lt;/h4&gt;
&lt;p&gt;When embarking on ServeNow, the choice of technology was critical. I needed a stack that offered rapid development, scalability, real-time capabilities, and robust performance without requiring a massive infrastructure investment.&lt;/p&gt;
&lt;h5 id=&quot;flutter-dart-for-the-frontend&quot;&gt;Flutter (Dart) for the Frontend&lt;/h5&gt;
&lt;p&gt;Flutter was a no-brainer for the mobile applications and the web admin panel.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Codebase&lt;/strong&gt;: Building four distinct applications (Customer, Provider, Handyman, and Admin Panel) from a single Flutter codebase drastically cut down development time and ensured UI/UX consistency across platforms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Flutter's native compilation and widget-based architecture deliver smooth, high-performance applications that feel truly native.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Experience&lt;/strong&gt;: Hot Reload and a rich widget catalog accelerated my development cycles, allowing for quick iterations and bug fixes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;firebase-for-the-backend-my-go-to-for-indie-development-&quot;&gt;Firebase for the Backend (My Go-To for Indie Development)&lt;/h5&gt;
&lt;p&gt;Firebase has been my backend of choice for many projects, and ServeNow was no exception. It offered the complete suite of tools I needed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Firestore&lt;/strong&gt;: This is the heart of ServeNow's data. I chose Firestore for its real-time capabilities, strong querying, and scalability.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture Decision&lt;/strong&gt;: I structured collections like &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;services&lt;/code&gt;, &lt;code&gt;providers&lt;/code&gt;, &lt;code&gt;handymen&lt;/code&gt;, &lt;code&gt;bookings&lt;/code&gt;, and &lt;code&gt;chats&lt;/code&gt;. For bookings, I leaned into &lt;strong&gt;denormalization&lt;/strong&gt; by embedding critical but static details (like customer name, service price) directly within the &lt;code&gt;bookings&lt;/code&gt; document. This minimizes expensive cross-collection reads for common operations and ensures faster data retrieval, especially for lists of bookings. Subcollections were used for highly related, nested data, like &lt;code&gt;messages&lt;/code&gt; within a &lt;code&gt;chat&lt;/code&gt; document.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Authentication&lt;/strong&gt;: For secure and easy user management across all apps, supporting email/password and potentially Google Sign-In for future expansion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud Functions (JavaScript)&lt;/strong&gt;: This was indispensable for server-side logic without managing actual servers.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture Decision&lt;/strong&gt;: Cloud Functions handle critical backend tasks:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Razorpay Webhooks&lt;/strong&gt;: Securely processing payment callbacks and updating booking statuses post-payment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FCM Triggers&lt;/strong&gt;: Sending real-time push notifications for booking updates, new job broadcasts (to handymen), and chat messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Aggregation&lt;/strong&gt;: Calculating real-time earnings for providers and handymen, and managing payout requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background Tasks&lt;/strong&gt;: Handling approval flows for new handymen and providers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Storage&lt;/strong&gt;: For storing user profile pictures, service images, and any other media files securely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Cloud Messaging (FCM)&lt;/strong&gt;: Essential for real-time communication between the apps – from notifying handymen about new jobs in their area to letting customers know their service provider is on the way.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;other-key-technologies-&quot;&gt;Other Key Technologies:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Razorpay&lt;/strong&gt;: Integrated as the primary payment gateway, crucial for handling transactions securely within the Indian market. Cloud Functions were vital here for handling payment success/failure callbacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rxdart&lt;/strong&gt;: I leveraged rxdart for reactive programming, especially for managing complex streams of data and state within the apps, ensuring a smooth and responsive user experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Maps&lt;/strong&gt;: Integrated for live tracking of handymen, defining service areas, and visualizing job locations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;architectural-decisions-amp-overcoming-challenges&quot;&gt;Architectural Decisions &amp;amp; Overcoming Challenges&lt;/h4&gt;
&lt;p&gt;Building ServeNow meant tackling typical marketplace complexities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real-time Synchronization&lt;/strong&gt;: A core requirement. Firestore's real-time listeners were used extensively. For example, when a handyman accepts a job, the customer's app updates instantly, and the provider's app reflects the booking status change. This required careful management of data updates and notification triggers via Cloud Functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Thinking beyond just a few users. Firestore's inherent scalability and Cloud Functions' serverless architecture meant the platform could grow without significant re-engineering of the backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job Broadcasting &amp;amp; Matching&lt;/strong&gt;: This was an interesting challenge for the Handyman app. When a new service request comes in, I use Firestore queries based on service type and location, combined with FCM, to broadcast the job to eligible handymen. The first to accept gets the job, with race conditions handled by Firestore transactions in Cloud Functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure Payouts&lt;/strong&gt;: The provider and handyman apps needed a robust payout request system. This involves tracking earnings in Firestore, initiating payout requests, and then an admin manually (or via a future integration) processing these, all managed through the Admin Panel and Cloud Functions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Documentation&lt;/strong&gt;: Knowing that this was a complex system, I invested significant time in creating a comprehensive 16-page developer documentation (v2.0). It covers everything from project setup to API details, making it easy for anyone to get started.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-indie-developer-ethos-production-grade-from-day-one&quot;&gt;The Indie Developer Ethos: Production-Grade from Day One&lt;/h4&gt;
&lt;p&gt;What makes ServeNow different is that it's built with a &quot;production-grade&quot; mindset. This isn't a collection of loosely connected tutorials; it's a fully integrated, tested system designed to be deployed and used in a real business environment. Every feature, every line of code, was written with stability, scalability, and maintainability in mind. It comes complete with signed APKs and a thorough setup guide, so you can literally launch it.&lt;/p&gt;
&lt;p&gt;Building ServeNow was a journey of applying everything I've learned about Flutter, Firebase, and shipping real products. It's proof that with dedication and the right tools, indie developers can build incredibly powerful, complete solutions.&lt;/p&gt;
&lt;h4 id=&quot;ready-to-launch-your-own-home-services-marketplace-&quot;&gt;Ready to Launch Your Own Home Services Marketplace?&lt;/h4&gt;
&lt;p&gt;If you're looking for a robust, ready-to-launch foundation for an on-demand home services marketplace, ServeNow is designed to be exactly that. It handles the complexities, so you can focus on your business.&lt;/p&gt;
&lt;p&gt;You can learn more about my work and projects like ServeNow by checking out my GitHub profile or reaching out via email.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;My GitHub Profile&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ssurekumar01111-hue&quot;&gt;ssurekumar01111-hue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email&lt;/strong&gt;: ssure.kumar01111@gmail.com&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://morningstar47jb.gumroad.com/l/viqos&quot;&gt;https://morningstar47jb.gumroad.com/l/viqos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks for reading! I hope this deep dive into ServeNow gives you some insights into building complex applications as an indie developer.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16036/how-i-built-a-complete-on-demand-home-services-app-servenow-with-flutter-firebase</guid>
<pubDate>Tue, 28 Apr 2026 04:39:08 +0000</pubDate>
</item>
<item>
<title>Engineering the Viral Loop</title>
<link>https://coderlegion.com/13905/engineering-the-viral-loop</link>
<description>&lt;hr&gt;
&lt;h2 id=&quot;engineering-the-viral-loop&quot;&gt;Engineering the Viral Loop&lt;/h2&gt;
&lt;p&gt;We measure &lt;strong&gt;referral codes and events&lt;/strong&gt;, not &lt;strong&gt;holdings&lt;/strong&gt;. K-factor style thinking applies to &lt;strong&gt;clicks → conversions&lt;/strong&gt;, instrumented in Firestore, without reading the user’s portfolio.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ingest-post-api-referral-event&quot;&gt;Ingest: &lt;code&gt;POST /api/referral-event&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;app/api/referral-event/route.ts&lt;/code&gt; validates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;action&lt;/code&gt;: &lt;strong&gt;&lt;code&gt;click&lt;/code&gt;&lt;/strong&gt; | &lt;strong&gt;&lt;code&gt;conversion&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;referralCode&lt;/code&gt;: required&lt;/li&gt;
&lt;li&gt;optional &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;campaign&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Metadata is &lt;strong&gt;sanitized and bounded&lt;/strong&gt; (&lt;code&gt;sanitizeMetadata&lt;/code&gt;: max keys, max string lengths) so events stay small and predictable.&lt;/p&gt;
&lt;p&gt;Firestore write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await db.collection('referralEvents').add({
  action,
  referralCode: String(referralCode).slice(0, 64),
  source: source || 'unknown',
  ...(campaignStr ? { campaign: campaignStr } : {}),
  ...(meta ? { metadata: meta } : {}),
  timestamp: Timestamp.now(),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;No portfolio fields&lt;/strong&gt; appear in this schema.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;reward-post-api-referral-complete&quot;&gt;Reward: &lt;code&gt;POST /api/referral/complete&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;app/api/referral/complete/route.ts&lt;/code&gt; is &lt;strong&gt;authenticated&lt;/strong&gt; (Bearer Firebase ID token). It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validates &lt;code&gt;REF-*&lt;/code&gt; referral code shape (&lt;code&gt;isValidReferralCode&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Resolves &lt;code&gt;referralIndex&lt;/code&gt; doc&lt;/li&gt;
&lt;li&gt;Enforces &lt;strong&gt;new-account&lt;/strong&gt; window (&lt;code&gt;REFEREE_MAX_ACCOUNT_AGE_MS&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;idempotent&lt;/strong&gt; claim keys per &lt;code&gt;(campaign, referee uid)&lt;/code&gt; (&lt;code&gt;sanitizeCampaignId&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Default campaign constant comes from &lt;code&gt;VIRAL_REFERRAL_CAMPAIGN_DEFAULT&lt;/code&gt; in &lt;code&gt;app/lib/viral/referralCodeServer.ts&lt;/code&gt; — align with &lt;strong&gt;&lt;code&gt;viral_moment_v1&lt;/code&gt;&lt;/strong&gt; in analytics UI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;attribution-plumbing&quot;&gt;Attribution plumbing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt; forces &lt;strong&gt;apex → www&lt;/strong&gt; so &lt;code&gt;?ref=&lt;/code&gt; survives and referral cookies stay on one origin (&lt;code&gt;pp_referral_code&lt;/code&gt;, landing capture).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;precision-on-privacy-&quot;&gt;Precision on “privacy”&lt;/h3&gt;
&lt;p&gt;We still store &lt;strong&gt;Firebase UIDs&lt;/strong&gt; for auth and &lt;strong&gt;referral codes&lt;/strong&gt; for growth. Claim: &lt;strong&gt;loop velocity&lt;/strong&gt; does not require &lt;strong&gt;portfolio PII&lt;/strong&gt; — not “we store nothing.”&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Part 7 of &lt;strong&gt;Sovereign Engineering&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/book/sovereign-intelligence&quot;&gt;Sovereign Intelligence&lt;/a&gt; book or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app&quot;&gt;try the app&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13905/engineering-the-viral-loop</guid>
<pubDate>Mon, 27 Apr 2026 11:19:00 +0000</pubDate>
</item>
<item>
<title>Growth Truth: Handling the Referral Spike</title>
<link>https://coderlegion.com/13898/growth-truth-handling-the-referral-spike</link>
<description>&lt;hr&gt;
&lt;h2 id=&quot;growth-truth-handling-the-referral-spike&quot;&gt;Growth Truth: Handling the Referral Spike&lt;/h2&gt;
&lt;p&gt;When traffic spikes, what breaks first — &lt;strong&gt;routing&lt;/strong&gt;, &lt;strong&gt;LLM quotas&lt;/strong&gt;, or &lt;strong&gt;database write paths&lt;/strong&gt;? Here is how we framed a real referral window against a &lt;strong&gt;stateless&lt;/strong&gt; chat surface.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;headline-traffic-ga4-&quot;&gt;Headline traffic (GA4)&lt;/h3&gt;
&lt;p&gt;Figures below are from &lt;strong&gt;Google Analytics 4&lt;/strong&gt; (Marketing performance) for a fixed &lt;strong&gt;28-day&lt;/strong&gt; window. GA4 can revise counts slightly as sessions reconcile; we cite the &lt;strong&gt;exported&lt;/strong&gt; values for this slice.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Date range&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;2026-03-06&lt;/strong&gt; → &lt;strong&gt;2026-04-02&lt;/strong&gt; (28 days)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Active users&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;4,669&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;New users&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;4,500&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Two different dials:&lt;/strong&gt; GA4 answers “how many people touched the property in this window.” Product-side analytics (referral events, campaign attribution, Stripe-adjacent rollups) answer “how did growth mechanics behave?” They are &lt;strong&gt;complementary&lt;/strong&gt;, not interchangeable — and &lt;strong&gt;GA4 active users&lt;/strong&gt; are not the same thing as &lt;strong&gt;Firebase Auth&lt;/strong&gt; sign-in counts unless you say so explicitly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;stateless-inference-and-where-burst-load-lands&quot;&gt;Stateless inference and where burst load lands&lt;/h3&gt;
&lt;p&gt;The app’s &lt;strong&gt;chat&lt;/strong&gt; path is &lt;strong&gt;stateless&lt;/strong&gt; for portfolio payloads (&lt;code&gt;/api/ai/chat&lt;/code&gt; — see Part 1). Under spike load, stress concentrates on:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Edge / routing&lt;/strong&gt; — session affinity is not the model; &lt;strong&gt;referral survival&lt;/strong&gt; is. We &lt;strong&gt;307&lt;/strong&gt; apex → &lt;code&gt;www&lt;/code&gt; so &lt;code&gt;?ref=&lt;/code&gt; and cookies stay coherent:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// middleware.ts — canonical host: apex → www
if (host === 'pocketportfolio.app') {
  url.hostname = 'www.pocketportfolio.app';
  return NextResponse.redirect(url, 307);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Serverless limits&lt;/strong&gt; — &lt;code&gt;maxDuration = 30&lt;/code&gt; on the chat route; streaming responses; provider rate limits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firestore&lt;/strong&gt; — quota counters (&lt;code&gt;aiUsage&lt;/code&gt;), analytics metadata (&lt;code&gt;toolUsage&lt;/code&gt;), &lt;strong&gt;&lt;code&gt;referralEvents&lt;/code&gt;&lt;/strong&gt; — hot writes, not giant prompt logs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; stateless inference keeps the &lt;strong&gt;heaviest sensitive payload&lt;/strong&gt; off disk; growth still needs &lt;strong&gt;solid&lt;/strong&gt; indexes and &lt;strong&gt;realistic&lt;/strong&gt; capacity planning for referrals and quotas.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-tends-to-fail-first&quot;&gt;What tends to fail first&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Referral attribution&lt;/strong&gt; (middleware, cookies) — if this slips, growth looks “direct only.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM provider&lt;/strong&gt; throttling or streaming failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firestore&lt;/strong&gt; write contention on high-frequency collections during campaigns.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Part 4 of &lt;strong&gt;Sovereign Engineering&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/book/sovereign-intelligence&quot;&gt;Sovereign Intelligence&lt;/a&gt; book or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app&quot;&gt;try the app&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13898/growth-truth-handling-the-referral-spike</guid>
<pubDate>Wed, 15 Apr 2026 12:04:00 +0000</pubDate>
</item>
<item>
<title>React Native Android build failed: what I would check first</title>
<link>https://coderlegion.com/14095/react-native-android-build-failed-what-i-would-check-first</link>
<description>&lt;p&gt;If a React Native Android build suddenly fails, the error message is often much less helpful than it looks.&lt;/p&gt;
&lt;p&gt;Sometimes it points to Gradle, sometimes to a plugin, sometimes to a dependency, and sometimes it just fails deep inside the Android build process without making the real cause obvious.&lt;/p&gt;
&lt;p&gt;I’ve run into this kind of problem enough times to stop treating “build failed” as one issue.&lt;/p&gt;
&lt;p&gt;It is usually a category of issues.&lt;/p&gt;
&lt;p&gt;This is the checklist I would go through first before changing random files or reinstalling everything.&lt;/p&gt;
&lt;h3 id=&quot;first-treat-build-failures-as-clues-not-conclusions&quot;&gt;First: treat build failures as clues, not conclusions&lt;/h3&gt;
&lt;p&gt;A React Native Android build can fail because of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gradle configuration issues&lt;/li&gt;
&lt;li&gt;plugin resolution problems&lt;/li&gt;
&lt;li&gt;dependency/version mismatches&lt;/li&gt;
&lt;li&gt;Kotlin or AGP incompatibility&lt;/li&gt;
&lt;li&gt;corrupted caches&lt;/li&gt;
&lt;li&gt;autolinking problems&lt;/li&gt;
&lt;li&gt;native module integration issues&lt;/li&gt;
&lt;li&gt;environment/setup problems on the machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means the text &lt;code&gt;BUILD FAILED&lt;/code&gt; is not the diagnosis. It is just the start of the diagnosis.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-check-first&quot;&gt;What I would check first&lt;/h3&gt;
&lt;h4 id=&quot;1-read-the-first-meaningful-error-not-the-last-line&quot;&gt;1. Read the first meaningful error, not the last line&lt;/h4&gt;
&lt;p&gt;One of the easiest mistakes is focusing on the bottom of the terminal output.&lt;/p&gt;
&lt;p&gt;The last line usually just says the build failed. The useful part is often earlier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the first missing plugin&lt;/li&gt;
&lt;li&gt;the first unresolved dependency&lt;/li&gt;
&lt;li&gt;the first incompatible version&lt;/li&gt;
&lt;li&gt;the first task that actually broke&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I see a huge wall of Gradle output, I try to find the first message that clearly indicates what could not be found, resolved, or configured.&lt;/p&gt;
&lt;p&gt;That usually narrows the problem much faster.&lt;/p&gt;
&lt;h3 id=&quot;2-check-react-native-gradle-agp-kotlin-compatibility&quot;&gt;2. Check React Native / Gradle / AGP / Kotlin compatibility&lt;/h3&gt;
&lt;p&gt;A lot of Android build failures are really version alignment problems.&lt;/p&gt;
&lt;p&gt;I would check whether these pieces actually make sense together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React Native version&lt;/li&gt;
&lt;li&gt;Gradle wrapper version&lt;/li&gt;
&lt;li&gt;Android Gradle Plugin version&lt;/li&gt;
&lt;li&gt;Kotlin version&lt;/li&gt;
&lt;li&gt;installed JDK version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A mismatch here can break the build even when your app code is fine.&lt;/p&gt;
&lt;p&gt;This is especially common after:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upgrading React Native&lt;/li&gt;
&lt;li&gt;adding or updating native libraries&lt;/li&gt;
&lt;li&gt;copying build config from another project&lt;/li&gt;
&lt;li&gt;mixing Expo/native config changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-look-at-settings-gradle-and-plugin-resolution-first&quot;&gt;3. Look at &lt;code&gt;settings.gradle&lt;/code&gt; and plugin resolution first&lt;/h3&gt;
&lt;p&gt;If the error mentions plugins not being found, configuration failing, or included builds not resolving correctly, I would go straight to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;android/settings.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;root &lt;code&gt;build.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;plugin management / includeBuild setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In newer React Native setups, especially around the Gradle plugin and autolinking, small mistakes there can break the entire build.&lt;/p&gt;
&lt;p&gt;This is one of the first places I would inspect if the error looks structural instead of library-specific.&lt;/p&gt;
&lt;h3 id=&quot;4-check-whether-this-is-really-a-cache-problem&quot;&gt;4. Check whether this is really a cache problem&lt;/h3&gt;
&lt;p&gt;Sometimes it is. Sometimes it is not.&lt;/p&gt;
&lt;p&gt;I would only treat it as a cache issue when the error actually suggests things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;temp workspace move failures&lt;/li&gt;
&lt;li&gt;corrupted transforms&lt;/li&gt;
&lt;li&gt;unexpected stale build behavior&lt;/li&gt;
&lt;li&gt;dependency artifacts behaving inconsistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In that case, I would think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gradle daemon state&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gradle&lt;/code&gt; cache corruption&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android/.gradle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;android/build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;whether Android Studio or another process is holding locks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But I would not start by deleting everything automatically unless the error really points that way.&lt;/p&gt;
&lt;h3 id=&quot;5-check-native-dependencies-and-recently-added-packages&quot;&gt;5. Check native dependencies and recently added packages&lt;/h3&gt;
&lt;p&gt;If the build worked before and stopped working after adding or updating something, that is one of the strongest signals you have.&lt;/p&gt;
&lt;p&gt;I would ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what changed last?&lt;/li&gt;
&lt;li&gt;did I add a native dependency?&lt;/li&gt;
&lt;li&gt;did I update Firebase packages?&lt;/li&gt;
&lt;li&gt;did I change Expo/native modules?&lt;/li&gt;
&lt;li&gt;did I update React Native or Gradle-related files?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That often gives a faster answer than staring at logs in isolation.&lt;/p&gt;
&lt;h3 id=&quot;6-watch-for-autolinking-and-native-module-issues&quot;&gt;6. Watch for autolinking and native module issues&lt;/h3&gt;
&lt;p&gt;React Native Android builds often fail because a native module was not linked/configured the way the project expects.&lt;/p&gt;
&lt;p&gt;This gets more likely when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mixing Expo modules with bare React Native config&lt;/li&gt;
&lt;li&gt;using libraries with native setup steps&lt;/li&gt;
&lt;li&gt;upgrading React Native without updating supporting config&lt;/li&gt;
&lt;li&gt;relying on autolinking that did not actually complete properly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the error mentions package resolution, missing generated files, or native tasks around module setup, I would treat autolinking as a serious suspect.&lt;/p&gt;
&lt;h3 id=&quot;7-distinguish-machine-setup-problems-from-project-problems&quot;&gt;7. Distinguish machine/setup problems from project problems&lt;/h3&gt;
&lt;p&gt;Sometimes the project is broken.&lt;/p&gt;
&lt;p&gt;Sometimes the machine is.&lt;/p&gt;
&lt;p&gt;That is an important difference.&lt;/p&gt;
&lt;p&gt;I would think about machine/environment issues when the error points toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK problems&lt;/li&gt;
&lt;li&gt;missing Android SDK components&lt;/li&gt;
&lt;li&gt;PATH / environment variable issues&lt;/li&gt;
&lt;li&gt;SSL / certificate / dependency download failures&lt;/li&gt;
&lt;li&gt;local file locking or permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why a build issue can appear “random” even when the project itself has not changed much.&lt;/p&gt;
&lt;h3 id=&quot;8-if-firebase-is-involved-do-not-assume-it-is-only-a-firebase-issue&quot;&gt;8. If Firebase is involved, do not assume it is only a Firebase issue&lt;/h3&gt;
&lt;p&gt;Firebase-related packages can surface as Android build failures even when the deeper cause is one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency mismatch&lt;/li&gt;
&lt;li&gt;outdated native setup&lt;/li&gt;
&lt;li&gt;plugin/version conflict&lt;/li&gt;
&lt;li&gt;Gradle packaging issue&lt;/li&gt;
&lt;li&gt;release/debug configuration difference&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So if the failure starts after adding Firebase or changing Firebase setup, I would still inspect the Android build chain broadly instead of narrowing too fast.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-mental-model&quot;&gt;A practical mental model&lt;/h3&gt;
&lt;p&gt;When I see “React Native Android build failed,” I usually sort the possibilities into these buckets:&lt;/p&gt;
&lt;h4 id=&quot;bucket-1-build-system&quot;&gt;Bucket 1: Build system&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Gradle&lt;/li&gt;
&lt;li&gt;AGP&lt;/li&gt;
&lt;li&gt;Kotlin&lt;/li&gt;
&lt;li&gt;plugin management&lt;/li&gt;
&lt;li&gt;wrapper version&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-2-native-integration&quot;&gt;Bucket 2: Native integration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;autolinking&lt;/li&gt;
&lt;li&gt;native modules&lt;/li&gt;
&lt;li&gt;Expo/native mix&lt;/li&gt;
&lt;li&gt;Firebase native setup&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;bucket-3-local-environment&quot;&gt;Bucket 3: Local environment&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;JDK&lt;/li&gt;
&lt;li&gt;SDK&lt;/li&gt;
&lt;li&gt;PATH&lt;/li&gt;
&lt;li&gt;cache corruption&lt;/li&gt;
&lt;li&gt;SSL/certificate/download issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That mental model helps me avoid random debugging.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-not-do-first&quot;&gt;What I would not do first&lt;/h3&gt;
&lt;p&gt;I would not immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reinstall the whole machine setup&lt;/li&gt;
&lt;li&gt;randomly edit multiple Gradle files&lt;/li&gt;
&lt;li&gt;downgrade several libraries at once&lt;/li&gt;
&lt;li&gt;clear every cache without understanding the error&lt;/li&gt;
&lt;li&gt;assume the last line in the terminal is the root cause&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That usually creates more noise.&lt;/p&gt;
&lt;h3 id=&quot;my-practical-order-of-checks&quot;&gt;My practical order of checks&lt;/h3&gt;
&lt;p&gt;If a React Native Android build fails, this is the order I would usually go through:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;find the first meaningful error in the output&lt;/li&gt;
&lt;li&gt;check version compatibility across RN / Gradle / AGP / Kotlin / JDK&lt;/li&gt;
&lt;li&gt;inspect &lt;code&gt;settings.gradle&lt;/code&gt; and plugin resolution&lt;/li&gt;
&lt;li&gt;review the most recent package or config change&lt;/li&gt;
&lt;li&gt;check native module/autolinking issues&lt;/li&gt;
&lt;li&gt;only then treat it as cache/environment cleanup if the error points there&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That order tends to save time.&lt;/p&gt;
&lt;h3 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h3&gt;
&lt;p&gt;“React Native Android build failed” is not one problem.&lt;/p&gt;
&lt;p&gt;It is usually one of a few repeated categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;version mismatch&lt;/li&gt;
&lt;li&gt;plugin/config problem&lt;/li&gt;
&lt;li&gt;native integration issue&lt;/li&gt;
&lt;li&gt;cache/environment issue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The faster you identify which category you are in, the faster the build becomes fixable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14095/react-native-android-build-failed-what-i-would-check-first</guid>
<pubDate>Mon, 06 Apr 2026 16:47:53 +0000</pubDate>
</item>
<item>
<title>[DISCUSS] The “Never 0.00” Challenge — design a resilient price pipeline (client → edge) together</title>
<link>https://coderlegion.com/5755/discuss-the-never-0-00-challenge-design-a-resilient-price-pipeline-client-edge-together</link>
<description>&lt;p&gt;&lt;strong&gt;[DISCUSS] The “Never 0.00” Challenge — design a resilient price pipeline (client → edge) together&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;New group, first thread. Let’s start with a problem every finance app faces: &lt;strong&gt;your quotes flicker to &lt;code&gt;0.00&lt;/code&gt;&lt;/strong&gt; right when users need truth. How do we design a pipeline that never panics, stays fast, and makes the UI &lt;em&gt;explainable&lt;/em&gt;?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m &lt;strong&gt;22s&lt;/strong&gt; from Pocket Portfolio (OSS dashboard: CSV → live P/L → Mock-Trade Lab). We’ve been hardening our price path and want to compare notes with you — &lt;strong&gt;architects, frontenders, edge-folk, crypto quants, and a11y pros&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4527813152350650800&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-story-2-mins-&quot;&gt;The story (2 mins)&lt;/h3&gt;
&lt;p&gt;You’re rendering a positions grid. Provider “A” hiccups for 900 ms, “B” is slow in one region, and “C” returns a weird &lt;code&gt;NaN&lt;/code&gt;. Your UI shows &lt;strong&gt;0.00&lt;/strong&gt; or a spinner graveyard. Users lose trust.&lt;/p&gt;
&lt;p&gt;We flipped the rule: &lt;em&gt;it’s better to show a clearly tagged &lt;strong&gt;stale-but-true&lt;/strong&gt; quote than a shiny &lt;strong&gt;0.00&lt;/strong&gt;.&lt;/em&gt; The UX stays calm, and the system buys enough time to recover.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-pattern-client-edge-&quot;&gt;The pattern (client → edge)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Client
  └── fetch quote(sym) with 1.4s budget
      └── Edge function (region-local)
          ├─ Try P1 (timeout 1.4s) -&amp;gt; OK? return
          ├─ Try P2 (1.4s) -&amp;gt; OK? return
          ├─ Try P3 (1.6s) -&amp;gt; OK? return
          └─ else: return last-known-good (stale:true) + tiny error summary
Cache
  └── KV: sym -&amp;gt; { price, ts, src }  // updated on successful hits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short, brutal timeouts&lt;/strong&gt; per provider (fail &lt;em&gt;fast&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotation&lt;/strong&gt; and &lt;strong&gt;backoff&lt;/strong&gt; to avoid “thundering herd”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last-known-good&lt;/strong&gt; as final fallback (with a &lt;strong&gt;stale&lt;/strong&gt; flag).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero logs spam&lt;/strong&gt; — count errors, sample detail, keep it cheap.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tiny-typescript-example-edge-&quot;&gt;Tiny TypeScript example (edge)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// /edge/quote.ts (simplified)
const providers = [
  { name: 'p1', url: env.P1_URL, timeoutMs: 1400 },
  { name: 'p2', url: env.P2_URL, timeoutMs: 1400 },
  { name: 'p3', url: env.P3_URL, timeoutMs: 1600 },
];

export async function getQuote(sym: string) {
  for (const p of providers) {
    try {
      const ctl = new AbortController();
      const t = setTimeout(() =&amp;gt; ctl.abort(), p.timeoutMs);
      const r = await fetch(`${p.url}/q?symbol=${sym}`, { signal: ctl.signal, cf:{ cacheTtl: 10 }});
      clearTimeout(t);
      if (!r.ok) continue;
      const { price } = await r.json();
      if (Number.isFinite(price)) {
        await KV.put(`q:${sym}`, JSON.stringify({ price, ts: Date.now(), src: p.name }), { expirationTtl: 120 });
        return { price, src: p.name, stale: false };
      }
    } catch {}
  }
  const last = await KV.get(`q:${sym}`);
  const cached = last ? JSON.parse(last) : null;
  return { price: cached?.price ?? null, src: cached?.src ?? 'none', stale: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14806969206289026923&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;ux-that-earns-trust-copy-paste-checklist-&quot;&gt;UX that earns trust (copy/paste checklist)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Never show 0.00&lt;/strong&gt; unless a symbol truly trades at zero (rare).&lt;/li&gt;
&lt;li&gt;✅ If &lt;strong&gt;stale&lt;/strong&gt;, show a subtle chip: &lt;code&gt;Last update 28s • p2&lt;/code&gt; with tooltip “Provider fallback active”.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Keyboard-first&lt;/strong&gt;: when a value changes, announce to SRs (&lt;code&gt;aria-live=&quot;polite&quot;&lt;/code&gt;) and use diff-highlights that fade in &amp;lt;1s.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Explain the &lt;em&gt;why&lt;/em&gt;&lt;/strong&gt;: on hover, show “Switched p1→p2 (timeout)”.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Error CSV&lt;/strong&gt; for failed imports; same spirit: explain, don’t mystify.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mini-spec-template-fill-in-and-post-as-a-reply-&quot;&gt;Mini-spec template (fill in and post as a reply)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;policy:
  ttl_cache_seconds: 10
  providers:
    - name: p1
      timeout_ms: 1400
    - name: p2
      timeout_ms: 1400
    - name: p3
      timeout_ms: 1600
  final_fallback: last_known_good
  stale_badge_after_ms: 15000
  count_telemetry: true   # no PII, just counters
ui:
  show_stale_chip: true
  announce_changes: polite
  hover_explain: provider_switch
observability:
  per_region_counters: true
  sample_errors: 0.05
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Post your filled YAML + any trade-offs (e.g., mobile vs desktop, crypto vs equities). &lt;strong&gt;We’ll review and suggest tweaks.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;one-hour-build-join-in-&quot;&gt;One-hour build (join in)&lt;/h3&gt;
&lt;p&gt;If you’ve got an hour and want something concrete:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add the &lt;strong&gt;stale chip&lt;/strong&gt; to your quote component (text + tooltip).&lt;/li&gt;
&lt;li&gt;Implement the second provider with timeouts (even a mock server).&lt;/li&gt;
&lt;li&gt;Store &lt;strong&gt;last-known-good&lt;/strong&gt; in a tiny KV/local cache.&lt;/li&gt;
&lt;li&gt;Paste a 10s GIF here. We’ll give quick notes.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-get-back&quot;&gt;What you get back&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Peer review&lt;/strong&gt; on your policy &amp;amp; code snippet&lt;/li&gt;
&lt;li&gt;A reusable &lt;strong&gt;fallback policy&lt;/strong&gt; you can ship this week&lt;/li&gt;
&lt;li&gt;A shout-out in our next Pocket Portfolio dev post (and we’ll link your repo)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-group-exists-and-where-we-re-going-&quot;&gt;Why this group exists (and where we’re going)&lt;/h3&gt;
&lt;p&gt;We care about &lt;strong&gt;open finance, DeFi, cryptography, and accessible design&lt;/strong&gt;. We’ll run &lt;strong&gt;Spec Clinics&lt;/strong&gt;, &lt;strong&gt;Paper Club&lt;/strong&gt;, and &lt;strong&gt;Ship Fridays&lt;/strong&gt;. Next threads will cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broker &lt;strong&gt;CSV normalization&lt;/strong&gt; (share one redacted row)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard-first dashboards&lt;/strong&gt; (a11y = speed)&lt;/li&gt;
&lt;li&gt;Wallet UX &amp;amp; security (MPC/AA, seedless recovery)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Introduce yourself below (stack, timezone, what you’re building). Then drop your &lt;strong&gt;fallback policy&lt;/strong&gt; reply. Let’s make “never 0.00” the default. ️✨&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13066996154180268769&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12880098923329289888&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13676190927944257299&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;— &lt;strong&gt;22s&lt;/strong&gt; (Pocket Portfolio)&lt;br&gt;
App: pocketportfolio.app/app • Repo: github.com/PocketPortfolio/Financialprofilenetwork&lt;/p&gt;
</description>
<category>React + TS</category>
<guid isPermaLink="true">https://coderlegion.com/5755/discuss-the-never-0-00-challenge-design-a-resilient-price-pipeline-client-edge-together</guid>
<pubDate>Mon, 29 Sep 2025 12:26:24 +0000</pubDate>
</item>
<item>
<title>Welcome to CoderLegion 22s</title>
<link>https://coderlegion.com/5738/welcome-to-coderlegion-22s</link>
<description>&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5881593112898619779&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;building-finance-tools-people-actually-trust-a-practical-playbook-from-the-pocket-portfolio-trenches-&quot;&gt;Building finance tools people actually trust: a practical playbook (from the Pocket Portfolio trenches)&lt;/h2&gt;
&lt;blockquote&gt;&lt;p&gt;I’m 22s, one of the folks behind Pocket Portfolio — an open-source, no-paywall investing dashboard where you can import a CSV -&amp;gt; get clean live P/L, and model trades in a Mock-Trade &amp;amp; Scenario Lab without touching real totals.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’ve shipped and broken my fair share of features. This post is the short list of things that made a night-and-day difference for us — equal parts tech, product, and what’s next.&lt;/p&gt;
&lt;p&gt;Use anything here in your own project. If you want to kick the tires, the app + repo are at the end.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-technical-design-for-no-0-00s-fallbacks-that-fail-gracefully-&quot;&gt;1) Technical: design for “no 0.00s” (fallbacks that fail gracefully)&lt;/h3&gt;
&lt;p&gt;Finance UI dies when prices flicker to 0.00. Our rule: client -&amp;gt; edge fallbacks with light telemetry and circuit-breakers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// edge/fallbackQuote.ts (pseudo)
const providers = [
  { name: 'p1', url: env.P1_URL, timeoutMs: 1400 },
  { name: 'p2', url: env.P2_URL, timeoutMs: 1400 },
  { name: 'p3', url: env.P3_URL, timeoutMs: 1600 },
];

export async function getQuote(sym: string) {
  const errors: string[] = [];
  for (const p of providers) {
    try {
      const ctl = new AbortController();
      const t = setTimeout(() =&amp;gt; ctl.abort(), p.timeoutMs);
      const r = await fetch(`${p.url}/q?symbol=${sym}`, { signal: ctl.signal, cf: { cacheTtl: 10 }});
      clearTimeout(t);
      if (!r.ok) throw new Error(`${p.name}:${r.status}`);
      const { price } = await r.json();
      if (!isFinite(price)) throw new Error(`${p.name}:NaN`);
      return { price, src: p.name };
    } catch (e: any) {
      errors.push(String(e.message));
      // soft failover: keep trying; never return 0
    }
  }
  // last resort: return &quot;stale but not zero&quot;
  const cached = await KV.get(`q:${sym}`); // store last good
  return { price: cached?.price ?? null, stale: true, errors };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why it works&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast happy path from provider 1 with short TTL.&lt;/li&gt;
&lt;li&gt;Soft failover and never-zero fallback (last known good).&lt;/li&gt;
&lt;li&gt;Errors are counted, not logged to death.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-technical-csv-imports-that-don-t-make-people-rage-quit&quot;&gt;2) Technical: CSV imports that don’t make people rage-quit&lt;/h3&gt;
&lt;p&gt;You will never have every broker format. Make it survivable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type ImportRow = {
  raw: Record&amp;lt;string, string&amp;gt;;  // source columns intact
  norm?: { sym: string; qty: number; price: number; ts: string };
  err?: { code: 'PARSE'|'SCHEMA'|'MISSING'; hint: string };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UX rules that helped immediately&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Always show a sample file (eToro/Coinbase/Generic) and a Download error CSV when something fails.&lt;/li&gt;
&lt;li&gt;Ask users to paste one redacted row in an issue; ship the mapping the same day.&lt;/li&gt;
&lt;li&gt;Keep the import stateless — users can re-run safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-product-ship-the-are-we-sure-layer&quot;&gt;3) Product: ship the “Are we sure?” layer&lt;/h3&gt;
&lt;p&gt;If you build tools that move money, people need a rehearsal. Our answer: a Mock-Trade &amp;amp; Scenario Lab that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instant: Add &lt;code&gt;TSLA +2 @ 260&lt;/code&gt; with zero setup.&lt;/li&gt;
&lt;li&gt;Isolated: Mock totals never touch real P/L.&lt;/li&gt;
&lt;li&gt;Obvious: A big &quot;mock trades active&quot; chip, and a reset.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one feature cut buyer’s remorse reports to near zero. Whatever you’re building, ask: What’s the one safe preview my user needs before a hard commit?&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-business-one-metric-that-punches-above-its-weight&quot;&gt;4) Business: one metric that punches above its weight&lt;/h3&gt;
&lt;p&gt;Time-to-first-insight (TTFI)&lt;br&gt;
From first page load -&amp;gt; the moment a user sees something true about their portfolio. We track it like performance.&lt;/p&gt;
&lt;p&gt;Things that reduce TTFI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sample CSVs in the modal&lt;/li&gt;
&lt;li&gt;Default scenario (&quot;add 1 share&quot;) with undo&lt;/li&gt;
&lt;li&gt;Fallback quotes (the “no 0.00s” rule)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your TTFI is &amp;lt; 60 seconds, activation will feel different.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-next-frontier-where-we-re-headed-&quot;&gt;5) Next frontier (where we’re headed)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Keyboard-first dashboards: a11y that doubles as speed. Arrow/Home/End for the grid; &lt;code&gt;M&lt;/code&gt; opens Mock-Trade; &lt;code&gt;?&lt;/code&gt; shows shortcuts.&lt;/li&gt;
&lt;li&gt;Explainers, not black boxes: when totals change, show why (&quot;+2 TSLA @ 260 changed delta P/L +$48&quot;). Debuggable numbers build trust.&lt;/li&gt;
&lt;li&gt;Local-first where it matters: scenarios and recent quotes cached on device; sensitive bits never have to round-trip.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re exploring the same things, I’d love to swap notes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building-and-where-to-try-it-&quot;&gt;What we’re building (and where to try it)&lt;/h3&gt;
&lt;p&gt;Pocket Portfolio is open-source and free to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App -&amp;gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/app?utm_source=coderlegion&amp;amp;utm_medium=post&amp;amp;utm_campaign=welcome&quot;&gt;https://www.pocketportfolio.app/app?utm_source=coderlegion&amp;amp;utm_medium=post&amp;amp;utm_campaign=welcome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repo -&amp;gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/PocketPortfolio/Financialprofilenetwork?utm_source=coderlegion&amp;amp;utm_medium=post&amp;amp;utm_campaign=welcome&quot;&gt;https://github.com/PocketPortfolio/Financialprofilenetwork?utm_source=coderlegion&amp;amp;utm_medium=post&amp;amp;utm_campaign=welcome&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good first issues right now&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSV mapping for a new broker format (drop one redacted row)&lt;/li&gt;
&lt;li&gt;Playwright test for the error-CSV -&amp;gt; fix -&amp;gt; success loop&lt;/li&gt;
&lt;li&gt;Light UI polish on the Mock-Trade modal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have thoughts on price fallbacks, import UX, or keyboard nav, hit me. I’ll review your project back — README, a11y, or a small PR.&lt;/p&gt;
&lt;p&gt;— 22s&lt;br&gt;
Founder, Pocket Portfolio&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9154819753562014126&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/5738/welcome-to-coderlegion-22s</guid>
<pubDate>Sun, 28 Sep 2025 20:07:11 +0000</pubDate>
</item>
<item>
<title>Using Firebase and Stripe for Modern Web Applications</title>
<link>https://coderlegion.com/4804/using-firebase-and-stripe-for-modern-web-applications</link>
<description>&lt;p&gt;Using Firebase and Stripe for Modern Web Applications&lt;/p&gt;
&lt;p&gt;Building secure, scalable, and efficient applications often requires integrating authentication, databases, and payment systems. Two popular tools that developers rely on are &lt;a rel=&quot;nofollow&quot; href=&quot;https://firebase.google.com/&quot;&gt;Firebase&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://stripe.com/&quot;&gt;Stripe&lt;/a&gt;. Together, they provide a seamless backend + payment infrastructure that accelerates development without heavy server management.&lt;/p&gt;
&lt;p&gt;In this tutorial, we’ll walk through how to integrate Firebase and Stripe in your project, with best practices for performance, serverless deployment, and real-world scenarios.&lt;/p&gt;
&lt;h3 id=&quot;why-use-firebase-stripe-&quot;&gt;Why Use Firebase + Stripe?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Firebase handles:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Authentication (Google, GitHub, Email, Phone, etc.)&lt;/li&gt;
&lt;li&gt;Realtime database &amp;amp; Firestore&lt;/li&gt;
&lt;li&gt;Hosting &amp;amp; serverless functions&lt;/li&gt;
&lt;li&gt;Notifications and cloud messaging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2.&lt;strong&gt;Stripe provides&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Secure and compliant payments&lt;/li&gt;
&lt;li&gt;Subscriptions and billing&lt;/li&gt;
&lt;li&gt;Global payment methods&lt;/li&gt;
&lt;li&gt;Scalable APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When combined, Firebase + Stripe lets you build production-ready SaaS, e-commerce, or subscription apps with minimal backend code.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-firebase&quot;&gt;Setting Up Firebase&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://console.firebase.google.com/&quot;&gt;Firebase Console&lt;/a&gt; and create a new project&lt;/li&gt;
&lt;li&gt;Add Firebase Authentication → enable Email/Password or OAuth&lt;br&gt;
providers.&lt;/li&gt;
&lt;li&gt;Set up Cloud Firestore for storing user data and transactions.&lt;/li&gt;
&lt;li&gt;Deploy backend logic using Firebase Cloud Functions (powered by&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/serverless-framework&quot;&gt;serverless framework&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;setting-up-stripe&quot;&gt;Setting Up Stripe&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Create a free account on &lt;a rel=&quot;nofollow&quot; href=&quot;https://stripe.com/&quot;&gt;Stripe&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;From the dashboard, copy your API keys.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install the Stripe SDK:&lt;/p&gt;
&lt;p&gt;npm install stripe firebase-admin firebase-functions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a test product or subscription plan in your Stripe Dashboard.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;integrating-firebase-cloud-functions-with-stripe&quot;&gt;Integrating Firebase Cloud Functions with Stripe&lt;/h3&gt;
&lt;p&gt;Example function for creating a checkout session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const functions = require(&quot;firebase-functions&quot;);
const stripe = require(&quot;stripe&quot;)(process.env.STRIPE_SECRET_KEY);

exports.createCheckoutSession = functions.https.onCall(async (data, context) =&amp;gt; {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: [&quot;card&quot;],
    line_items: [
      {
        price: data.priceId,
        quantity: 1,
      },
    ],
    mode: &quot;subscription&quot;,
    success_url: &quot;https://yourapp.com/success&quot;,
    cancel_url: &quot;https://yourapp.com/cancel&quot;,
  });
  return { id: session.id };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function can be called from your frontend using Firebase SDK.&lt;/p&gt;
&lt;h3 id=&quot;best-practices-for-performance-amp-security&quot;&gt;Best Practices for Performance &amp;amp; Security&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use a &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/bastion-server&quot;&gt;bastion server&lt;/a&gt; or secure environment for sensitive API&lt;br&gt;
operations.&lt;/li&gt;
&lt;li&gt;Deploy in &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/serverless-containers&quot;&gt;serverless containers&lt;/a&gt; for scalability.&lt;/li&gt;
&lt;li&gt;Ensure PCI compliance via Stripe’s hosted checkout.&lt;/li&gt;
&lt;li&gt;Use Firebase Authentication to link payments with user accounts.&lt;/li&gt;
&lt;li&gt;Optimize billing operations with &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/server-optimization&quot;&gt;server optimization&lt;/a&gt; techniques.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;advanced-use-cases&quot;&gt;Advanced Use Cases&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SaaS Apps → Manage &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/serverless-ml&quot;&gt;serverless ML&lt;/a&gt; workflows, bill users per usage.&lt;/li&gt;
&lt;li&gt;Gaming Platforms → Use &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/lambda-gpus&quot;&gt;lambda GPUs&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/serverless-gpu&quot;&gt;serverless GPU&lt;/a&gt; for compute&lt;br&gt;
while charging via Stripe.&lt;/li&gt;
&lt;li&gt;Virtual Desktops → Combine with &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/non-persistent-vdi&quot;&gt;non-persistent VDI&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/aws-appstream-vs-workspaces&quot;&gt;AWS AppStream&lt;br&gt;
vs Workspaces&lt;/a&gt; billing.&lt;/li&gt;
&lt;li&gt;Hybrid Architectures → Deploy with &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/on-premises-server&quot;&gt;on premises servers&lt;/a&gt; + Firebase&lt;br&gt;
Cloud Functions.&lt;/li&gt;
&lt;li&gt;Frontend Hosting → Integrate with &lt;a rel=&quot;nofollow&quot; href=&quot;https://serverlesssavants.org/vercel-serverless&quot;&gt;Vercel Serverless&lt;/a&gt; or similar&lt;br&gt;
providers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;By combining Firebase and Stripe, you eliminate the need for managing on prem servers, payment gateways, or complex authentication stacks. This integration is ideal for startups, SaaS projects, and enterprise systems looking for top-rated serverless hosting for frontend developers.&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/4804/using-firebase-and-stripe-for-modern-web-applications</guid>
<pubDate>Mon, 18 Aug 2025 20:25:55 +0000</pubDate>
</item>
<item>
<title>Learn how to integrate Firebase AI Logic into your iOS app.</title>
<link>https://coderlegion.com/3286/learn-how-to-integrate-firebase-ai-logic-into-your-ios-app</link>
<description>&lt;h2 id=&quot;implementing-firebase-ai-logic-in-your-ios-app-with-swift&quot;&gt;Implementing Firebase AI Logic in Your iOS App with Swift&lt;/h2&gt;
&lt;p&gt;Google Firebase has introduced &lt;strong&gt;AI Logic&lt;/strong&gt;, a new way to build intelligent, dynamic behavior into your mobile apps using natural language prompts and structured logic—without needing to build or fine-tune your own models.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll walk through how to integrate Firebase AI Logic into your &lt;strong&gt;iOS app using Swift&lt;/strong&gt;, from setup to implementation.&lt;/p&gt;
&lt;h3 id=&quot;what-is-firebase-ai-logic-&quot;&gt;What Is Firebase AI Logic?&lt;/h3&gt;
&lt;p&gt;Firebase AI Logic is a managed service that allows you to define, test, and run AI-powered logic blocks using a mix of prompts and structured configuration. Think of it as serverless AI functions that take inputs, apply logic, and return structured outputs.&lt;/p&gt;
&lt;p&gt;With AI Logic, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create logic using natural language prompts.&lt;/li&gt;
&lt;li&gt;Call this logic from your iOS app using the &lt;strong&gt;Firebase AI SDK&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use AI-generated decisions to personalize your UI, make recommendations, and more.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xcode 16.2+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS 15+&lt;/strong&gt; or &lt;strong&gt;macOS 12+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase SDK v11.13.0+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A Firebase-connected app&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;firebase-console-setup&quot;&gt;Firebase Console Setup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to your project in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://console.firebase.google.com/&quot;&gt;Firebase Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;AI&lt;/strong&gt;, select &lt;strong&gt;AI Logic&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Start&lt;/strong&gt;, then under &lt;strong&gt;Gemini Developer API&lt;/strong&gt;, tap &lt;strong&gt;Start using this API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the dialog that appears, tap &lt;strong&gt;Enable&lt;/strong&gt;. Wait until the API is enabled.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Continue&lt;/strong&gt; to complete activation.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;enable-app-check-recommended-&quot;&gt;Enable App Check (Recommended)&lt;/h4&gt;
&lt;p&gt;To restrict API calls to authorized devices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the AI Logic panel, click &lt;strong&gt;Register on App Check&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the App Check dashboard, click &lt;strong&gt;Start&lt;/strong&gt;, then &lt;strong&gt;Register&lt;/strong&gt; for the app you want to secure.&lt;/li&gt;
&lt;li&gt;If your app supports only iOS 14+, you can register &lt;strong&gt;App Attest&lt;/strong&gt; only. Otherwise, also register &lt;strong&gt;Device Check&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complete all required fields and finish registration.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Next:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the AI Logic dashboard, go to the &lt;strong&gt;APIs&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Firebase AI Logic APIs&lt;/strong&gt;, then click &lt;strong&gt;Apply&lt;/strong&gt; to enforce App Check.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Enabling enforcement can take up to &lt;strong&gt;30 minutes&lt;/strong&gt; to propagate.&lt;/p&gt;
&lt;h3 id=&quot;enable-app-transport-security-exception&quot;&gt;Enable App Transport Security Exception&lt;/h3&gt;
&lt;p&gt;In my case, I also needed to enable &lt;strong&gt;App Transport Security Exception&lt;/strong&gt;.&lt;br&gt;
To do this, go to &lt;strong&gt;Project → Signing &amp;amp; Capabilities → Add Capability → App Transport Security Exception&lt;/strong&gt;.&lt;br&gt;
Inside the capability, tap the &lt;strong&gt;+&lt;/strong&gt; button and add &lt;strong&gt;googleapis.com&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;code-implementation&quot;&gt;Code Implementation&lt;/h3&gt;
&lt;h4 id=&quot;app-check&quot;&gt;App Check&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Add the &lt;strong&gt;Firebase App Check&lt;/strong&gt; library to your project.&lt;/li&gt;
&lt;li&gt;Create a file named &lt;code&gt;AppCheckManager.swift&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import Firebase
import FirebaseAppCheck
 
final class AppCheckManager: NSObject, AppCheckProviderFactory {
 
public static let shared = AppCheckManager()
 
func createProvider(with app: FirebaseApp) -&amp;gt; AppCheckProvider? {
return AppAttestProvider(app: app)
}
 
public func setProviderFactory() {
#if DEBUG
 AppCheck.setAppCheckProviderFactory(AppCheckDebugProviderFactory())
#else
AppCheck.setAppCheckProviderFactory(self)
#endif
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your &lt;code&gt;AppDelegate&lt;/code&gt; or main app file, before &lt;code&gt;FirebaseApp.configure()&lt;/code&gt;, add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;AppCheckManager.shared.setProviderFactory()
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;In your &lt;strong&gt;Signing &amp;amp; Capabilities&lt;/strong&gt;, add the &lt;strong&gt;App Attest&lt;/strong&gt; entitlement.&lt;/li&gt;
&lt;li&gt;In the generated &lt;code&gt;.entitlements&lt;/code&gt; file, change the App Attest mode from &lt;code&gt;development&lt;/code&gt; to &lt;code&gt;production&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;To enable Firebase debug logging:&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Product &amp;gt; Scheme &amp;gt; Edit Scheme…&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Run &amp;gt; Arguments&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Add a new argument: &lt;code&gt;-FIRDebugEnabled&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run the app. You should see a log like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;&amp;lt;Warning&amp;gt; [AppCheckCore][I-GAC004001] App Check debug token: '1E04B27F-15CD-46FA-862B-ED8E67841C9D'.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy this token, go to &lt;strong&gt;App Check &amp;gt; Apps&lt;/strong&gt; in the Firebase Console, click the three dots next to your iOS app, choose &lt;strong&gt;Manage debug tokens&lt;/strong&gt;, then add and save the token.&lt;/p&gt;
&lt;p&gt;You're done setting up App Check.&lt;/p&gt;
&lt;h3 id=&quot;using-firebase-ai-logic&quot;&gt;Using Firebase AI Logic&lt;/h3&gt;
&lt;p&gt;Add the &lt;code&gt;FirebaseAI&lt;/code&gt; package:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;General &amp;gt; Frameworks, Libraries, and Embedded Content&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Tap the &lt;strong&gt;+&lt;/strong&gt; button and add &lt;code&gt;FirebaseAI&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;simple-response&quot;&gt;Simple Response&lt;/h4&gt;
&lt;p&gt;To generate a text response from a prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;import FirebaseAI
 
func callAI() async {
do {
let prompt = &quot;Your prompt here...&quot;
let ai = FirebaseAI.firebaseAI(backend: .googleAI())
let model = ai.generativeModel(modelName: &quot;gemini-2.0-flash&quot;)
let response = try await model.generateContent(prompt)
 
if let answer = response.text {
print(&quot;FirebaseAI response:&quot;, answer)
}
} catch {
print(&quot;FirebaseAI error:&quot;, error)
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;streamed-response&quot;&gt;Streamed Response&lt;/h4&gt;
&lt;p&gt;For a more responsive UI, stream partial results as they’re generated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;func aiStrem() async {
do {
let prompt = &quot;Your prompt here...&quot;
let ai = FirebaseAI.firebaseAI(backend: .googleAI())
let model = ai.generativeModel(modelName: &quot;gemini-2.0-flash&quot;)
let response = try model.generateContentStream(prompt)
 
for try await chunk in response {
if let text = chunk.text {
print(&quot;FirebaseAI text:&quot;, text)
}
}
} catch {
print(&quot;FirebaseAI error:&quot;, error)
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h3&gt;
&lt;h4 id=&quot;http-403-errors&quot;&gt;HTTP 403 Errors&lt;/h4&gt;
&lt;p&gt;If you see a 403 error:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://console.cloud.google.com/&quot;&gt;Google Cloud Console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Select your Firebase project&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;APIs &amp;amp; Services &amp;gt; Credentials&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Find your iOS key, click it&lt;/li&gt;
&lt;li&gt;Ensure &lt;strong&gt;Firebase AI Logic API&lt;/strong&gt; is enabled under &lt;em&gt;API Restrictions&lt;/em&gt;. If not:&lt;ul&gt;
&lt;li&gt;Select it&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Wait up to 30 minutes before retrying&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the error persists:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporarily switch to &lt;strong&gt;“Don’t restrict key”&lt;/strong&gt; mode to test.&lt;/li&gt;
&lt;li&gt;You may also try &lt;strong&gt;disabling and re-enabling App Check&lt;/strong&gt; for Firebase AI Logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Firebase AI Logic allows iOS developers to integrate intelligent logic blocks into apps without the need to manage ML models or infrastructure. With App Check, you can keep API access secure, and with just a few lines of Swift, you can call or stream AI responses to build truly dynamic user experiences.&lt;/p&gt;
&lt;p&gt;Thanks for reading! If you have questions or want a complete sample project, feel free to reach out or leave a comment.&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/3286/learn-how-to-integrate-firebase-ai-logic-into-your-ios-app</guid>
<pubDate>Sun, 08 Jun 2025 20:36:07 +0000</pubDate>
</item>
<item>
<title>DevLog 20250523: Firebase Auth</title>
<link>https://coderlegion.com/3139/devlog-20250523-firebase-auth</link>
<description>&lt;h3 id=&quot;the-authentication-problem&quot;&gt;The Authentication Problem&lt;/h3&gt;
&lt;p&gt;Let's face it: develop a custom authentication service is conceptually straightforward but practically troublesome. You either allow user registration or not, then authenticate by taking a look at username and password (likely hashed) against database, then provide user profile results for authorized users. Throughout the process you might set browser cache, deal with (generated) API keys for app usages (so password need not be sent every time). It's a handful of steps, and lots of work to code everything piece by piece. Worse, there is a whole database to worry about.  One would naturally expect, such a thing should just exist as a service. Yes, there is.&lt;/p&gt;
&lt;h3 id=&quot;quick-comparison&quot;&gt;Quick Comparison&lt;/h3&gt;
&lt;p&gt;|Platform|What it is|Free tier|Pros|Cons|&lt;br&gt;
|-|-|-|-|-|&lt;br&gt;
|&lt;strong&gt;AWS Cognito&lt;/strong&gt; | Amazon’s identity and user‑management service for web and mobile apps, integrating natively with API Gateway/Lambda and Amplify JS. | 10,000 MAUs for direct/social sign‑in + 50 MAUs for SAML/OIDC federation; Identity Pools are free | No extra vendor to learn; pay only for MAUs above free tier; built‑in social and SAML/OIDC support; AWS‑managed hosted UI. | Learning curve around User Pools vs Identity Pools and IAM policies.|&lt;br&gt;
|&lt;strong&gt;Auth0&lt;/strong&gt; | Popular identity platform with SDKs for nearly every framework. | 25,000 MAUs; unlimited social &amp;amp; Okta connections; 5 organizations; 5 actions/forms; passwordless &amp;amp; custom domain  | Intuitive, developer‑friendly dashboard; extensible via Rules (JavaScript); excellent documentation. | Pricing increases sharply above free tier.|&lt;br&gt;
|&lt;strong&gt;Firebase Authentication&lt;/strong&gt; | Google’s auth service, part of Firebase/GCP. | Unlimited email/password, anonymous, and most social sign‑ins; pay only for phone‑SMS verification | Fastest setup for web/mobile; pre‑built UI widgets; seamless integration with other Firebase services. | SMS‑based authentication incurs per‑message cost.|&lt;br&gt;
|&lt;strong&gt;Supabase Auth&lt;/strong&gt; | Open‑source Firebase alternative built on PostgreSQL. | Unlimited API requests; 50,000 MAUs; 500 MB database; 5 GB bandwidth; 1 GB file storage; community support | True SQL under the hood; you own your data; robust JS client. | Free projects pause after 1 week of inactivity; limited to 2 active projects.|&lt;br&gt;
|&lt;strong&gt;Netlify Identity&lt;/strong&gt; | Netlify’s built‑in auth service powered by GoTrue. | Up to 1,000 registered users per site | Seamless drop‑in for Netlify‑hosted. | Limited to Netlify sites; basic compared to full‑blown IDaaS; deprecated as of Feb 28, 2025 .|&lt;br&gt;
|&lt;strong&gt;Okta Developer Edition&lt;/strong&gt;  | Enterprise‑grade identity platform in a developer sandbox. | 100 MAUs | Enterprise features: OpenID Connect federation, SCIM provisioning, lifecycle hooks, universal directory | Console can feel heavy for hobby projects.|&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recommendations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full AWS&lt;/strong&gt;: Use &lt;strong&gt;AWS Cognito&lt;/strong&gt; (with Amplify libraries) to stay within the AWS ecosystem and enjoy a free tier up to 10 K direct/social MAUs and 50 MAUs for SAML/OIDC. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rapid prototyping&lt;/strong&gt;: &lt;strong&gt;Firebase Authentication&lt;/strong&gt; for the fastest setup (unlimited email/password/social; pay only for SMS). &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL‑power &amp;amp; control&lt;/strong&gt;: &lt;strong&gt;Supabase Auth&lt;/strong&gt; if you need open‑source Postgres‑based auth with a 50 K MAU free tier and 500 MB of built‑in storage. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For reasons I would not disclose, we picked Firebase for Divooka and other Methodox services.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The Setup&lt;/h3&gt;
&lt;p&gt;For Firebase Auth, the &quot;API Key&quot; just identifies the application and can totally be shown on the client side script.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;utf-8&quot;/&amp;gt;
  &amp;lt;title&amp;gt;Custom Email/Password Auth&amp;lt;/title&amp;gt;
  &amp;lt;script type=&quot;module&quot;&amp;gt;
    import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.8.1/firebase-app.js&quot;;
    import {
      getAuth,
      createUserWithEmailAndPassword,
      updateProfile,
      signInWithEmailAndPassword,
      signOut
    } from &quot;https://www.gstatic.com/firebasejs/11.8.1/firebase-auth.js&quot;;

    // Firebase web config
    // For Firebase JS SDK v7.20.0 and later, measurementId is optional
    const firebaseConfig = {
        apiKey: &quot;*********************&quot;,
        authDomain: &quot;*********************&quot;,
        projectId: &quot;*********************&quot;,
        storageBucket: &quot;*********************&quot;,
        messagingSenderId: &quot;*********************&quot;,
        appId: &quot;*********************&quot;,
        measurementId: &quot;*********************&quot;
    };
    // Initialize Firebase
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);

    // Register a new user
    async function registerUser() {
      const email = document.getElementById(&quot;regEmail&quot;).value;
      const password = document.getElementById(&quot;regPass&quot;).value;
      const username = document.getElementById(&quot;regName&quot;).value;
      try {
        const cred = await createUserWithEmailAndPassword(auth, email, password);
        // Set a displayName on the Firebase user record
        await updateProfile(cred.user, { displayName: username });
        alert(&quot;Registration successful! You can now log in.&quot;);
      } catch (e) {
        console.error(e);
        alert(&quot;Registration error: &quot; + e.message);
      }
    }

    // Login existing user
    async function loginUser() {
      const email = document.getElementById(&quot;loginEmail&quot;).value;
      const password = document.getElementById(&quot;loginPass&quot;).value;
      try {
        const cred = await signInWithEmailAndPassword(auth, email, password);
        const idToken = await cred.user.getIdToken();

        // Call your API
        const resp = await fetch(&quot;https://myapi.com/profile&quot;, {
          headers: { &quot;Authorization&quot;: &quot;Bearer &quot; + idToken }
        });
        if (!resp.ok) throw new Error(await resp.text());
        const profile = await resp.json();
        document.getElementById(&quot;output&quot;).textContent = JSON.stringify(profile, null, 2);
      } catch (e) {
        console.error(e);
        alert(&quot;Login error: &quot; + e.message);
      }
    }

    // Logout
    function logoutUser() {
      signOut(auth);
      document.getElementById(&quot;output&quot;).textContent = &quot;&quot;;
      alert(&quot;Signed out&quot;);
    }

    window.onload = () =&amp;gt; {
      document.getElementById(&quot;btnRegister&quot;).onclick = registerUser;
      document.getElementById(&quot;btnLogin&quot;).onclick    = loginUser;
      document.getElementById(&quot;btnLogout&quot;).onclick   = logoutUser;
    };
  &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h2&amp;gt;Register&amp;lt;/h2&amp;gt;
  &amp;lt;input id=&quot;regName&quot;  type=&quot;text&quot;     placeholder=&quot;Username&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;regEmail&quot; type=&quot;email&quot;    placeholder=&quot;Email&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;regPass&quot;  type=&quot;password&quot; placeholder=&quot;Password&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;button id=&quot;btnRegister&quot;&amp;gt;Register&amp;lt;/button&amp;gt;

  &amp;lt;hr/&amp;gt;

  &amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;
  &amp;lt;input id=&quot;loginEmail&quot; type=&quot;email&quot;    placeholder=&quot;Email&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;loginPass&quot;  type=&quot;password&quot; placeholder=&quot;Password&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;button id=&quot;btnLogin&quot;&amp;gt;Log In&amp;lt;/button&amp;gt;
  &amp;lt;button id=&quot;btnLogout&quot;&amp;gt;Log Out&amp;lt;/button&amp;gt;

  &amp;lt;h3&amp;gt;Profile:&amp;lt;/h3&amp;gt;
  &amp;lt;pre id=&quot;output&quot;&amp;gt;&amp;lt;/pre&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;create-api-key&quot;&gt;Create API Key&lt;/h3&gt;
&lt;p&gt;To create API key, navigate to the project then find &quot;Add App&quot; in Project Settings, which is a &lt;strong&gt;hidden menu&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3xk2syz5jlz1g043ama1.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqu308x2hp0ljkq85fzy.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v0tpxprzuqyxiwkmt53z.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;Official doc on API key, which is not very clear without pictures: &lt;a rel=&quot;nofollow&quot; href=&quot;https://firebase.google.com/docs/projects/api-keys&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3139/devlog-20250523-firebase-auth</guid>
<pubDate>Fri, 23 May 2025 19:06:57 +0000</pubDate>
</item>
</channel>
</rss>