[{"data":1,"prerenderedAt":3909},["ShallowReactive",2],{"/blog/firebase-dynamic-links-shutdown-deep-linking-2026/":3,"/blog/firebase-dynamic-links-shutdown-deep-linking-2026/-related":830,"/blog/firebase-dynamic-links-shutdown-deep-linking-2026/-resources":3899},{"id":4,"title":5,"author":6,"body":10,"category":738,"createdAt":739,"description":740,"disclosure":741,"extension":742,"faq":743,"howTo":741,"location":759,"meta":760,"navigation":766,"ogImage":741,"path":767,"primaryKeyword":768,"readingTime":769,"related":770,"seo":774,"sources":775,"stem":820,"tags":821,"takeaways":822,"updatedAt":739,"url":828,"__hash__":829},"blog/blog/firebase-dynamic-links-shutdown-deep-linking-2026.md","Deep Linking in 2026: What Replaced Firebase Dynamic Links and How to Set It Up",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":729},"minimark",[13,21,24,29,72,75,79,82,103,106,125,128,132,269,272,276,311,314,318,321,351,455,480,556,559,634,637,641,668,671,697,700,704,707,725],[14,15,16,20],"p",{},[17,18,19],"strong",{},"A deep link is a link that opens a specific screen inside a mobile app, not just the app's front door. Firebase Dynamic Links, the free Google service many apps used for this, shut down on August 25, 2025."," In 2026 you have two real choices. Use Universal Links and Android App Links on your own domain for free, or pay a provider such as Branch, AppsFlyer, Adjust or Airbridge if you need links that survive an app install.",[14,22,23],{},"This guide replaces the older deep linking explainers and vendor docs that no longer load. Every date and price below comes from the vendor's own page, checked on October 17, 2026.",[25,26,28],"h2",{"id":27},"what-deep-linking-means-in-plain-words","What deep linking means, in plain words",[30,31,32,39,50,60,66],"ul",{},[33,34,35,38],"li",{},[17,36,37],{},"Deep link."," A link that goes to a place inside an app, such as a product, an invoice or an invite. A normal link opens the app's home screen at best.",[33,40,41,44,45,49],{},[17,42,43],{},"Universal Links (iOS)."," Apple's version. You tap a normal https link, and iOS opens your app if it is installed. If not, the link opens in the browser. Apple checks a file on your website, called ",[46,47,48],"code",{},"apple-app-site-association",", to confirm the link belongs to your app.",[33,51,52,55,56,59],{},[17,53,54],{},"Android App Links."," Google's version. Same idea. Android checks a file called ",[46,57,58],{},"assetlinks.json"," on your website.",[33,61,62,65],{},[17,63,64],{},"Deferred deep link."," A link that works even when the app is not installed yet. The user taps, installs the app from the store, and lands on the right screen on first open. This is what Dynamic Links did well, and it is the part plain Universal Links and App Links do not do.",[33,67,68,71],{},[17,69,70],{},"Click analytics."," Counts of who clicked which link. Dynamic Links had some. Google's migration guide offers no replacement for it.",[14,73,74],{},"Marketers mostly care about the last two. Developers mostly care about the first three.",[25,76,78],{"id":77},"what-happened-to-firebase-dynamic-links","What happened to Firebase Dynamic Links",[14,80,81],{},"Google's own FAQ says Dynamic Links shut down on August 25, 2025. This is what that means for old links:",[30,83,84,87,90,97,100],{},[33,85,86],{},"Every clicked link returns an HTTP 404 to the user.",[33,88,89],{},"Short link creation and link stats API calls return 400 or 403 errors.",[33,91,92,93,96],{},"Existing ",[46,94,95],{},".page.link"," domains cannot be kept or transferred. Domain and link data is marked for deletion on the shutdown date.",[33,98,99],{},"The SDKs will not crash your app by themselves, but your code has to handle the new responses. For example, on iOS, short link creation now returns a 403, and first-open attribution returns empty data.",[33,101,102],{},"Email link sign-in, password reset and email verification on mobile needed updated SDKs. Google has separate guides for iOS and Android.",[14,104,105],{},"Google's own advice, in its FAQ:",[107,108,109,112,115,118],"ol",{},[33,110,111],{},"If you need full feature parity, use a third-party provider. The FAQ names Adjust, Airbridge, AppsFlyer, Bitly, Branch, Kochava and Singular, and says Google has not vetted them.",[33,113,114],{},"If you only need to open an installed app, use Universal Links and App Links. The official migration guide covers this path.",[33,116,117],{},"If you no longer need the links, review them, delete the URL prefixes and remove the SDK.",[33,119,120,121,124],{},"Export your link metadata, and if you keep a custom domain, update your rewrite rules in ",[46,122,123],{},"firebase.json",".",[14,126,127],{},"The migration guide is honest about the gap. Universal Links and App Links do not send users to the app store, and do not carry the journey through a first install. For iOS it recommends Smart App Banners as the closest substitute.",[25,129,131],{"id":130},"what-to-use-now","What to use now",[133,134,135,154],"table",{},[136,137,138],"thead",{},[139,140,141,145,148,151],"tr",{},[142,143,144],"th",{},"Option",[142,146,147],{},"Deferred deep linking",[142,149,150],{},"Click analytics",[142,152,153],{},"Price (checked October 17, 2026)",[155,156,157,173,195,215,235,254],"tbody",{},[139,158,159,165,168,170],{},[160,161,162],"td",{},[17,163,164],{},"Universal Links + App Links on your own domain",[160,166,167],{},"No",[160,169,167],{},[160,171,172],{},"Free. You pay for your domain and hosting.",[139,174,175,180,183,185],{},[160,176,177],{},[17,178,179],{},"Branch",[160,181,182],{},"Yes",[160,184,182],{},[160,186,187,188],{},"Basics plan: free trial, no price shown. Essentials and Enterprise: \"Request a demo.\" See ",[189,190,194],"a",{"href":191,"rel":192},"https://www.branch.io/pricing/",[193],"nofollow","branch.io/pricing",[139,196,197,202,205,207],{},[160,198,199],{},[17,200,201],{},"AppsFlyer (OneLink)",[160,203,204],{},"Yes, from the Standard plan (Zero is free but has no deferred linking)",[160,206,182],{},[160,208,209,210],{},"Deep Linking plans: Zero is labelled \"Free.\" Standard, Advanced and Premium show \"Custom pricing\" with \"Request a demo.\" See ",[189,211,214],{"href":212,"rel":213},"https://www.appsflyer.com/pricing/deep-linking",[193],"appsflyer.com/pricing/deep-linking",[139,216,217,222,224,226],{},[160,218,219],{},[17,220,221],{},"Adjust",[160,223,182],{},[160,225,182],{},[160,227,228,229,234],{},"See ",[189,230,233],{"href":231,"rel":232},"https://www.adjust.com/pricing/",[193],"adjust.com/pricing",". Its pricing page would not load for us on October 17, so we are not quoting a number.",[139,236,237,242,244,246],{},[160,238,239],{},[17,240,241],{},"Airbridge",[160,243,182],{},[160,245,182],{},[160,247,248,249],{},"Core plan: $40+ a month after a 30-day free trial, deep linking included. Growth plan: custom. See ",[189,250,253],{"href":251,"rel":252},"https://www.airbridge.io/pricing",[193],"airbridge.io/pricing",[139,255,256,261,264,266],{},[160,257,258],{},[17,259,260],{},"Bitly, Kochava, Singular",[160,262,263],{},"Check each vendor",[160,265,263],{},[160,267,268],{},"Named in Google's FAQ. Prices not checked here, so see each vendor's pricing page.",[14,270,271],{},"Two cautions on that table. First, \"deferred\" and \"analytics\" here are what these vendors sell in general. Test the exact features on the plan you are quoted. Second, several of these tools are attribution platforms first and link tools second. If you only need links, that can be a lot of product to buy.",[25,273,275],{"id":274},"how-to-choose","How to choose",[107,277,278,284,290,296,302],{},[33,279,280,283],{},[17,281,282],{},"Your users already have the app, and links come from emails, your website or in-app shares."," Use plain Universal Links and App Links. It is free, it is the platform's own mechanism, and no vendor can shut it down.",[33,285,286,289],{},[17,287,288],{},"You run paid ads that send new users to the app store."," You need deferred deep linking and attribution. Pick a provider, and ask for a quote that matches your install volume.",[33,291,292,295],{},[17,293,294],{},"You are a small team that wants links and little else."," Look at the lowest published tier first (Airbridge Core or AppsFlyer Zero; Zero covers basic routing only), and read what the free or entry plan actually excludes before you commit.",[33,297,298,301],{},[17,299,300],{},"You want first-open routing without a vendor."," Combine Universal Links, a Smart App Banner on iOS, and a small server route that remembers the intended screen. It takes some build time, but you own it.",[33,303,304,307,308,310],{},[17,305,306],{},"You have old Dynamic Links printed or published."," Links on a ",[46,309,95],{}," domain are gone for good, because Google will not let anyone keep those domains. If your links used your own custom domain, point that domain at your website and redirect the old paths to the right pages. Either way, publish new links on your own domain.",[14,312,313],{},"If you are unsure, start with option 1. You can add a provider later without changing your domain files.",[25,315,317],{"id":316},"set-up-universal-links-and-app-links-on-your-own-domain","Set up Universal Links and App Links on your own domain",[14,319,320],{},"Use a domain you control, and use the same domain in every step. Apple and Google both want the files served over HTTPS with no redirects.",[107,322,323,337],{},[33,324,325,328,329,332,333,336],{},[17,326,327],{},"Pick the links."," Decide which paths open the app, for example ",[46,330,331],{},"/invite/*"," or ",[46,334,335],{},"/invoice/*",". Keep the list short.",[33,338,339,342,343,346,347,350],{},[17,340,341],{},"Create the iOS file."," Host it at ",[46,344,345],{},"https://yourdomain.com/.well-known/apple-app-site-association",". The name has no ",[46,348,349],{},".json"," extension. Apple's docs say to host it over HTTPS with a valid certificate and no redirects. A minimal version looks like this:",[352,353,358],"pre",{"className":354,"code":355,"language":356,"meta":357,"style":357},"language-json shiki shiki-themes github-light github-dark","{\n  \"applinks\": {\n    \"details\": [\n      {\n        \"appIDs\": [\"TEAMID.com.example.app\"],\n        \"components\": [{ \"/\": \"/invite/*\" }]\n      }\n    ]\n  }\n}\n","json","",[46,359,360,369,379,388,394,410,431,437,443,449],{"__ignoreMap":357},[361,362,365],"span",{"class":363,"line":364},"line",1,[361,366,368],{"class":367},"sVt8B","{\n",[361,370,372,376],{"class":363,"line":371},2,[361,373,375],{"class":374},"sj4cs","  \"applinks\"",[361,377,378],{"class":367},": {\n",[361,380,382,385],{"class":363,"line":381},3,[361,383,384],{"class":374},"    \"details\"",[361,386,387],{"class":367},": [\n",[361,389,391],{"class":363,"line":390},4,[361,392,393],{"class":367},"      {\n",[361,395,397,400,403,407],{"class":363,"line":396},5,[361,398,399],{"class":374},"        \"appIDs\"",[361,401,402],{"class":367},": [",[361,404,406],{"class":405},"sZZnC","\"TEAMID.com.example.app\"",[361,408,409],{"class":367},"],\n",[361,411,413,416,419,422,425,428],{"class":363,"line":412},6,[361,414,415],{"class":374},"        \"components\"",[361,417,418],{"class":367},": [{ ",[361,420,421],{"class":374},"\"/\"",[361,423,424],{"class":367},": ",[361,426,427],{"class":405},"\"/invite/*\"",[361,429,430],{"class":367}," }]\n",[361,432,434],{"class":363,"line":433},7,[361,435,436],{"class":367},"      }\n",[361,438,440],{"class":363,"line":439},8,[361,441,442],{"class":367},"    ]\n",[361,444,446],{"class":363,"line":445},9,[361,447,448],{"class":367},"  }\n",[361,450,452],{"class":363,"line":451},10,[361,453,454],{"class":367},"}\n",[107,456,457,467],{"start":381},[33,458,459,462,463,466],{},[17,460,461],{},"Add the iOS entitlement."," In Xcode, open Signing & Capabilities, add Associated Domains, and add ",[46,464,465],{},"applinks:yourdomain.com",". Use the bare domain, with no path and no trailing slash. Personal development teams cannot use this capability (per Flutter's setup guide).",[33,468,469,342,472,475,476,479],{},[17,470,471],{},"Create the Android file.",[46,473,474],{},"https://yourdomain.com/.well-known/assetlinks.json",", with content type ",[46,477,478],{},"application/json"," and no redirects:",[352,481,483],{"className":354,"code":482,"language":356,"meta":357,"style":357},"[{\n  \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n  \"target\": {\n    \"namespace\": \"android_app\",\n    \"package_name\": \"com.example.app\",\n    \"sha256_cert_fingerprints\": [\"AA:BB:...:FF\"]\n  }\n}]\n",[46,484,485,490,502,509,522,534,547,551],{"__ignoreMap":357},[361,486,487],{"class":363,"line":364},[361,488,489],{"class":367},"[{\n",[361,491,492,495,497,500],{"class":363,"line":371},[361,493,494],{"class":374},"  \"relation\"",[361,496,402],{"class":367},[361,498,499],{"class":405},"\"delegate_permission/common.handle_all_urls\"",[361,501,409],{"class":367},[361,503,504,507],{"class":363,"line":381},[361,505,506],{"class":374},"  \"target\"",[361,508,378],{"class":367},[361,510,511,514,516,519],{"class":363,"line":390},[361,512,513],{"class":374},"    \"namespace\"",[361,515,424],{"class":367},[361,517,518],{"class":405},"\"android_app\"",[361,520,521],{"class":367},",\n",[361,523,524,527,529,532],{"class":363,"line":396},[361,525,526],{"class":374},"    \"package_name\"",[361,528,424],{"class":367},[361,530,531],{"class":405},"\"com.example.app\"",[361,533,521],{"class":367},[361,535,536,539,541,544],{"class":363,"line":412},[361,537,538],{"class":374},"    \"sha256_cert_fingerprints\"",[361,540,402],{"class":367},[361,542,543],{"class":405},"\"AA:BB:...:FF\"",[361,545,546],{"class":367},"]\n",[361,548,549],{"class":363,"line":433},[361,550,448],{"class":367},[361,552,553],{"class":363,"line":439},[361,554,555],{"class":367},"}]\n",[14,557,558],{},"If you use Google Play App Signing, copy the fingerprint from the Play Console (Release, Setup, App signing). The one from your local keystore will usually not match what is on users' phones.",[107,560,561,594,600],{"start":396},[33,562,563,566,567,570,571,574,575,578,579,582,583,586,587,590,591,124],{},[17,564,565],{},"Add the Android intent filter."," In ",[46,568,569],{},"AndroidManifest.xml",", add an ",[46,572,573],{},"intent-filter"," with ",[46,576,577],{},"android:autoVerify=\"true\"",", the ",[46,580,581],{},"VIEW"," action, the ",[46,584,585],{},"DEFAULT"," and ",[46,588,589],{},"BROWSABLE"," categories, and ",[46,592,593],{},"\u003Cdata android:scheme=\"https\" android:host=\"yourdomain.com\" />",[33,595,596,599],{},[17,597,598],{},"Handle the link in your app."," Read the incoming URL and route to the right screen. Do this for both a cold start and an app that is already running.",[33,601,602,605],{},[17,603,604],{},"Test before you ship.",[30,606,607,614,631],{},[33,608,609,610,613],{},"iOS: Apple fetches your file through its own CDN. Per Apple's docs, the CDN asks for your file within 24 hours, and devices re-check about once a week. You can see what Apple has cached at ",[46,611,612],{},"https://app-site-association.cdn-apple.com/a/v1/yourdomain.com",". For a private dev server, Apple's alternate mode bypasses the CDN. Then tap a link from the Notes app on a real device.",[33,615,616,617,620,621,624,625,630],{},"Android: run ",[46,618,619],{},"adb shell pm get-app-links com.example.app"," and look for ",[46,622,623],{},"verified"," next to your domain. Google's ",[189,626,629],{"href":627,"rel":628},"https://developers.google.com/digital-asset-links/tools/generator",[193],"Statement List Generator and Tester"," can generate and check the file.",[33,632,633],{},"We know of no Apple-run web validator for the apple-app-site-association file today, so on iOS rely on the CDN URL above and a real device.",[14,635,636],{},"The usual failures are a redirect in front of the file (including www to non-www), the wrong signing fingerprint, a Team ID typo, and testing before Apple's CDN has caught up.",[25,638,640],{"id":639},"flutter-specifics","Flutter specifics",[14,642,643,644,586,649,654,655,658,659,586,662,665,666,124],{},"Flutter has official guides for both platforms: ",[189,645,648],{"href":646,"rel":647},"https://docs.flutter.dev/cookbook/navigation/set-up-universal-links",[193],"set up universal links",[189,650,653],{"href":651,"rel":652},"https://docs.flutter.dev/cookbook/navigation/set-up-app-links",[193],"set up app links",". Per those pages, Flutter 3.27 and later turn deep linking on by default. Older versions need ",[46,656,657],{},"FlutterDeepLinkingEnabled"," set to YES in ",[46,660,661],{},"Info.plist",[46,663,664],{},"flutter_deeplinking_enabled"," set to true in ",[46,667,569],{},[14,669,670],{},"Two ways to receive links:",[30,672,673,683],{},[33,674,675,678,679,682],{},[17,676,677],{},"go_router."," Flutter's built-in handling feeds incoming links to the ",[46,680,681],{},"Router",". go_router (version 18.1.0 on pub.dev today) is a declarative router built on that system and lists deep linking as a feature. Define your paths as routes, and an incoming link lands on the matching screen.",[33,684,685,688,689,692,693,696],{},[17,686,687],{},"app_links."," The ",[46,690,691],{},"app_links"," package (7.2.2 today) handles Android App Links, iOS Universal Links and custom schemes. You subscribe to ",[46,694,695],{},"AppLinks().uriLinkStream",", which delivers both the first link and later ones. Per Flutter's docs, if you use a plugin like this, set the deep linking flag to false so Flutter's own handler does not conflict.",[14,698,699],{},"Flutter DevTools also has a Deep Links validator. It checks website configuration and manifest files, and works for Android and iOS from Flutter 3.27.",[25,701,703],{"id":702},"need-a-hand","Need a hand?",[14,705,706],{},"App369 is Simon Dziak's studio. He builds and runs Workspace369, a production Flutter and Firebase SaaS.",[30,708,709,717],{},[33,710,711,712,716],{},"Links open the wrong screen, or the app never opens from a link: the ",[189,713,715],{"href":714},"/fixed-price/flutter-firebase-bug-fix/","App Fix Ticket"," diagnoses one issue for $450 and fixes it for $1,200, both fixed price (the $450 is credited toward the fix).",[33,718,719,720,724],{},"Connecting a link provider, your backend and a CRM or Stripe: ",[189,721,723],{"href":722},"/fixed-price/api-integration/","API Integration",", $1,200 fixed price, 5 business days.",[726,727,728],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":357,"searchDepth":371,"depth":371,"links":730},[731,732,733,734,735,736,737],{"id":27,"depth":371,"text":28},{"id":77,"depth":371,"text":78},{"id":130,"depth":371,"text":131},{"id":274,"depth":371,"text":275},{"id":316,"depth":371,"text":317},{"id":639,"depth":371,"text":640},{"id":702,"depth":371,"text":703},"AI & Technology","2026-10-17T12:00:00.000Z","What deep linking is, what happened when Firebase Dynamic Links shut down on August 25, 2025, and how to set up Universal Links and Android App Links in 2026. Vendor options and prices checked October 17, 2026.",null,"md",[744,747,750,753,756],{"question":745,"answer":746},"When did Firebase Dynamic Links shut down?","On August 25, 2025. Google says all clicked links return an HTTP 404, short link and link stats API calls return 400 or 403 errors, and existing .page.link domains cannot be kept or transferred.",{"question":748,"answer":749},"What replaced Firebase Dynamic Links?","Nothing from Google replaced it one for one. Google tells developers to use Universal Links and Android App Links for links that open an installed app, and a third-party deep linking provider for full feature parity. Google names Adjust, Airbridge, AppsFlyer, Bitly, Branch, Kochava and Singular, and says it has not vetted them.",{"question":751,"answer":752},"What is a deferred deep link?","A link that still works when the app is not installed yet. The user taps, goes to the app store, installs the app, and lands on the right screen on first open. Universal Links and App Links cannot do this on their own. You need a provider or your own server logic.",{"question":754,"answer":755},"Do I need a paid service for deep links?","Not if your users already have the app. Universal Links and App Links cost nothing beyond a domain you own. You need a provider only for deferred deep linking, click analytics, or campaign attribution.",{"question":757,"answer":758},"How do I test Universal Links and App Links?","On iOS, check the file through Apple's CDN and tap a link from the Notes app. On Android, use adb pm get-app-links to see whether your domain shows as verified, and Google's Statement List Generator and Tester to check assetlinks.json. For Flutter apps, DevTools has a Deep Links validator.","Boynton Beach, FL",{"readingTimeStats":761},{"text":762,"minutes":763,"time":764,"words":765},"8 min read",7.7,462000,1540,true,"/blog/firebase-dynamic-links-shutdown-deep-linking-2026","firebase dynamic links alternative","9 min",[771,772,773],"mobile-app-analytics-tracking-guide-2026","firebase-security-guide-2026","how-to-debug-a-flutter-firebase-bug-2026",{"title":5,"description":740},[776,779,782,785,788,791,794,796,799,801,803,806,809,812,814,816,818],{"title":777,"url":778},"Firebase Dynamic Links FAQ (shutdown date, link behaviour, recommended alternatives)","https://firebase.google.com/support/dynamic-links-faq",{"title":780,"url":781},"Firebase, Migrate to App Links and Universal Links (official migration guide)","https://firebase.google.com/support/guides/app-links-universal-links",{"title":783,"url":784},"Apple, Supporting associated domains","https://developer.apple.com/documentation/xcode/supporting-associated-domains",{"title":786,"url":787},"Apple CDN copy of an apple-app-site-association file (example: www.apple.com)","https://app-site-association.cdn-apple.com/a/v1/www.apple.com",{"title":789,"url":790},"Android, Verify Android App Links","https://developer.android.com/training/app-links/verify-android-applinks",{"title":792,"url":793},"Android, Configure website associations (assetlinks.json requirements)","https://developer.android.com/training/app-links/configure-assetlinks",{"title":795,"url":627},"Google, Statement List Generator and Tester",{"title":797,"url":798},"Flutter, Deep linking","https://docs.flutter.dev/ui/navigation/deep-linking",{"title":800,"url":646},"Flutter, Set up universal links (iOS)",{"title":802,"url":651},"Flutter, Set up app links (Android)",{"title":804,"url":805},"Flutter DevTools, Validate deep links","https://docs.flutter.dev/tools/devtools/deep-links",{"title":807,"url":808},"app_links package on pub.dev","https://pub.dev/packages/app_links",{"title":810,"url":811},"go_router package on pub.dev","https://pub.dev/packages/go_router",{"title":813,"url":191},"Branch pricing",{"title":815,"url":212},"AppsFlyer deep linking pricing",{"title":817,"url":231},"Adjust pricing",{"title":819,"url":251},"Airbridge pricing","blog/firebase-dynamic-links-shutdown-deep-linking-2026","firebase dynamic links alternative, deep linking, deferred deep linking, universal links, android app links, apple-app-site-association, assetlinks.json, branch alternatives, appsflyer onelink, flutter deep linking, mobile deep linking",[823,824,825,826,827],"Firebase Dynamic Links shut down on August 25, 2025. Clicked links now return an HTTP 404, and Google says the .page.link domains cannot be kept or transferred.","Google names two routes. Use Universal Links and Android App Links if you only need links to open an installed app. Use a third-party provider if you need full feature parity.","Universal Links and App Links are free and live on your own domain. They do not do deferred deep linking, and they do not give you click analytics.","Two files make it work: /.well-known/apple-app-site-association on iOS and /.well-known/assetlinks.json on Android. Serve both over HTTPS with no redirects.","Pricing as of October 17, 2026: Airbridge Core is $40+ a month, AppsFlyer has a free Zero plan, Branch and the paid AppsFlyer plans are sales-led, and Adjust lists no price we could read.","https://app369.com/blog/firebase-dynamic-links-shutdown-deep-linking-2026/","kWZsntPKkyZ0cbGNktuWgqusyXrXGdyedktgEIg6sDo",[831,2110,3491],{"id":832,"title":833,"author":834,"body":837,"category":2092,"createdAt":2093,"description":2094,"disclosure":741,"extension":742,"faq":741,"howTo":741,"location":2095,"meta":2096,"navigation":766,"ogImage":741,"path":2102,"primaryKeyword":741,"readingTime":2103,"related":741,"seo":2104,"sources":741,"stem":2105,"tags":2106,"takeaways":741,"updatedAt":2107,"url":2108,"__hash__":2109},"blog/blog/mobile-app-analytics-tracking-guide-2026.md","Mobile App Analytics & Tracking Guide (2026)",{"name":7,"social":835},{"twitter":836},"@app369dev",{"type":11,"value":838,"toc":2042},[839,845,854,858,861,864,890,894,897,902,912,915,921,1004,1010,1014,1020,1023,1029,1033,1045,1051,1057,1061,1067,1073,1082,1086,1089,1093,1099,1102,1107,1127,1133,1139,1147,1151,1156,1159,1163,1186,1191,1195,1200,1203,1207,1230,1235,1239,1244,1254,1260,1266,1270,1273,1277,1280,1306,1311,1345,1349,1352,1357,1384,1389,1407,1414,1418,1421,1453,1473,1477,1480,1500,1503,1507,1510,1527,1531,1534,1538,1541,1573,1577,1580,1612,1616,1619,1622,1642,1646,1649,1688,1695,1699,1702,1706,1720,1724,1795,1799,1831,1834,1838,1841,1845,1851,1857,1863,1867,1870,1890,1894,1897,1901,1904,1924,1928,1931,1935,1967,1971,1975,1981,1985,1992,1996,1999,2003,2006,2010,2013,2017,2020,2031],[14,840,841,844],{},[17,842,843],{},"Mobile app analytics is the backbone of every successful app in 2026."," Without data-driven insights into how users interact with your application, you are essentially flying blind — making decisions based on assumptions rather than evidence.",[14,846,847,848,853],{},"At ",[189,849,852],{"href":850,"rel":851},"https://app369.com",[193],"App369",", we integrate analytics into every application we build from day one. In this comprehensive guide, we cover the key metrics you need to track, the best analytics tools available in 2026, how to implement tracking without sacrificing user privacy, and advanced techniques like cohort analysis and A/B testing that separate successful apps from the rest.",[25,855,857],{"id":856},"why-mobile-app-analytics-matters-more-than-ever","Why Mobile App Analytics Matters More Than Ever",[14,859,860],{},"The mobile app market has never been more competitive. Getting users to download your app is only the first hurdle — keeping them engaged is where analytics becomes indispensable.",[14,862,863],{},"Here is why analytics should be a non-negotiable part of your app strategy:",[107,865,866,872,878,884],{},[33,867,868,871],{},[17,869,870],{},"User retention is more cost-effective than acquisition."," Paid user acquisition is expensive, so keeping the users you already have through data-informed improvements is usually the cheaper lever.",[33,873,874,877],{},[17,875,876],{},"Most installs do not stick."," Analytics shows where new users drop off and why.",[33,879,880,883],{},[17,881,882],{},"Revenue optimization requires granular data."," Pricing, paywall and checkout changes only pay off when you can measure them per segment.",[33,885,886,889],{},[17,887,888],{},"Investors and stakeholders demand metrics."," If you are seeking funding or reporting to a board, key performance indicators like DAU/MAU ratio, LTV, and churn rate are table stakes.",[25,891,893],{"id":892},"key-metrics-every-app-should-track","Key Metrics Every App Should Track",[14,895,896],{},"Not all metrics are created equal. Tracking too many vanity metrics can be just as harmful as tracking too few. Here are the essential metrics organized by category.",[898,899,901],"h3",{"id":900},"engagement-metrics","Engagement Metrics",[14,903,904,907,908,911],{},[17,905,906],{},"Daily Active Users (DAU) and Monthly Active Users (MAU)"," are the most fundamental engagement metrics. The ",[17,909,910],{},"DAU/MAU ratio"," (also called the stickiness ratio) tells you how often your monthly users come back each day.",[14,913,914],{},"Healthy ratios vary a lot by category: messaging and social apps are used daily, while shopping or travel apps are opened far less often. Compare your ratio against your own history and apps in the same category rather than a single universal target.",[14,916,917,920],{},[17,918,919],{},"Session length and frequency"," reveal how users interact with your app on each visit.",[133,922,923,936],{},[136,924,925],{},[139,926,927,930,933],{},[142,928,929],{},"App Category",[142,931,932],{},"Avg Session Length",[142,934,935],{},"Avg Sessions/Day",[155,937,938,949,960,971,982,993],{},[139,939,940,943,946],{},[160,941,942],{},"Gaming",[160,944,945],{},"8.2 min",[160,947,948],{},"2.1",[139,950,951,954,957],{},[160,952,953],{},"Social Media",[160,955,956],{},"7.4 min",[160,958,959],{},"4.8",[139,961,962,965,968],{},[160,963,964],{},"E-commerce",[160,966,967],{},"5.1 min",[160,969,970],{},"1.4",[139,972,973,976,979],{},[160,974,975],{},"Finance",[160,977,978],{},"3.2 min",[160,980,981],{},"1.8",[139,983,984,987,990],{},[160,985,986],{},"Health & Fitness",[160,988,989],{},"6.8 min",[160,991,992],{},"1.2",[139,994,995,998,1001],{},[160,996,997],{},"Productivity",[160,999,1000],{},"4.5 min",[160,1002,1003],{},"3.3",[14,1005,1006,1009],{},[17,1007,1008],{},"Screen flow analysis"," maps the paths users take through your app. This reveals your most-visited screens, common navigation patterns, and where users get stuck or leave. Tools like Firebase Analytics and Mixpanel provide visual funnel reports that make this analysis straightforward.",[898,1011,1013],{"id":1012},"retention-metrics","Retention Metrics",[14,1015,1016,1019],{},[17,1017,1018],{},"Day 1, Day 7, and Day 30 retention rates"," are critical benchmarks for app health.",[14,1021,1022],{},"Retention typically drops steeply after the first day and flattens over the following weeks. Benchmark against your category (published benchmarks from analytics vendors differ by category and year). If your curve keeps falling instead of flattening, it signals a problem with onboarding, core value delivery, or overall user experience.",[14,1024,1025,1028],{},[17,1026,1027],{},"Churn rate"," is the inverse of retention — the percentage of users who stop using your app within a given period.",[898,1030,1032],{"id":1031},"revenue-and-monetization-metrics","Revenue and Monetization Metrics",[14,1034,1035,586,1038,1041,1042,124],{},[17,1036,1037],{},"Average Revenue Per User (ARPU)",[17,1039,1040],{},"Lifetime Value (LTV)"," are essential for understanding the financial health of your app. LTV should always exceed your Customer Acquisition Cost (CAC) — a healthy ratio is ",[17,1043,1044],{},"LTV:CAC of 3:1 or higher",[14,1046,1047,1050],{},[17,1048,1049],{},"Conversion rate"," tracks the percentage of users who complete a desired action (purchase, subscription, sign-up).",[14,1052,1053,1056],{},[17,1054,1055],{},"Revenue per session"," helps you understand the monetary value of each user interaction. This metric is particularly important for ad-supported apps and e-commerce applications.",[898,1058,1060],{"id":1059},"performance-metrics","Performance Metrics",[14,1062,1063,1066],{},[17,1064,1065],{},"App crash rate"," directly impacts user satisfaction and retention.",[14,1068,1069,1072],{},[17,1070,1071],{},"Load time and latency"," affect both user experience and app store rankings.",[14,1074,1075,1078,1079,124],{},[17,1076,1077],{},"API response time"," should be monitored to ensure backend performance does not degrade the user experience. A good target is ",[17,1080,1081],{},"under 200ms for critical API calls",[25,1083,1085],{"id":1084},"top-mobile-app-analytics-tools-in-2026","Top Mobile App Analytics Tools in 2026",[14,1087,1088],{},"Choosing the right analytics platform depends on your app's scale, budget, and the depth of analysis you need. Here is an in-depth comparison of the leading tools.",[898,1090,1092],{"id":1091},"firebase-analytics-google-analytics-for-firebase","Firebase Analytics (Google Analytics for Firebase)",[14,1094,1095,1098],{},[17,1096,1097],{},"Best for:"," Apps of all sizes, especially those using Firebase as their backend",[14,1100,1101],{},"It provides free, unlimited event logging with automatic collection of key metrics.",[14,1103,1104],{},[17,1105,1106],{},"Key features:",[30,1108,1109,1112,1115,1118,1121,1124],{},[33,1110,1111],{},"Automatic event tracking (first_open, session_start, screen_view, in_app_purchase)",[33,1113,1114],{},"Custom event logging with up to 500 distinct event types",[33,1116,1117],{},"Audience segmentation and user properties",[33,1119,1120],{},"Integration with Google Ads, BigQuery, and Crashlytics",[33,1122,1123],{},"Predictive analytics (churn probability, revenue predictions)",[33,1125,1126],{},"Funnel analysis and user flow visualization",[14,1128,1129,1132],{},[17,1130,1131],{},"Pricing:"," Free for core analytics. BigQuery export (for advanced analysis) costs based on data volume.",[14,1134,1135,1138],{},[17,1136,1137],{},"Limitations:"," Limited real-time reporting, 24-48 hour data latency for some reports, restricted to 25 user properties.",[14,1140,1141,1142,1146],{},"At App369, we use Firebase Analytics as the default analytics solution for most of our ",[189,1143,1145],{"href":1144},"/service/flutter-development/","Flutter development projects"," due to its native integration with the Firebase ecosystem.",[898,1148,1150],{"id":1149},"mixpanel","Mixpanel",[14,1152,1153,1155],{},[17,1154,1097],{}," Product teams that need deep behavioral analytics and experimentation",[14,1157,1158],{},"Mixpanel has evolved into one of the most powerful product analytics platforms, with a particular strength in event-based analysis, funnel optimization, and user segmentation.",[14,1160,1161],{},[17,1162,1106],{},[30,1164,1165,1168,1171,1174,1177,1180,1183],{},[33,1166,1167],{},"Unlimited event tracking with rich property support",[33,1169,1170],{},"Advanced funnel analysis with conversion breakdowns",[33,1172,1173],{},"Cohort analysis with retention curves",[33,1175,1176],{},"A/B testing and experimentation framework",[33,1178,1179],{},"Interactive query builder (no SQL required)",[33,1181,1182],{},"Real-time data availability",[33,1184,1185],{},"AI-powered insights and anomaly detection (launched in 2025)",[14,1187,1188,1190],{},[17,1189,1131],{}," Free tier supports up to 20 million events/month. Growth plan starts at $28/month. Enterprise pricing is custom.",[898,1192,1194],{"id":1193},"amplitude","Amplitude",[14,1196,1197,1199],{},[17,1198,1097],{}," Enterprise apps and teams focused on behavioral analytics at scale",[14,1201,1202],{},"Amplitude is the leading enterprise-grade product analytics platform, trusted by companies like PayPal, Walmart, and NBC Universal. Its strength lies in advanced behavioral analysis and data governance.",[14,1204,1205],{},[17,1206,1106],{},[30,1208,1209,1212,1215,1218,1221,1224,1227],{},[33,1210,1211],{},"Behavioral cohorting with unlimited segmentation",[33,1213,1214],{},"Journey mapping across mobile, web, and backend events",[33,1216,1217],{},"Revenue analytics with subscription tracking",[33,1219,1220],{},"Predictive analytics powered by machine learning",[33,1222,1223],{},"Data governance tools (taxonomy management, event approval workflows)",[33,1225,1226],{},"CDP (Customer Data Platform) integration",[33,1228,1229],{},"Session replay (launched 2025)",[14,1231,1232,1234],{},[17,1233,1131],{}," Free tier includes up to 50 million events/month. Growth plan starts at $49/month. Enterprise is custom.",[898,1236,1238],{"id":1237},"other-notable-tools","Other Notable Tools",[14,1240,1241,1243],{},[17,1242,221],{}," — Specializes in mobile attribution and fraud prevention. Essential if you run paid acquisition campaigns across multiple ad networks.",[14,1245,1246,1249,1250,1253],{},[17,1247,1248],{},"AppsFlyer"," — The market leader in mobile attribution, used by ",[17,1251,1252],{},"98 of the top 100 app advertisers"," globally. Critical for understanding which marketing channels drive the most valuable users.",[14,1255,1256,1259],{},[17,1257,1258],{},"Flurry Analytics (Yahoo)"," — A free alternative with solid basic analytics capabilities. Good for small apps that need simple dashboards without complex analysis.",[14,1261,1262,1265],{},[17,1263,1264],{},"UXCam"," — Offers session recording and heatmaps for mobile apps. Invaluable for understanding qualitative user behavior alongside quantitative analytics.",[25,1267,1269],{"id":1268},"implementing-analytics-a-step-by-step-guide","Implementing Analytics: A Step-by-Step Guide",[14,1271,1272],{},"Setting up analytics correctly from the start saves significant time and prevents data quality issues down the road. Here is how we approach analytics implementation at App369.",[898,1274,1276],{"id":1275},"step-1-define-your-measurement-plan","Step 1: Define Your Measurement Plan",[14,1278,1279],{},"Before writing a single line of tracking code, document what you need to measure and why. A measurement plan should include:",[30,1281,1282,1288,1294,1300],{},[33,1283,1284,1287],{},[17,1285,1286],{},"Business objectives:"," What does success look like for your app?",[33,1289,1290,1293],{},[17,1291,1292],{},"Key Performance Indicators (KPIs):"," Which 5-8 metrics directly indicate progress toward those objectives?",[33,1295,1296,1299],{},[17,1297,1298],{},"Event taxonomy:"," A structured naming convention for all custom events",[33,1301,1302,1305],{},[17,1303,1304],{},"User properties:"," What user attributes do you need to segment by?",[14,1307,1308],{},[17,1309,1310],{},"Event naming best practices:",[30,1312,1313,1326,1329,1339,1342],{},[33,1314,1315,1316,1319,1320,1319,1323],{},"Use snake_case: ",[46,1317,1318],{},"add_to_cart",", ",[46,1321,1322],{},"complete_purchase",[46,1324,1325],{},"view_product",[33,1327,1328],{},"Include a verb + noun structure",[33,1330,1331,1332,1335,1336],{},"Be specific: ",[46,1333,1334],{},"click_signup_button"," is better than ",[46,1337,1338],{},"click",[33,1340,1341],{},"Keep names under 40 characters",[33,1343,1344],{},"Document every event in a shared spreadsheet or data dictionary",[898,1346,1348],{"id":1347},"step-2-set-up-core-sdk-integration","Step 2: Set Up Core SDK Integration",[14,1350,1351],{},"For Flutter apps, the integration process typically follows this pattern:",[14,1353,1354],{},[17,1355,1356],{},"Firebase Analytics integration:",[107,1358,1359,1369,1375,1381],{},[33,1360,1361,1362,1365,1366],{},"Add ",[46,1363,1364],{},"firebase_analytics"," package to your ",[46,1367,1368],{},"pubspec.yaml",[33,1370,1371,1372],{},"Initialize Firebase in your ",[46,1373,1374],{},"main.dart",[33,1376,1377,1378],{},"Configure automatic screen tracking with the ",[46,1379,1380],{},"FirebaseAnalyticsObserver",[33,1382,1383],{},"Set user properties for segmentation (subscription tier, account age, etc.)",[14,1385,1386],{},[17,1387,1388],{},"Mixpanel integration:",[107,1390,1391,1398,1401,1404],{},[33,1392,1393,1394,1397],{},"Add the ",[46,1395,1396],{},"mixpanel_flutter"," package",[33,1399,1400],{},"Initialize with your project token",[33,1402,1403],{},"Identify users after authentication",[33,1405,1406],{},"Set super properties that attach to every event",[14,1408,1409,1410,124],{},"For a complete walkthrough of Firebase setup in Flutter apps, see our guide on ",[189,1411,1413],{"href":1412},"/blog/flutter-app-development-cost-2026/","Flutter app development",[898,1415,1417],{"id":1416},"step-3-implement-custom-event-tracking","Step 3: Implement Custom Event Tracking",[14,1419,1420],{},"Beyond automatic events, custom tracking captures the interactions unique to your app. Focus on tracking:",[30,1422,1423,1429,1435,1441,1447],{},[33,1424,1425,1428],{},[17,1426,1427],{},"Onboarding steps completed"," (each screen in the flow)",[33,1430,1431,1434],{},[17,1432,1433],{},"Core feature usage"," (the actions that define your app's value)",[33,1436,1437,1440],{},[17,1438,1439],{},"Conversion events"," (purchase, subscribe, complete_signup)",[33,1442,1443,1446],{},[17,1444,1445],{},"Error events"," (API failures, form validation errors)",[33,1448,1449,1452],{},[17,1450,1451],{},"Performance events"," (load times for critical screens)",[14,1454,1455,1458,1459,586,1462,1465,1466,1468,1469,1472],{},[17,1456,1457],{},"Pro tip:"," Use event properties to capture context. Instead of logging separate events for ",[46,1460,1461],{},"view_product_shoes",[46,1463,1464],{},"view_product_shirts",", log a single ",[46,1467,1325],{}," event with a ",[46,1470,1471],{},"category"," property. This keeps your event taxonomy clean and manageable.",[898,1474,1476],{"id":1475},"step-4-configure-funnels-and-goals","Step 4: Configure Funnels and Goals",[14,1478,1479],{},"Set up funnel analysis for your critical user journeys:",[30,1481,1482,1488,1494],{},[33,1483,1484,1487],{},[17,1485,1486],{},"Onboarding funnel:"," App open > Complete Step 1 > Complete Step 2 > Activation",[33,1489,1490,1493],{},[17,1491,1492],{},"Purchase funnel:"," View Product > Add to Cart > Begin Checkout > Complete Purchase",[33,1495,1496,1499],{},[17,1497,1498],{},"Subscription funnel:"," View Pricing > Start Trial > Convert to Paid",[14,1501,1502],{},"If your funnel shows significantly lower completion, it is a clear signal that your onboarding flow needs optimization.",[898,1504,1506],{"id":1505},"step-5-set-up-automated-alerts","Step 5: Set Up Automated Alerts",[14,1508,1509],{},"Configure alerts for critical metric changes so your team can respond quickly:",[30,1511,1512,1515,1518,1521,1524],{},[33,1513,1514],{},"Crash rate exceeds threshold (e.g., above 0.5%)",[33,1516,1517],{},"DAU drops more than 10% week-over-week",[33,1519,1520],{},"Conversion rate changes significantly",[33,1522,1523],{},"Revenue per user declines",[33,1525,1526],{},"Server response time exceeds acceptable limits",[25,1528,1530],{"id":1529},"privacy-compliance-gdpr-ccpa-and-beyond","Privacy Compliance: GDPR, CCPA, and Beyond",[14,1532,1533],{},"Privacy regulations have reshaped mobile analytics. In 2026, compliance is not optional — it is a legal requirement that carries significant financial penalties for violations.",[898,1535,1537],{"id":1536},"gdpr-general-data-protection-regulation","GDPR (General Data Protection Regulation)",[14,1539,1540],{},"The GDPR applies to any app that collects data from EU residents, regardless of where the app developer is based. Key requirements:",[30,1542,1543,1549,1555,1561,1567],{},[33,1544,1545,1548],{},[17,1546,1547],{},"Explicit consent"," before collecting any personal data",[33,1550,1551,1554],{},[17,1552,1553],{},"Right to access"," — users can request all data you hold about them",[33,1556,1557,1560],{},[17,1558,1559],{},"Right to erasure"," — users can request deletion of their data",[33,1562,1563,1566],{},[17,1564,1565],{},"Data minimization"," — only collect data that serves a specific, documented purpose",[33,1568,1569,1572],{},[17,1570,1571],{},"Data Protection Impact Assessment (DPIA)"," for high-risk processing",[898,1574,1576],{"id":1575},"ccpacpra-california-consumer-privacy-act","CCPA/CPRA (California Consumer Privacy Act)",[14,1578,1579],{},"The California Privacy Rights Act (CPRA), which expanded the original CCPA, requires:",[30,1581,1582,1588,1594,1600,1606],{},[33,1583,1584,1587],{},[17,1585,1586],{},"Opt-out mechanism"," for data selling/sharing",[33,1589,1590,1593],{},[17,1591,1592],{},"Right to know"," what data is collected and how it is used",[33,1595,1596,1599],{},[17,1597,1598],{},"Right to delete"," personal information",[33,1601,1602,1605],{},[17,1603,1604],{},"Right to correct"," inaccurate data",[33,1607,1608,1611],{},[17,1609,1610],{},"Sensitive personal information"," protections",[898,1613,1615],{"id":1614},"apple-app-tracking-transparency-att","Apple App Tracking Transparency (ATT)",[14,1617,1618],{},"Since iOS 14.5, Apple requires apps to request explicit permission before tracking user activity across other apps and websites.",[14,1620,1621],{},"This has fundamentally changed mobile analytics:",[30,1623,1624,1630,1636],{},[33,1625,1626,1629],{},[17,1627,1628],{},"First-party data is now king."," Invest in collecting data directly within your app rather than relying on cross-app tracking.",[33,1631,1632,1635],{},[17,1633,1634],{},"Probabilistic attribution"," has replaced deterministic tracking for many ad campaigns.",[33,1637,1638,1641],{},[17,1639,1640],{},"SKAdNetwork (SKAN)"," is Apple's privacy-preserving attribution framework. Version 5.0 (released in 2025) provides improved conversion value modeling.",[898,1643,1645],{"id":1644},"implementing-privacy-compliant-analytics","Implementing Privacy-Compliant Analytics",[14,1647,1648],{},"Here is our recommended approach for privacy-compliant analytics:",[107,1650,1651,1657,1663,1676,1682],{},[33,1652,1653,1656],{},[17,1654,1655],{},"Use a Consent Management Platform (CMP)"," to collect and manage user consent",[33,1658,1659,1662],{},[17,1660,1661],{},"Implement data anonymization"," — hash or remove personally identifiable information",[33,1664,1665,1668,1669,586,1672,1675],{},[17,1666,1667],{},"Configure analytics tools for privacy mode"," — Firebase supports ",[46,1670,1671],{},"analytics_storage: denied",[46,1673,1674],{},"ad_storage: denied"," settings",[33,1677,1678,1681],{},[17,1679,1680],{},"Set appropriate data retention periods"," — delete analytics data after 14-26 months",[33,1683,1684,1687],{},[17,1685,1686],{},"Document your data processing activities"," in a Record of Processing Activities (ROPA)",[14,1689,1690,1691,124],{},"For detailed guidance on compliance during development, see our ",[189,1692,1694],{"href":1693},"/blog/app-development-process-step-by-step-guide-2026/","app development process guide",[25,1696,1698],{"id":1697},"ab-testing-data-driven-feature-decisions","A/B Testing: Data-Driven Feature Decisions",[14,1700,1701],{},"A/B testing (also called split testing) is one of the most powerful applications of mobile analytics. Instead of debating whether a design change will improve conversion, you test it with real users and let the data decide.",[898,1703,1705],{"id":1704},"how-ab-testing-works-in-mobile-apps","How A/B Testing Works in Mobile Apps",[107,1707,1708,1711,1714,1717],{},[33,1709,1710],{},"Define a hypothesis (e.g., \"Changing the CTA button color from blue to green will increase sign-up conversions by 10%\")",[33,1712,1713],{},"Split your user base into a control group (sees the original) and a test group (sees the variation)",[33,1715,1716],{},"Run the test until you reach statistical significance (typically requires 1,000+ conversions per variant)",[33,1718,1719],{},"Analyze results and implement the winning variant",[898,1721,1723],{"id":1722},"popular-mobile-ab-testing-tools","Popular Mobile A/B Testing Tools",[133,1725,1726,1739],{},[136,1727,1728],{},[139,1729,1730,1733,1736],{},[142,1731,1732],{},"Tool",[142,1734,1735],{},"Best For",[142,1737,1738],{},"Pricing",[155,1740,1741,1752,1763,1774,1785],{},[139,1742,1743,1746,1749],{},[160,1744,1745],{},"Firebase Remote Config",[160,1747,1748],{},"Flutter/Firebase apps",[160,1750,1751],{},"Free",[139,1753,1754,1757,1760],{},[160,1755,1756],{},"Optimizely",[160,1758,1759],{},"Enterprise experimentation",[160,1761,1762],{},"Custom pricing",[139,1764,1765,1768,1771],{},[160,1766,1767],{},"LaunchDarkly",[160,1769,1770],{},"Feature flagging + testing",[160,1772,1773],{},"From $10/month",[139,1775,1776,1779,1782],{},[160,1777,1778],{},"Mixpanel Experiments",[160,1780,1781],{},"Product-led experimentation",[160,1783,1784],{},"Included in Growth plan",[139,1786,1787,1790,1793],{},[160,1788,1789],{},"Amplitude Experiment",[160,1791,1792],{},"Enterprise-scale testing",[160,1794,1784],{},[898,1796,1798],{"id":1797},"ab-testing-best-practices","A/B Testing Best Practices",[30,1800,1801,1807,1813,1819,1825],{},[33,1802,1803,1806],{},[17,1804,1805],{},"Test one variable at a time"," to isolate the impact",[33,1808,1809,1812],{},[17,1810,1811],{},"Run tests for at least 2 full business cycles"," (typically 2-4 weeks) to account for day-of-week effects",[33,1814,1815,1818],{},[17,1816,1817],{},"Ensure adequate sample size"," — use a statistical significance calculator before starting",[33,1820,1821,1824],{},[17,1822,1823],{},"Pre-register your hypothesis"," to avoid post-hoc rationalization",[33,1826,1827,1830],{},[17,1828,1829],{},"Consider long-term metrics,"," not just immediate conversion. A change that boosts sign-ups might hurt Day 30 retention.",[14,1832,1833],{},"Many tests end without a clear winner, so you need a culture of rapid experimentation: the more tests you run, the more winning insights you uncover.",[25,1835,1837],{"id":1836},"cohort-analysis-understanding-user-behavior-over-time","Cohort Analysis: Understanding User Behavior Over Time",[14,1839,1840],{},"Cohort analysis groups users by a shared characteristic (typically their sign-up date) and tracks their behavior over time. This is one of the most valuable analytical techniques for understanding retention and engagement patterns.",[898,1842,1844],{"id":1843},"types-of-cohort-analysis","Types of Cohort Analysis",[14,1846,1847,1850],{},[17,1848,1849],{},"Acquisition cohorts"," group users by when they first used your app. This reveals whether recent product changes have improved the new user experience.",[14,1852,1853,1856],{},[17,1854,1855],{},"Behavioral cohorts"," group users by actions they have taken. For example, you might compare users who completed onboarding within 24 hours versus those who did not.",[14,1858,1859,1862],{},[17,1860,1861],{},"Revenue cohorts"," group users by their spending patterns, helping identify which user segments generate the most lifetime value.",[898,1864,1866],{"id":1865},"how-to-read-a-cohort-retention-table","How to Read a Cohort Retention Table",[14,1868,1869],{},"A cohort retention table shows the percentage of users from each cohort who return in subsequent weeks or months. A healthy app shows:",[30,1871,1872,1878,1884],{},[33,1873,1874,1877],{},[17,1875,1876],{},"Flattening retention curves:"," Retention stabilizes after an initial drop-off period",[33,1879,1880,1883],{},[17,1881,1882],{},"Improving cohorts over time:"," Newer cohorts retain better than older ones (indicating product improvements are working)",[33,1885,1886,1889],{},[17,1887,1888],{},"Clear \"aha moment\" signals:"," Cohorts that complete a key action early (e.g., add 3 friends, complete first workout) show significantly higher long-term retention",[25,1891,1893],{"id":1892},"building-a-data-driven-analytics-culture","Building a Data-Driven Analytics Culture",[14,1895,1896],{},"Having the right tools and tracking in place is only half the battle. The other half is building a team culture that actually uses data to drive decisions.",[898,1898,1900],{"id":1899},"weekly-metrics-reviews","Weekly Metrics Reviews",[14,1902,1903],{},"Establish a weekly cadence where your team reviews key metrics:",[30,1905,1906,1909,1912,1915,1918,1921],{},[33,1907,1908],{},"DAU/MAU and stickiness trends",[33,1910,1911],{},"Retention curves for recent cohorts",[33,1913,1914],{},"Funnel conversion rates",[33,1916,1917],{},"Revenue metrics (ARPU, LTV)",[33,1919,1920],{},"Active A/B test results",[33,1922,1923],{},"Crash rate and performance metrics",[898,1925,1927],{"id":1926},"democratize-data-access","Democratize Data Access",[14,1929,1930],{},"Make analytics dashboards accessible to everyone on the team — not just product managers and analysts. When engineers, designers, and marketers can see the impact of their work in real-time, it creates a powerful feedback loop.",[898,1932,1934],{"id":1933},"avoid-common-analytics-pitfalls","Avoid Common Analytics Pitfalls",[107,1936,1937,1943,1949,1955,1961],{},[33,1938,1939,1942],{},[17,1940,1941],{},"Tracking everything and analyzing nothing."," More events do not equal better insights. Focus on metrics that drive decisions.",[33,1944,1945,1948],{},[17,1946,1947],{},"Confusing correlation with causation."," A metric going up after a release does not prove the release caused the increase.",[33,1950,1951,1954],{},[17,1952,1953],{},"Ignoring statistical significance."," Making decisions based on small sample sizes leads to false conclusions.",[33,1956,1957,1960],{},[17,1958,1959],{},"Only looking at averages."," Averages hide important distribution patterns. Always segment your data.",[33,1962,1963,1966],{},[17,1964,1965],{},"Neglecting qualitative research."," Analytics tells you what users do, not why. Combine quantitative analytics with user interviews and session recordings.",[25,1968,1970],{"id":1969},"frequently-asked-questions","Frequently Asked Questions",[898,1972,1974],{"id":1973},"what-is-the-best-free-mobile-analytics-tool","What is the best free mobile analytics tool?",[14,1976,1977,1980],{},[17,1978,1979],{},"Firebase Analytics"," is the best free option for most mobile apps. It provides unlimited event logging, automatic crash reporting through Crashlytics, audience segmentation, and integration with Google's advertising ecosystem. For Flutter apps in particular, Firebase is the natural choice due to its first-party support through FlutterFire packages.",[898,1982,1984],{"id":1983},"how-many-analytics-events-should-i-track","How many analytics events should I track?",[14,1986,1987,1988,1991],{},"Start with ",[17,1989,1990],{},"15-25 core events"," that map directly to your key user journeys and business objectives. You can expand from there based on specific questions that arise.",[898,1993,1995],{"id":1994},"how-do-i-track-analytics-without-violating-gdpr","How do I track analytics without violating GDPR?",[14,1997,1998],{},"Implement a Consent Management Platform to collect user consent before enabling analytics tracking. Use anonymization techniques (IP truncation, user ID hashing), configure analytics tools to respect consent preferences, and set appropriate data retention periods. Firebase and Mixpanel both offer consent-mode configurations that adjust data collection based on user preferences.",[898,2000,2002],{"id":2001},"can-i-use-multiple-analytics-tools-simultaneously","Can I use multiple analytics tools simultaneously?",[14,2004,2005],{},"Yes, and many apps do. A common combination is Firebase Analytics for basic metrics and crash reporting plus Mixpanel or Amplitude for deep product analytics plus AppsFlyer or Adjust for marketing attribution. The key is to maintain a single source of truth for each metric type and avoid duplication.",[898,2007,2009],{"id":2008},"how-do-i-measure-the-roi-of-my-analytics-investment","How do I measure the ROI of my analytics investment?",[14,2011,2012],{},"Track the revenue impact of changes made based on analytics insights.",[25,2014,2016],{"id":2015},"next-steps","Next Steps",[14,2018,2019],{},"Implementing a robust analytics strategy is essential for any mobile app that aims to grow and retain users in 2026. Whether you are building a new app or optimizing an existing one, start with the fundamentals — define your key metrics, choose the right tools, ensure privacy compliance, and build a culture of data-driven decision-making.",[14,2021,2022,2025,2026,2030],{},[17,2023,2024],{},"Need help implementing analytics in your mobile app?"," ",[189,2027,2029],{"href":2028},"/service/consultations/","Book a consultation"," ($100 per 15 minutes, paid upfront) with our team at App369. We will review your current analytics setup, identify gaps, and create a measurement plan tailored to your business objectives.",[14,2032,2033,2034,2037,2038,124],{},"For more on the app development process, read our ",[189,2035,2036],{"href":1693},"step-by-step guide to app development"," or explore ",[189,2039,2041],{"href":2040},"/blog/how-long-does-it-take-to-develop-an-app-timeline-guide/","how long it takes to develop an app",{"title":357,"searchDepth":371,"depth":371,"links":2043},[2044,2045,2051,2057,2064,2070,2075,2079,2084,2091],{"id":856,"depth":371,"text":857},{"id":892,"depth":371,"text":893,"children":2046},[2047,2048,2049,2050],{"id":900,"depth":381,"text":901},{"id":1012,"depth":381,"text":1013},{"id":1031,"depth":381,"text":1032},{"id":1059,"depth":381,"text":1060},{"id":1084,"depth":371,"text":1085,"children":2052},[2053,2054,2055,2056],{"id":1091,"depth":381,"text":1092},{"id":1149,"depth":381,"text":1150},{"id":1193,"depth":381,"text":1194},{"id":1237,"depth":381,"text":1238},{"id":1268,"depth":371,"text":1269,"children":2058},[2059,2060,2061,2062,2063],{"id":1275,"depth":381,"text":1276},{"id":1347,"depth":381,"text":1348},{"id":1416,"depth":381,"text":1417},{"id":1475,"depth":381,"text":1476},{"id":1505,"depth":381,"text":1506},{"id":1529,"depth":371,"text":1530,"children":2065},[2066,2067,2068,2069],{"id":1536,"depth":381,"text":1537},{"id":1575,"depth":381,"text":1576},{"id":1614,"depth":381,"text":1615},{"id":1644,"depth":381,"text":1645},{"id":1697,"depth":371,"text":1698,"children":2071},[2072,2073,2074],{"id":1704,"depth":381,"text":1705},{"id":1722,"depth":381,"text":1723},{"id":1797,"depth":381,"text":1798},{"id":1836,"depth":371,"text":1837,"children":2076},[2077,2078],{"id":1843,"depth":381,"text":1844},{"id":1865,"depth":381,"text":1866},{"id":1892,"depth":371,"text":1893,"children":2080},[2081,2082,2083],{"id":1899,"depth":381,"text":1900},{"id":1926,"depth":381,"text":1927},{"id":1933,"depth":381,"text":1934},{"id":1969,"depth":371,"text":1970,"children":2085},[2086,2087,2088,2089,2090],{"id":1973,"depth":381,"text":1974},{"id":1983,"depth":381,"text":1984},{"id":1994,"depth":381,"text":1995},{"id":2001,"depth":381,"text":2002},{"id":2008,"depth":381,"text":2009},{"id":2015,"depth":371,"text":2016},"App Development","2026-02-18T10:00:00.000Z","Master mobile app analytics in 2026. Learn the key metrics, top tools like Firebase and Mixpanel, implementation strategies, and privacy compliance tips.","Global",{"readingTimeStats":2097},{"text":2098,"minutes":2099,"time":2100,"words":2101},"13 min read",12.92,775200,2584,"/blog/mobile-app-analytics-tracking-guide-2026","14 min",{"title":833,"description":2094},"blog/mobile-app-analytics-tracking-guide-2026","mobile app analytics, app tracking, Firebase Analytics, Mixpanel, app metrics, user retention tracking, A/B testing mobile apps, GDPR app compliance, app development analytics 2026","2026-10-12T12:00:00.000Z","https://app369.com/blog/mobile-app-analytics-tracking-guide-2026/","FSKvTUgP8N9CbcV0aLSV_UBm_BxUx9PqXcK_aV1YM7w",{"id":2111,"title":2112,"author":2113,"body":2115,"category":738,"createdAt":3472,"description":3473,"disclosure":741,"extension":742,"faq":741,"howTo":741,"location":759,"meta":3474,"navigation":766,"ogImage":741,"path":3480,"primaryKeyword":741,"readingTime":3481,"related":3482,"seo":3486,"sources":741,"stem":3487,"tags":3488,"takeaways":741,"updatedAt":2107,"url":3489,"__hash__":3490},"blog/blog/firebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":2114},{"twitter":9},{"type":11,"value":2116,"toc":3456},[2117,2129,2136,2140,2143,2152,2155,2175,2178,2182,2198,2201,2246,2249,2255,2293,2296,2309,2313,2322,2325,2460,2463,2525,2528,2532,2541,2544,2597,2600,2652,2661,2664,2688,2692,2695,2698,2801,2804,2882,2885,2889,2898,2906,2911,2933,2942,3054,3057,3100,3104,3107,3110,3116,3205,3211,3261,3267,3332,3335,3339,3344,3388,3399,3403,3407,3415,3419,3422,3426,3441,3445,3453],[14,2118,2119,2122,2123,2128],{},[17,2120,2121],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[189,2124,2127],{"href":2125,"rel":2126},"https://www.audityour.app/guides/firebase-security-rules-guide",[193],"AuditYourApp's Firebase security analysis",", roughly 30% of Firebase applications in production have overly permissive rules on at least one collection — meaning nearly one in three apps allows unauthorized reads, writes, or both. Firebase does not enforce any access restrictions by default beyond the initial test/production mode selection, so every permission must be explicitly defined by the developer.",[14,2130,2131,2132,2135],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[189,2133,852],{"href":2134},"/service/mobile-apps/"," uses to secure Firebase backends across production applications.",[25,2137,2139],{"id":2138},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,2141,2142],{},"Firebase Security Rules operate as a server-side access control layer that evaluates every read and write request before it reaches your database. Without properly configured rules, any user with your Firebase project's configuration (which is embedded in every client app and publicly visible) can read, modify, or delete data directly through the Firebase SDK or REST API.",[14,2144,2145,2146,2151],{},"According to ",[189,2147,2150],{"href":2148,"rel":2149},"https://firebase.google.com/docs/rules/",[193],"Firebase's official documentation",", Security Rules provide fine-grained authorization that cannot be bypassed from client code. Unlike client-side validation, which can be disabled by modifying the app, Security Rules execute on Firebase's servers and cannot be circumvented.",[14,2153,2154],{},"Rules apply to three Firebase services independently:",[30,2156,2157,2163,2169],{},[33,2158,2159,2162],{},[17,2160,2161],{},"Cloud Firestore"," — document-level read/write control with path-based matching",[33,2164,2165,2168],{},[17,2166,2167],{},"Realtime Database"," — node-level access with cascading permissions",[33,2170,2171,2174],{},[17,2172,2173],{},"Cloud Storage"," — file-level access based on path, metadata, and authentication state",[14,2176,2177],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[25,2179,2181],{"id":2180},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,2183,2184,2185,586,2188,2191,2192,2197],{},"Firebase's setup wizard offers two starting configurations: ",[17,2186,2187],{},"test mode",[17,2189,2190],{},"locked mode",". Test mode grants public read and write access to the entire database for 30 days. According to ",[189,2193,2196],{"href":2194,"rel":2195},"https://vibeappscanner.com/is-firebase-safe",[193],"VibeAppScanner's Firebase safety analysis",", deploying with test mode rules is the single most common Firebase security failure in production.",[14,2199,2200],{},"Test mode rules look like this:",[352,2202,2206],{"className":2203,"code":2204,"language":2205,"meta":357,"style":357},"language-javascript shiki shiki-themes github-light github-dark","rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n    }\n  }\n}\n","javascript",[46,2207,2208,2213,2218,2223,2228,2233,2238,2242],{"__ignoreMap":357},[361,2209,2210],{"class":363,"line":364},[361,2211,2212],{},"rules_version = '2';\n",[361,2214,2215],{"class":363,"line":371},[361,2216,2217],{},"service cloud.firestore {\n",[361,2219,2220],{"class":363,"line":381},[361,2221,2222],{},"  match /databases/{database}/documents {\n",[361,2224,2225],{"class":363,"line":390},[361,2226,2227],{},"    match /{document=**} {\n",[361,2229,2230],{"class":363,"line":396},[361,2231,2232],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[361,2234,2235],{"class":363,"line":412},[361,2236,2237],{},"    }\n",[361,2239,2240],{"class":363,"line":433},[361,2241,448],{},[361,2243,2244],{"class":363,"line":439},[361,2245,454],{},[14,2247,2248],{},"The timestamp creates an illusion of safety — after the date passes, all access is blocked, breaking the app entirely. Before the date, anyone on the internet can read and overwrite every document. Both outcomes are unacceptable in production.",[14,2250,2251,2252,2254],{},"The correct starting point is ",[17,2253,2190],{},", where all access is denied by default:",[352,2256,2258],{"className":2203,"code":2257,"language":2205,"meta":357,"style":357},"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if false;\n    }\n  }\n}\n",[46,2259,2260,2264,2268,2272,2276,2281,2285,2289],{"__ignoreMap":357},[361,2261,2262],{"class":363,"line":364},[361,2263,2212],{},[361,2265,2266],{"class":363,"line":371},[361,2267,2217],{},[361,2269,2270],{"class":363,"line":381},[361,2271,2222],{},[361,2273,2274],{"class":363,"line":390},[361,2275,2227],{},[361,2277,2278],{"class":363,"line":396},[361,2279,2280],{},"      allow read, write: if false;\n",[361,2282,2283],{"class":363,"line":412},[361,2284,2237],{},[361,2286,2287],{"class":363,"line":433},[361,2288,448],{},[361,2290,2291],{"class":363,"line":439},[361,2292,454],{},[14,2294,2295],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,2297,2298,2299,2303,2304,2308],{},"AI app builders often leave test-mode rules in place instead of a locked baseline like this one. If your Lovable, Bolt or Cursor app has open data, sign-in loops or failing deploys, the ",[189,2300,2302],{"href":2301},"/fixed-price/ai-app-rescue/","AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[189,2305,2307],{"href":2306},"/tools/supabase-oct-30-checker/","Supabase October 30 grants checker"," lists the tables that will need explicit grants after that date.",[25,2310,2312],{"id":2311},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,2314,2315,2316,2321],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[189,2317,2320],{"href":2318,"rel":2319},"https://firebase.google.com/docs/firestore/security/get-started",[193],"Firebase's Firestore security documentation",", if several rules match a request, access is allowed when any one of them allows it, so a broad rule can't be narrowed by a more specific one. Keep each allow condition as tight as it can be.",[14,2323,2324],{},"Here is a production-ready pattern for a user-owned data collection:",[352,2326,2328],{"className":2203,"code":2327,"language":2205,"meta":357,"style":357},"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n\n    // Users can only read and write their own profile\n    match /users/{userId} {\n      allow read, write: if request.auth != null && request.auth.uid == userId;\n    }\n\n    // Posts are publicly readable, but only the author can write\n    match /posts/{postId} {\n      allow read: if true;\n      allow create: if request.auth != null\n                    && request.resource.data.authorId == request.auth.uid;\n      allow update, delete: if request.auth != null\n                            && resource.data.authorId == request.auth.uid;\n    }\n\n    // Admin-only collection\n    match /adminConfig/{docId} {\n      allow read, write: if request.auth != null\n                         && request.auth.token.admin == true;\n    }\n  }\n}\n",[46,2329,2330,2334,2338,2342,2347,2352,2357,2362,2366,2370,2375,2381,2387,2393,2399,2405,2411,2416,2421,2427,2433,2439,2445,2450,2455],{"__ignoreMap":357},[361,2331,2332],{"class":363,"line":364},[361,2333,2212],{},[361,2335,2336],{"class":363,"line":371},[361,2337,2217],{},[361,2339,2340],{"class":363,"line":381},[361,2341,2222],{},[361,2343,2344],{"class":363,"line":390},[361,2345,2346],{"emptyLinePlaceholder":766},"\n",[361,2348,2349],{"class":363,"line":396},[361,2350,2351],{},"    // Users can only read and write their own profile\n",[361,2353,2354],{"class":363,"line":412},[361,2355,2356],{},"    match /users/{userId} {\n",[361,2358,2359],{"class":363,"line":433},[361,2360,2361],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[361,2363,2364],{"class":363,"line":439},[361,2365,2237],{},[361,2367,2368],{"class":363,"line":445},[361,2369,2346],{"emptyLinePlaceholder":766},[361,2371,2372],{"class":363,"line":451},[361,2373,2374],{},"    // Posts are publicly readable, but only the author can write\n",[361,2376,2378],{"class":363,"line":2377},11,[361,2379,2380],{},"    match /posts/{postId} {\n",[361,2382,2384],{"class":363,"line":2383},12,[361,2385,2386],{},"      allow read: if true;\n",[361,2388,2390],{"class":363,"line":2389},13,[361,2391,2392],{},"      allow create: if request.auth != null\n",[361,2394,2396],{"class":363,"line":2395},14,[361,2397,2398],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[361,2400,2402],{"class":363,"line":2401},15,[361,2403,2404],{},"      allow update, delete: if request.auth != null\n",[361,2406,2408],{"class":363,"line":2407},16,[361,2409,2410],{},"                            && resource.data.authorId == request.auth.uid;\n",[361,2412,2414],{"class":363,"line":2413},17,[361,2415,2237],{},[361,2417,2419],{"class":363,"line":2418},18,[361,2420,2346],{"emptyLinePlaceholder":766},[361,2422,2424],{"class":363,"line":2423},19,[361,2425,2426],{},"    // Admin-only collection\n",[361,2428,2430],{"class":363,"line":2429},20,[361,2431,2432],{},"    match /adminConfig/{docId} {\n",[361,2434,2436],{"class":363,"line":2435},21,[361,2437,2438],{},"      allow read, write: if request.auth != null\n",[361,2440,2442],{"class":363,"line":2441},22,[361,2443,2444],{},"                         && request.auth.token.admin == true;\n",[361,2446,2448],{"class":363,"line":2447},23,[361,2449,2237],{},[361,2451,2453],{"class":363,"line":2452},24,[361,2454,448],{},[361,2456,2458],{"class":363,"line":2457},25,[361,2459,454],{},[14,2461,2462],{},"Key principles for effective rules:",[30,2464,2465,2489,2498,2511],{},[33,2466,2467,2470,2471,586,2474,2477,2478,1319,2481,2484,2485,2488],{},[17,2468,2469],{},"Separate read and write permissions."," Use ",[46,2472,2473],{},"allow read",[46,2475,2476],{},"allow write"," independently. Break write into ",[46,2479,2480],{},"create",[46,2482,2483],{},"update",", and ",[46,2486,2487],{},"delete"," when different logic applies.",[33,2490,2491,2497],{},[17,2492,2493,2494,124],{},"Always check ",[46,2495,2496],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[33,2499,2500,586,2507,2510],{},[17,2501,2502,2503,2506],{},"Use ",[46,2504,2505],{},"resource.data"," for existing document fields",[46,2508,2509],{},"request.resource.data"," for incoming data in create/update operations.",[33,2512,2513,2516,2517,2520,2521,2524],{},[17,2514,2515],{},"Avoid wildcards at the root level."," The pattern ",[46,2518,2519],{},"match /{document=**}"," applies to every document in the database. Use it only with ",[46,2522,2523],{},"if false"," as a catch-all deny rule.",[14,2526,2527],{},"Structure your database with security in mind. Organize collections by permission boundary — keep admin-only data in separate collections rather than mixing it with user-readable documents.",[25,2529,2531],{"id":2530},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,2533,2534,2535,2540],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[189,2536,2539],{"href":2537,"rel":2538},"https://firebase.google.com/docs/auth/admin/custom-claims",[193],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,2542,2543],{},"Set custom claims from a Cloud Function or admin server:",[352,2545,2547],{"className":2203,"code":2546,"language":2205,"meta":357,"style":357},"const admin = require('firebase-admin');\n\n// Assign admin role\nawait admin.auth().setCustomUserClaims(userId, { admin: true });\n\n// Assign editor role with department\nawait admin.auth().setCustomUserClaims(userId, {\n  role: 'editor',\n  department: 'marketing'\n});\n",[46,2548,2549,2554,2558,2563,2568,2572,2577,2582,2587,2592],{"__ignoreMap":357},[361,2550,2551],{"class":363,"line":364},[361,2552,2553],{},"const admin = require('firebase-admin');\n",[361,2555,2556],{"class":363,"line":371},[361,2557,2346],{"emptyLinePlaceholder":766},[361,2559,2560],{"class":363,"line":381},[361,2561,2562],{},"// Assign admin role\n",[361,2564,2565],{"class":363,"line":390},[361,2566,2567],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[361,2569,2570],{"class":363,"line":396},[361,2571,2346],{"emptyLinePlaceholder":766},[361,2573,2574],{"class":363,"line":412},[361,2575,2576],{},"// Assign editor role with department\n",[361,2578,2579],{"class":363,"line":433},[361,2580,2581],{},"await admin.auth().setCustomUserClaims(userId, {\n",[361,2583,2584],{"class":363,"line":439},[361,2585,2586],{},"  role: 'editor',\n",[361,2588,2589],{"class":363,"line":445},[361,2590,2591],{},"  department: 'marketing'\n",[361,2593,2594],{"class":363,"line":451},[361,2595,2596],{},"});\n",[14,2598,2599],{},"Reference claims in Security Rules:",[352,2601,2603],{"className":2203,"code":2602,"language":2205,"meta":357,"style":357},"match /content/{docId} {\n  // Admins have full access\n  allow read, write: if request.auth.token.admin == true;\n\n  // Editors can read and update, but not delete\n  allow read, update: if request.auth.token.role == 'editor';\n\n  // All authenticated users can read\n  allow read: if request.auth != null;\n}\n",[46,2604,2605,2610,2615,2620,2624,2629,2634,2638,2643,2648],{"__ignoreMap":357},[361,2606,2607],{"class":363,"line":364},[361,2608,2609],{},"match /content/{docId} {\n",[361,2611,2612],{"class":363,"line":371},[361,2613,2614],{},"  // Admins have full access\n",[361,2616,2617],{"class":363,"line":381},[361,2618,2619],{},"  allow read, write: if request.auth.token.admin == true;\n",[361,2621,2622],{"class":363,"line":390},[361,2623,2346],{"emptyLinePlaceholder":766},[361,2625,2626],{"class":363,"line":396},[361,2627,2628],{},"  // Editors can read and update, but not delete\n",[361,2630,2631],{"class":363,"line":412},[361,2632,2633],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[361,2635,2636],{"class":363,"line":433},[361,2637,2346],{"emptyLinePlaceholder":766},[361,2639,2640],{"class":363,"line":439},[361,2641,2642],{},"  // All authenticated users can read\n",[361,2644,2645],{"class":363,"line":445},[361,2646,2647],{},"  allow read: if request.auth != null;\n",[361,2649,2650],{"class":363,"line":451},[361,2651,454],{},[14,2653,2654,2655,2660],{},"As noted in ",[189,2656,2659],{"href":2657,"rel":2658},"https://medium.com/@NadunKulatunge/advanced-use-cases-and-best-practices-of-firebase-security-rules-cb5d3339d8a7",[193],"Nadun Kulatunge's advanced Firebase rules guide on Medium",", custom claims should be limited to authorization data only — role identifiers, permission flags, and department codes. Do not store user preferences or profile data in claims, as the token has a size limit and claims are only refreshed when the token is re-issued (typically every hour).",[14,2662,2663],{},"Implementation guidelines:",[30,2665,2666,2672,2682],{},[33,2667,2668,2671],{},[17,2669,2670],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[33,2673,2674,2677,2678,2681],{},[17,2675,2676],{},"Force token refresh after claim changes."," Call ",[46,2679,2680],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[33,2683,2684,2687],{},[17,2685,2686],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[25,2689,2691],{"id":2690},"data-validation-in-security-rules","Data Validation in Security Rules",[14,2693,2694],{},"Security Rules can enforce data structure requirements directly, preventing malformed data from reaching the database regardless of what the client sends. This is the first line of defense against both bugs and deliberate manipulation.",[14,2696,2697],{},"Validation patterns for common requirements:",[352,2699,2701],{"className":2203,"code":2700,"language":2205,"meta":357,"style":357},"match /users/{userId} {\n  allow create: if request.auth.uid == userId\n    // Required fields must exist\n    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n    // Name must be a string between 2 and 100 characters\n    && request.resource.data.name is string\n    && request.resource.data.name.size() >= 2\n    && request.resource.data.name.size() \u003C= 100\n    // Email must be a string and match the auth email\n    && request.resource.data.email is string\n    && request.resource.data.email == request.auth.token.email\n    // createdAt must be a timestamp\n    && request.resource.data.createdAt is timestamp\n    // No unexpected fields allowed\n    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n\n  allow update: if request.auth.uid == userId\n    // Prevent changing the email field\n    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n}\n",[46,2702,2703,2708,2713,2718,2723,2728,2733,2738,2743,2748,2753,2758,2763,2768,2773,2778,2782,2787,2792,2797],{"__ignoreMap":357},[361,2704,2705],{"class":363,"line":364},[361,2706,2707],{},"match /users/{userId} {\n",[361,2709,2710],{"class":363,"line":371},[361,2711,2712],{},"  allow create: if request.auth.uid == userId\n",[361,2714,2715],{"class":363,"line":381},[361,2716,2717],{},"    // Required fields must exist\n",[361,2719,2720],{"class":363,"line":390},[361,2721,2722],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[361,2724,2725],{"class":363,"line":396},[361,2726,2727],{},"    // Name must be a string between 2 and 100 characters\n",[361,2729,2730],{"class":363,"line":412},[361,2731,2732],{},"    && request.resource.data.name is string\n",[361,2734,2735],{"class":363,"line":433},[361,2736,2737],{},"    && request.resource.data.name.size() >= 2\n",[361,2739,2740],{"class":363,"line":439},[361,2741,2742],{},"    && request.resource.data.name.size() \u003C= 100\n",[361,2744,2745],{"class":363,"line":445},[361,2746,2747],{},"    // Email must be a string and match the auth email\n",[361,2749,2750],{"class":363,"line":451},[361,2751,2752],{},"    && request.resource.data.email is string\n",[361,2754,2755],{"class":363,"line":2377},[361,2756,2757],{},"    && request.resource.data.email == request.auth.token.email\n",[361,2759,2760],{"class":363,"line":2383},[361,2761,2762],{},"    // createdAt must be a timestamp\n",[361,2764,2765],{"class":363,"line":2389},[361,2766,2767],{},"    && request.resource.data.createdAt is timestamp\n",[361,2769,2770],{"class":363,"line":2395},[361,2771,2772],{},"    // No unexpected fields allowed\n",[361,2774,2775],{"class":363,"line":2401},[361,2776,2777],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[361,2779,2780],{"class":363,"line":2407},[361,2781,2346],{"emptyLinePlaceholder":766},[361,2783,2784],{"class":363,"line":2413},[361,2785,2786],{},"  allow update: if request.auth.uid == userId\n",[361,2788,2789],{"class":363,"line":2418},[361,2790,2791],{},"    // Prevent changing the email field\n",[361,2793,2794],{"class":363,"line":2423},[361,2795,2796],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[361,2798,2799],{"class":363,"line":2429},[361,2800,454],{},[14,2802,2803],{},"Validation capabilities in Firestore rules include:",[30,2805,2806,2834,2844,2858,2867,2873],{},[33,2807,2808,2811,2812,1319,2815,1319,2818,1319,2821,1319,2824,1319,2827,2830,2831],{},[17,2809,2810],{},"Type checking"," — verify that fields are ",[46,2813,2814],{},"string",[46,2816,2817],{},"int",[46,2819,2820],{},"float",[46,2822,2823],{},"bool",[46,2825,2826],{},"timestamp",[46,2828,2829],{},"list",", or ",[46,2832,2833],{},"map",[33,2835,2836,2839,2840,2843],{},[17,2837,2838],{},"String length"," — use ",[46,2841,2842],{},".size()"," to enforce minimum and maximum lengths",[33,2845,2846,2849,2850,2853,2854,2857],{},[17,2847,2848],{},"Field existence"," — ",[46,2851,2852],{},"hasAll()"," for required fields, ",[46,2855,2856],{},"hasOnly()"," to prevent extra fields",[33,2859,2860,2839,2863,2866],{},[17,2861,2862],{},"Field immutability",[46,2864,2865],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[33,2868,2869,2872],{},[17,2870,2871],{},"Numeric ranges"," — enforce minimum and maximum values for numbers",[33,2874,2875,2878,2879,2881],{},[17,2876,2877],{},"List constraints"," — check ",[46,2880,2842],{}," on lists to limit array lengths",[14,2883,2884],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[25,2886,2888],{"id":2887},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,2890,2891,2892,2897],{},"Firebase provides a rules simulator in the Firebase Console and a local emulator suite for testing rules before deployment. Deploying untested rules is a common way for mistakes to reach production; ",[189,2893,2896],{"href":2894,"rel":2895},"https://firebase.google.com/docs/rules/unit-tests",[193],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,2899,2900,2901,2905],{},"For a quick first pass, paste your rules into App369's free ",[189,2902,2904],{"href":2903},"/tools/firebase-rules-checker/","Firebase Security Rules checker",". It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.",[14,2907,2908],{},[17,2909,2910],{},"Use the Firebase Emulator Suite for local testing:",[352,2912,2916],{"className":2913,"code":2914,"language":2915,"meta":357,"style":357},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[46,2917,2918],{"__ignoreMap":357},[361,2919,2920,2924,2927,2930],{"class":363,"line":364},[361,2921,2923],{"class":2922},"sScJk","firebase",[361,2925,2926],{"class":405}," emulators:start",[361,2928,2929],{"class":374}," --only",[361,2931,2932],{"class":405}," firestore\n",[14,2934,2935],{},[17,2936,2937,2938,2941],{},"Write unit tests with the ",[46,2939,2940],{},"@firebase/rules-unit-testing"," package:",[352,2943,2945],{"className":2203,"code":2944,"language":2205,"meta":357,"style":357},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase/rules-unit-testing');\n\nconst testEnv = await initializeTestEnvironment({\n  projectId: 'your-project-id',\n  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n});\n\n// Test: authenticated user can read their own document\nconst userContext = testEnv.authenticatedContext('user123');\nawait assertSucceeds(\n  userContext.firestore().collection('users').doc('user123').get()\n);\n\n// Test: user cannot read another user's document\nawait assertFails(\n  userContext.firestore().collection('users').doc('other-user').get()\n);\n\n// Test: unauthenticated user cannot write\nconst unauthedContext = testEnv.unauthenticatedContext();\nawait assertFails(\n  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n);\n",[46,2946,2947,2952,2956,2961,2966,2971,2975,2979,2984,2989,2994,2999,3004,3008,3013,3018,3023,3027,3031,3036,3041,3045,3050],{"__ignoreMap":357},[361,2948,2949],{"class":363,"line":364},[361,2950,2951],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase/rules-unit-testing');\n",[361,2953,2954],{"class":363,"line":371},[361,2955,2346],{"emptyLinePlaceholder":766},[361,2957,2958],{"class":363,"line":381},[361,2959,2960],{},"const testEnv = await initializeTestEnvironment({\n",[361,2962,2963],{"class":363,"line":390},[361,2964,2965],{},"  projectId: 'your-project-id',\n",[361,2967,2968],{"class":363,"line":396},[361,2969,2970],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[361,2972,2973],{"class":363,"line":412},[361,2974,2596],{},[361,2976,2977],{"class":363,"line":433},[361,2978,2346],{"emptyLinePlaceholder":766},[361,2980,2981],{"class":363,"line":439},[361,2982,2983],{},"// Test: authenticated user can read their own document\n",[361,2985,2986],{"class":363,"line":445},[361,2987,2988],{},"const userContext = testEnv.authenticatedContext('user123');\n",[361,2990,2991],{"class":363,"line":451},[361,2992,2993],{},"await assertSucceeds(\n",[361,2995,2996],{"class":363,"line":2377},[361,2997,2998],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[361,3000,3001],{"class":363,"line":2383},[361,3002,3003],{},");\n",[361,3005,3006],{"class":363,"line":2389},[361,3007,2346],{"emptyLinePlaceholder":766},[361,3009,3010],{"class":363,"line":2395},[361,3011,3012],{},"// Test: user cannot read another user's document\n",[361,3014,3015],{"class":363,"line":2401},[361,3016,3017],{},"await assertFails(\n",[361,3019,3020],{"class":363,"line":2407},[361,3021,3022],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[361,3024,3025],{"class":363,"line":2413},[361,3026,3003],{},[361,3028,3029],{"class":363,"line":2418},[361,3030,2346],{"emptyLinePlaceholder":766},[361,3032,3033],{"class":363,"line":2423},[361,3034,3035],{},"// Test: unauthenticated user cannot write\n",[361,3037,3038],{"class":363,"line":2429},[361,3039,3040],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[361,3042,3043],{"class":363,"line":2435},[361,3044,3017],{},[361,3046,3047],{"class":363,"line":2441},[361,3048,3049],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[361,3051,3052],{"class":363,"line":2447},[361,3053,3003],{},[14,3055,3056],{},"Deployment workflow:",[107,3058,3059,3074,3080,3088,3094],{},[33,3060,3061,3064,3065,1319,3068,2484,3071],{},[17,3062,3063],{},"Write rules locally"," in ",[46,3066,3067],{},"firestore.rules",[46,3069,3070],{},"database.rules.json",[46,3072,3073],{},"storage.rules",[33,3075,3076,3079],{},[17,3077,3078],{},"Test with the emulator"," — run the full test suite against the local emulator before every deployment",[33,3081,3082,3087],{},[17,3083,2502,3084],{},[46,3085,3086],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[33,3089,3090,3093],{},[17,3091,3092],{},"Version control your rules files"," — commit them to Git and review changes in pull requests",[33,3095,3096,3099],{},[17,3097,3098],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[25,3101,3103],{"id":3102},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,3105,3106],{},"Security Rules handle authorization and basic data validation, but complex business logic requires server-side enforcement through Cloud Functions. Cloud Functions execute in a trusted environment where the code cannot be inspected or modified by clients.",[14,3108,3109],{},"Use Cloud Functions as a second validation layer for:",[14,3111,3112,3115],{},[17,3113,3114],{},"Financial transactions"," — verify amounts, check account balances, and enforce rate limits server-side:",[352,3117,3119],{"className":2203,"code":3118,"language":2205,"meta":357,"style":357},"exports.processPayment = functions.https.onCall(async (data, context) => {\n  // Verify authentication\n  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n\n  // Server-side amount validation\n  if (data.amount \u003C= 0 || data.amount > 10000) {\n    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n  }\n\n  // Check user's balance from a trusted source\n  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n  if (userDoc.data().balance \u003C data.amount) {\n    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n  }\n\n  // Process the transaction with admin privileges\n  // ...\n});\n",[46,3120,3121,3126,3131,3136,3140,3145,3150,3155,3159,3163,3168,3173,3178,3183,3187,3191,3196,3201],{"__ignoreMap":357},[361,3122,3123],{"class":363,"line":364},[361,3124,3125],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[361,3127,3128],{"class":363,"line":371},[361,3129,3130],{},"  // Verify authentication\n",[361,3132,3133],{"class":363,"line":381},[361,3134,3135],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[361,3137,3138],{"class":363,"line":390},[361,3139,2346],{"emptyLinePlaceholder":766},[361,3141,3142],{"class":363,"line":396},[361,3143,3144],{},"  // Server-side amount validation\n",[361,3146,3147],{"class":363,"line":412},[361,3148,3149],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[361,3151,3152],{"class":363,"line":433},[361,3153,3154],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[361,3156,3157],{"class":363,"line":439},[361,3158,448],{},[361,3160,3161],{"class":363,"line":445},[361,3162,2346],{"emptyLinePlaceholder":766},[361,3164,3165],{"class":363,"line":451},[361,3166,3167],{},"  // Check user's balance from a trusted source\n",[361,3169,3170],{"class":363,"line":2377},[361,3171,3172],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[361,3174,3175],{"class":363,"line":2383},[361,3176,3177],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[361,3179,3180],{"class":363,"line":2389},[361,3181,3182],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[361,3184,3185],{"class":363,"line":2395},[361,3186,448],{},[361,3188,3189],{"class":363,"line":2401},[361,3190,2346],{"emptyLinePlaceholder":766},[361,3192,3193],{"class":363,"line":2407},[361,3194,3195],{},"  // Process the transaction with admin privileges\n",[361,3197,3198],{"class":363,"line":2413},[361,3199,3200],{},"  // ...\n",[361,3202,3203],{"class":363,"line":2418},[361,3204,2596],{},[14,3206,3207,3210],{},[17,3208,3209],{},"Data aggregation and denormalization"," — use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[352,3212,3214],{"className":2203,"code":3213,"language":2205,"meta":357,"style":357},"exports.onPostCreated = functions.firestore\n  .document('posts/{postId}')\n  .onCreate(async (snap, context) => {\n    // Increment the author's post count atomically\n    const authorId = snap.data().authorId;\n    await admin.firestore().collection('users').doc(authorId).update({\n      postCount: admin.firestore.FieldValue.increment(1)\n    });\n  });\n",[46,3215,3216,3221,3226,3231,3236,3241,3246,3251,3256],{"__ignoreMap":357},[361,3217,3218],{"class":363,"line":364},[361,3219,3220],{},"exports.onPostCreated = functions.firestore\n",[361,3222,3223],{"class":363,"line":371},[361,3224,3225],{},"  .document('posts/{postId}')\n",[361,3227,3228],{"class":363,"line":381},[361,3229,3230],{},"  .onCreate(async (snap, context) => {\n",[361,3232,3233],{"class":363,"line":390},[361,3234,3235],{},"    // Increment the author's post count atomically\n",[361,3237,3238],{"class":363,"line":396},[361,3239,3240],{},"    const authorId = snap.data().authorId;\n",[361,3242,3243],{"class":363,"line":412},[361,3244,3245],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[361,3247,3248],{"class":363,"line":433},[361,3249,3250],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[361,3252,3253],{"class":363,"line":439},[361,3254,3255],{},"    });\n",[361,3257,3258],{"class":363,"line":445},[361,3259,3260],{},"  });\n",[14,3262,3263,3266],{},[17,3264,3265],{},"Rate limiting"," — prevent abuse by tracking request frequency per user:",[352,3268,3270],{"className":2203,"code":3269,"language":2205,"meta":357,"style":357},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n  const userId = context.auth.uid;\n  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n\n  if (lastSubmission.exists) {\n    const lastTime = lastSubmission.data().timestamp.toMillis();\n    if (Date.now() - lastTime \u003C 60000) { // 1 minute cooldown\n      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n    }\n  }\n\n  // Process the submission...\n});\n",[46,3271,3272,3277,3282,3287,3291,3296,3301,3306,3311,3315,3319,3323,3328],{"__ignoreMap":357},[361,3273,3274],{"class":363,"line":364},[361,3275,3276],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[361,3278,3279],{"class":363,"line":371},[361,3280,3281],{},"  const userId = context.auth.uid;\n",[361,3283,3284],{"class":363,"line":381},[361,3285,3286],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[361,3288,3289],{"class":363,"line":390},[361,3290,2346],{"emptyLinePlaceholder":766},[361,3292,3293],{"class":363,"line":396},[361,3294,3295],{},"  if (lastSubmission.exists) {\n",[361,3297,3298],{"class":363,"line":412},[361,3299,3300],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[361,3302,3303],{"class":363,"line":433},[361,3304,3305],{},"    if (Date.now() - lastTime \u003C 60000) { // 1 minute cooldown\n",[361,3307,3308],{"class":363,"line":439},[361,3309,3310],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[361,3312,3313],{"class":363,"line":445},[361,3314,2237],{},[361,3316,3317],{"class":363,"line":451},[361,3318,448],{},[361,3320,3321],{"class":363,"line":2377},[361,3322,2346],{"emptyLinePlaceholder":766},[361,3324,3325],{"class":363,"line":2383},[361,3326,3327],{},"  // Process the submission...\n",[361,3329,3330],{"class":363,"line":2389},[361,3331,2596],{},[14,3333,3334],{},"Cloud Functions complement Security Rules — they do not replace them. Rules should still enforce basic authentication and authorization checks. Functions handle the logic that rules cannot express.",[25,3336,3338],{"id":3337},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,3340,3341,3343],{},[189,3342,852],{"href":1144}," implements Firebase security as a standard component of every project that uses the Firebase stack:",[30,3345,3346,3352,3358,3364,3370,3376],{},[33,3347,3348,3351],{},[17,3349,3350],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[33,3353,3354,3357],{},[17,3355,3356],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[33,3359,3360,3363],{},[17,3361,3362],{},"Comprehensive data validation"," in rules for every writable collection — type checking, field constraints, and immutability guards",[33,3365,3366,3369],{},[17,3367,3368],{},"Automated rules testing"," in CI/CD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[33,3371,3372,3375],{},[17,3373,3374],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[33,3377,3378,3381,3382,3387],{},[17,3379,3380],{},"Security audits"," against the ",[189,3383,3386],{"href":3384,"rel":3385},"https://owasp.org/www-project-mobile-top-10/",[193],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,3389,3390,3391,3394,3395,124],{},"App369 builds ",[189,3392,3393],{"href":1144},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[189,3396,3398],{"href":3397},"/blog/flutter-security-best-practices-2026/","Flutter security best practices",[25,3400,3402],{"id":3401},"faq","FAQ",[898,3404,3406],{"id":3405},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,3408,3409,3410,3414],{},"Any user on the internet can read, modify, and delete every document in your database. Test mode grants unrestricted public access using a time-based condition — before the expiry date, anyone with your project ID (which is public) has full access. According to ",[189,3411,3413],{"href":2125,"rel":3412},[193],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[898,3416,3418],{"id":3417},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,3420,3421],{},"Partially. Security Rules can enforce authentication, field-level authorization, type checking, string length constraints, and field immutability. They cannot perform cross-document queries, call external APIs, enforce complex business logic, or implement rate limiting. Use Cloud Functions for operations that exceed what rules can express, and use rules as the first layer of defense for every request.",[898,3423,3425],{"id":3424},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,3427,3428,3429,3432,3433,3436,3437,3440],{},"Use Firebase Authentication custom claims. Set ",[46,3430,3431],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[46,3434,3435],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[46,3438,3439],{},"users/{userId}.isAdmin"," for authorization — clients can modify their own documents if write rules are misconfigured. Custom claims are set server-side and cannot be modified by clients.",[898,3442,3444],{"id":3443},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,3446,3447,3448,3452],{},"Review and update rules whenever you add a new collection, modify a data schema, or change role definitions. Include rules files in version control and require pull request reviews for all changes. Run the Firebase Emulator test suite in CI before every deployment. At minimum, audit your rules quarterly against the current ",[189,3449,3451],{"href":2148,"rel":3450},[193],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[726,3454,3455],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":357,"searchDepth":371,"depth":371,"links":3457},[3458,3459,3460,3461,3462,3463,3464,3465,3466],{"id":2138,"depth":371,"text":2139},{"id":2180,"depth":371,"text":2181},{"id":2311,"depth":371,"text":2312},{"id":2530,"depth":371,"text":2531},{"id":2690,"depth":371,"text":2691},{"id":2887,"depth":371,"text":2888},{"id":3102,"depth":371,"text":3103},{"id":3337,"depth":371,"text":3338},{"id":3401,"depth":371,"text":3402,"children":3467},[3468,3469,3470,3471],{"id":3405,"depth":381,"text":3406},{"id":3417,"depth":381,"text":3418},{"id":3424,"depth":381,"text":3425},{"id":3443,"depth":381,"text":3444},"2026-03-07T10:00:00.000Z","Firebase security rules complete guide for 2026: Firestore rules, role-based access control, data validation, and Cloud Functions for server-side protection.",{"readingTimeStats":3475},{"text":3476,"minutes":3477,"time":3478,"words":3479},"11 min read",10.415,624900,2083,"/blog/firebase-security-guide-2026","11 min",[3483,3484,3485],"supabase-permission-denied-new-tables-october-30-2026","ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":2112,"description":3473},"blog/firebase-security-guide-2026","firebase security rules, firestore security, firebase authentication, cloud functions, role-based access control, firebase guide","https://app369.com/blog/firebase-security-guide-2026/","03zzXvqm83bKHIwJCQ0nwQhHoP5iUWwZrIqQ7AgyfgY",{"id":3492,"title":3493,"author":3494,"body":3496,"category":738,"createdAt":3885,"description":3886,"disclosure":741,"extension":742,"faq":741,"howTo":741,"location":759,"meta":3887,"navigation":766,"ogImage":741,"path":3893,"primaryKeyword":741,"readingTime":769,"related":741,"seo":3894,"sources":741,"stem":3895,"tags":3896,"takeaways":741,"updatedAt":3885,"url":3897,"__hash__":3898},"blog/blog/how-to-debug-a-flutter-firebase-bug-2026.md","How to Debug a Flutter and Firebase Bug: Find the Root Cause (2026)",{"name":7,"social":3495},{"twitter":9},{"type":11,"value":3497,"toc":3864},[3498,3504,3512,3516,3519,3533,3536,3540,3543,3628,3637,3641,3649,3653,3657,3694,3698,3701,3716,3720,3746,3750,3774,3778,3785,3789,3797,3801,3813,3815,3819,3827,3831,3837,3841,3850,3854,3857,3861],[14,3499,3500,3503],{},[17,3501,3502],{},"Most Flutter and Firebase bugs that take days to fix take days to find, not to fix."," Once you can reproduce the bug on demand and you know which layer it lives in (the app, Firebase's servers, or the build tooling), the fix is usually small. This guide is the order to work in, then the five problems that come up most.",[14,3505,3506,3507,3511],{},"It is the method behind App369's ",[189,3508,3510],{"href":3509},"/fixed-price/#fix-ticket","fixed-price Flutter & Firebase Fix Ticket",", and the one Simon uses on his own production Flutter and Firebase SaaS, Workspace369.",[25,3513,3515],{"id":3514},"step-1-write-down-exact-reproduction-steps","Step 1: Write down exact reproduction steps",[14,3517,3518],{},"Before reading any code, write the bug as steps someone else could follow:",[107,3520,3521,3524,3527,3530],{},[33,3522,3523],{},"Which build: the app version, build number, and whether it is debug, profile or release.",[33,3525,3526],{},"Which device and OS version, and whether it happens on web, iOS, Android or all three.",[33,3528,3529],{},"Which account or data: a new user, an existing user, a specific record.",[33,3531,3532],{},"The exact taps, and what you expected versus what happened.",[14,3534,3535],{},"If it only happens \"sometimes\", note what the failing cases have in common: a slow network, a large account, the app coming back from the background. A bug you can't reproduce can't be verified as fixed.",[25,3537,3539],{"id":3538},"step-2-find-out-which-layer-is-failing","Step 2: Find out which layer is failing",[14,3541,3542],{},"Each layer has its own log, and the error is usually already in one of them:",[133,3544,3545,3555],{},[136,3546,3547],{},[139,3548,3549,3552],{},[142,3550,3551],{},"Layer",[142,3553,3554],{},"Where to look",[155,3556,3557,3575,3588,3599,3613],{},[139,3558,3559,3562],{},[160,3560,3561],{},"Flutter app",[160,3563,3564,3565,3568,3569,3574],{},"The ",[46,3566,3567],{},"flutter run"," console, and ",[189,3570,3573],{"href":3571,"rel":3572},"https://docs.flutter.dev/tools/devtools",[193],"Flutter DevTools"," for widgets, network and memory",[139,3576,3577,3580],{},[160,3578,3579],{},"Release builds in users' hands",[160,3581,3582,3587],{},[189,3583,3586],{"href":3584,"rel":3585},"https://firebase.google.com/docs/crashlytics",[193],"Firebase Crashlytics"," for crashes and non-fatal errors you record",[139,3589,3590,3593],{},[160,3591,3592],{},"Cloud Functions",[160,3594,3595,3596],{},"Logs Explorer in the Google Cloud console, or ",[46,3597,3598],{},"firebase functions:log",[139,3600,3601,3604],{},[160,3602,3603],{},"Firestore and Storage",[160,3605,3606,3607,332,3610],{},"The exact error code your app receives, such as ",[46,3608,3609],{},"permission-denied",[46,3611,3612],{},"failed-precondition",[139,3614,3615,3618],{},[160,3616,3617],{},"Build tooling",[160,3619,3620,3621,574,3624,3627],{},"The full output of ",[46,3622,3623],{},"flutter build",[46,3625,3626],{},"-v",", plus Xcode or Gradle's own logs",[14,3629,3630,3631,3633,3634,3636],{},"A Firestore ",[46,3632,3609],{}," error means Security Rules refused the request; it is not an app crash. A ",[46,3635,3612],{}," error on a query often includes a link to create the missing index. Read the full message before changing code.",[25,3638,3640],{"id":3639},"step-3-isolate-it-with-the-emulator-suite","Step 3: Isolate it with the Emulator Suite",[14,3642,3564,3643,3648],{},[189,3644,3647],{"href":3645,"rel":3646},"https://firebase.google.com/docs/emulator-suite",[193],"Firebase Local Emulator Suite"," runs Auth, Firestore, Functions and Storage on your machine. Point a debug build at the emulators, load the data that triggers the bug, and you can repeat the failing steps as often as you need without touching production data. Security Rules problems in particular are easier to see in the emulator UI, which shows which rule allowed or denied each request.",[25,3650,3652],{"id":3651},"the-five-issues-that-come-up-most","The five issues that come up most",[898,3654,3656],{"id":3655},"build-failures","Build failures",[30,3658,3659,3666,3684,3691],{},[33,3660,3661,3662,3665],{},"Run ",[46,3663,3664],{},"flutter doctor -v"," first. It catches missing toolchains, licenses and Xcode or Android SDK problems.",[33,3667,3668,3669,3672,3673,586,3676,3679,3680,3683],{},"For iOS, check the minimum iOS version in the ",[46,3670,3671],{},"Podfile"," against what your Firebase plugins require, then run ",[46,3674,3675],{},"pod repo update",[46,3677,3678],{},"pod install"," in the ",[46,3681,3682],{},"ios"," folder.",[33,3685,3686,3687,3690],{},"For Android, check that the Android Gradle Plugin, Gradle and Kotlin versions are compatible with each other, and that ",[46,3688,3689],{},"minSdkVersion"," meets your plugins' minimum.",[33,3692,3693],{},"After upgrading Flutter or the FlutterFire plugins, upgrade all FlutterFire plugins together; mixed versions are a common cause of build errors.",[898,3695,3697],{"id":3696},"login-loops","Login loops",[14,3699,3700],{},"A user signs in and lands back on the sign-in screen, or the app flickers between screens. The usual causes:",[30,3702,3703,3710,3713],{},[33,3704,3705,3706,3709],{},"The router redirects before Firebase Auth has restored the session. On a cold start, ",[46,3707,3708],{},"authStateChanges()"," emits once the saved user is loaded; decide the first screen after that first event, not before.",[33,3711,3712],{},"Redirect logic that reads two different sources of truth, such as the auth state and a user profile document that hasn't loaded yet.",[33,3714,3715],{},"On web, the redirect sign-in flow returning to a page that immediately redirects again.",[898,3717,3719],{"id":3718},"firestore-writes-that-hang","Firestore writes that hang",[14,3721,3722,3723,332,3726,3729,3730,3733,3734,3736,3737,3739,3740,3742,3743,3745],{},"A ",[46,3724,3725],{},"set()",[46,3727,3728],{},"update()"," call whose ",[46,3731,3732],{},"Future"," never completes is often not stuck at all. With offline persistence, which is on by default on Android and iOS, the write is applied to the local cache immediately, and the ",[46,3735,3732],{}," completes only when the server confirms it. On a poor connection or with no connection, that can take a long time. Don't block the UI on the ",[46,3738,3732],{},": let the snapshot listener show the change. If the write is rejected, the ",[46,3741,3732],{}," completes with an error such as ",[46,3744,3609],{},", so make sure something catches and reports it.",[898,3747,3749],{"id":3748},"cloud-function-errors","Cloud Function errors",[30,3751,3752,3758,3765,3771],{},[33,3753,3754,3755,124],{},"Read the function's own logs before the client error; the client often only receives ",[46,3756,3757],{},"internal",[33,3759,3760,3761,3764],{},"In callable functions, throw an ",[46,3762,3763],{},"HttpsError"," with a clear code and message, so the app can show a useful error instead of a generic one.",[33,3766,3767,3768,124],{},"Check the function's region in the app matches where it is deployed; a callable in the wrong region returns ",[46,3769,3770],{},"not-found",[33,3772,3773],{},"For timeouts, compare the configured timeout and memory with what the function actually does.",[898,3775,3777],{"id":3776},"a-broken-screen","A broken screen",[14,3779,3780,3781,3784],{},"A red error screen in debug, or a grey one in release, means an exception was thrown during build. The console shows the first exception; fix that one first, because later errors are often caused by it. Common causes are a ",[46,3782,3783],{},"null"," value from a document field that doesn't exist on older records, and a list or column without bounded height inside a scrolling parent.",[25,3786,3788],{"id":3787},"fix-it-with-a-test","Fix it with a test",[14,3790,3791,3792,3796],{},"When you've found the root cause, write a test that fails because of it, then make it pass. Widget tests cover screens and state; the ",[189,3793,3795],{"href":3645,"rel":3794},[193],"Firebase emulators"," let integration tests and Security Rules tests run against real Firebase behavior. The test keeps the bug from coming back in the next release.",[25,3798,3800],{"id":3799},"when-to-get-help","When to get help",[14,3802,3803,3804,3807,3808,3812],{},"If a bug has eaten more than a day, App369 offers a ",[189,3805,3806],{"href":3509},"fixed-price Fix Ticket",". The $450 Diagnosis delivers the root cause with evidence and a fixed repair quote within 3 business days, and the $450 is credited toward the fix. The $1,200 Fix delivers one scoped defect fixed as a pull request, with a regression test and handover notes, within 5 business days. If the problem turns out to be your Security Rules, the ",[189,3809,3811],{"href":3810},"/blog/firebase-security-guide-2026/","Firebase Security Rules guide"," is a good next read.",[25,3814,1970],{"id":1969},[898,3816,3818],{"id":3817},"why-does-my-firestore-write-never-finish","Why does my Firestore write never finish?",[14,3820,3821,3822,3824,3825,124],{},"With offline persistence, a write is saved to the local cache at once, but its ",[46,3823,3732],{}," completes only when the server confirms it. On a slow or missing connection that can take a long time. Show the change from your snapshot listener instead of waiting on the write, and catch errors such as ",[46,3826,3609],{},[898,3828,3830],{"id":3829},"why-does-my-flutter-app-keep-returning-to-the-login-screen","Why does my Flutter app keep returning to the login screen?",[14,3832,3833,3834,3836],{},"Usually because the router decides where to go before Firebase Auth has restored the saved session, or because redirect logic depends on data that hasn't loaded yet. Wait for the first ",[46,3835,3708],{}," event before choosing the first screen.",[898,3838,3840],{"id":3839},"how-do-i-see-cloud-functions-errors","How do I see Cloud Functions errors?",[14,3842,3843,3844,3846,3847,3849],{},"In Logs Explorer in the Google Cloud console, or with ",[46,3845,3598],{},". The app often receives only a generic ",[46,3848,3757],{}," error, so the function's own logs are where the real message is.",[898,3851,3853],{"id":3852},"can-i-debug-firebase-without-touching-production-data","Can I debug Firebase without touching production data?",[14,3855,3856],{},"Yes. The Firebase Local Emulator Suite runs Auth, Firestore, Functions and Storage locally. Load test data that reproduces the bug and point a debug build at the emulators.",[898,3858,3860],{"id":3859},"what-should-i-send-a-developer-to-fix-a-flutter-bug","What should I send a developer to fix a Flutter bug?",[14,3862,3863],{},"Exact reproduction steps, the app version and build number, the device and OS, the error message or a screen recording, and access to the repository. With those, a developer can usually reproduce the bug on the first try.",{"title":357,"searchDepth":371,"depth":371,"links":3865},[3866,3867,3868,3869,3876,3877,3878],{"id":3514,"depth":371,"text":3515},{"id":3538,"depth":371,"text":3539},{"id":3639,"depth":371,"text":3640},{"id":3651,"depth":371,"text":3652,"children":3870},[3871,3872,3873,3874,3875],{"id":3655,"depth":381,"text":3656},{"id":3696,"depth":381,"text":3697},{"id":3718,"depth":381,"text":3719},{"id":3748,"depth":381,"text":3749},{"id":3776,"depth":381,"text":3777},{"id":3787,"depth":371,"text":3788},{"id":3799,"depth":371,"text":3800},{"id":1969,"depth":371,"text":1970,"children":3879},[3880,3881,3882,3883,3884],{"id":3817,"depth":381,"text":3818},{"id":3829,"depth":381,"text":3830},{"id":3839,"depth":381,"text":3840},{"id":3852,"depth":381,"text":3853},{"id":3859,"depth":381,"text":3860},"2026-10-12T15:00:00.000Z","Find the root cause of a Flutter and Firebase bug: reproduce it, read the right logs, isolate it with the Emulator Suite, and fix the five most common issues.",{"readingTimeStats":3888},{"text":3889,"minutes":3890,"time":3891,"words":3892},"6 min read",5.665,339900,1133,"/blog/how-to-debug-a-flutter-firebase-bug-2026",{"title":3493,"description":3886},"blog/how-to-debug-a-flutter-firebase-bug-2026","flutter debugging, firebase debugging, flutter build failure, firebase auth, firestore, cloud functions errors","https://app369.com/blog/how-to-debug-a-flutter-firebase-bug-2026/","Og5eeAe_Qd9f0SuWjNTINlCluQrpEjwYZ_XH4tsTFRU",[3900,3902,3905,3906],{"keyword":5,"url":828,"title":5,"relevance":3901},83,{"keyword":3903,"url":3904,"title":3903,"relevance":2383},"Flutter vs Native in 2026: An Honest Decision Guide","https://app369.com/blog/flutter-vs-native-app-development-2026/",{"keyword":3493,"url":3897,"title":3493,"relevance":439},{"keyword":3907,"url":3908,"title":3907,"relevance":412},"Best Android App Development Companies in 2026","https://app369.com/blog/best-app-development-company-android-2026/",1792225358358]