[{"data":1,"prerenderedAt":3543},["ShallowReactive",2],{"\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026\u002F":3,"\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026\u002F-related":580,"\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026\u002F-resources":3527},{"id":4,"title":5,"author":6,"body":10,"category":482,"createdAt":483,"description":484,"disclosure":485,"extension":486,"faq":487,"howTo":485,"location":503,"meta":504,"navigation":510,"ogImage":485,"path":511,"primaryKeyword":512,"readingTime":513,"related":514,"seo":518,"sources":519,"stem":570,"tags":571,"takeaways":572,"updatedAt":483,"url":578,"__hash__":579},"blog\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026.md","Twitter Digits Is Gone: Phone-Number Sign-In for Mobile Apps in 2026",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":469},"minimark",[13,21,24,30,35,123,132,137,140,162,166,169,290,297,301,310,313,346,350,388,392,452,456],[14,15,16,20],"p",{},[17,18,19],"strong",{},"Twitter Digits, the free \"sign in with your phone number\" SDK, shut down on September 30, 2017, and Firebase phone authentication took its place."," In 2026, adding SMS sign-in to a mobile app is still straightforward, but it's never free and it attracts fraud. Firebase is the closest successor and the simplest option for Flutter, iOS and Android. Twilio Verify suits apps that want a dedicated verification service. Whatever you choose, limit which countries you send codes to before you launch.",[14,22,23],{},"This page replaces the Digits pages and Fabric docs that older tutorials still link to. Digits dates come from its own blog and Twitter's, read through the Wayback Machine. Prices come from each vendor's pricing page and were checked on October 17, 2026.",[14,25,26,29],{},[17,27,28],{},"Disclosure:"," App369 is my studio, and I build most apps with Flutter and Firebase, which appear below. No vendor on this page paid for a mention.",[31,32,34],"h2",{"id":33},"what-happened-to-digits-and-when","What happened to Digits, and when",[36,37,38,54],"table",{},[39,40,41],"thead",{},[42,43,44,48,51],"tr",{},[45,46,47],"th",{},"Date",[45,49,50],{},"What happened",[45,52,53],{},"Source",[55,56,57,75,90,105],"tbody",{},[42,58,59,63,66],{},[60,61,62],"td",{},"October 22, 2014",[60,64,65],{},"Twitter launches Digits at its Flight conference, alongside Fabric: free sign-in with a phone number and an SMS code",[60,67,68],{},[69,70,74],"a",{"href":71,"rel":72},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F2016\u002Fhttps:\u002F\u002Fblog.twitter.com\u002F2014\u002Fa-better-way-to-sign-in-with-digits",[73],"nofollow","Twitter blog",[42,76,77,80,83],{},[60,78,79],{},"January 18, 2017",[60,81,82],{},"Google agrees to acquire Fabric from Twitter",[60,84,85],{},[69,86,89],{"href":87,"rel":88},"https:\u002F\u002Fdevelopers.googleblog.com\u002Fwelcoming-fabric-to-google\u002F",[73],"Google Developers Blog",[42,91,92,95,98],{},[60,93,94],{},"May 17, 2017",[60,96,97],{},"The Digits team announces Digits will become Firebase phone authentication",[60,99,100],{},[69,101,104],{"href":102,"rel":103},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F2017\u002Fhttp:\u002F\u002Fget.digits.com\u002Fblog\u002Fintroducing-firebase-phone-authentication",[73],"Digits blog",[42,106,107,112,117],{},[60,108,109],{},[17,110,111],{},"September 30, 2017",[60,113,114],{},[17,115,116],{},"Digits shuts down; apps still on the Digits SDK can no longer sign users in",[60,118,119],{},[69,120,104],{"href":121,"rel":122},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F20171226142759\u002Fhttps:\u002F\u002Fget.digits.com\u002Fblog\u002Fdigits-is-now-shut-down",[73],[14,124,125,126,131],{},"Some Digits features didn't carry over to Firebase, including delivering the code by voice call and the optional account PIN, according to ",[69,127,130],{"href":128,"rel":129},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F2017\u002Fhttps:\u002F\u002Fdocs.fabric.io\u002Fapple\u002Fdigits\u002Fapple-migration.html",[73],"Fabric's migration guide",".",[133,134,136],"h3",{"id":135},"what-the-old-addresses-do-today","What the old addresses do today",[14,138,139],{},"Checked on October 17, 2026:",[141,142,143,150,156],"ul",{},[144,145,146,149],"li",{},[17,147,148],{},"get.digits.com"," and its blog no longer resolve. The Wayback Machine has copies.",[144,151,152,155],{},[17,153,154],{},"digits.com"," now belongs to an unrelated accounting software company, so old \"Digits\" links send readers somewhere confusing.",[144,157,158,161],{},[17,159,160],{},"docs.fabric.io\u002Fapple\u002Fdigits\u002F"," redirects to the Firebase home page.",[31,163,165],{"id":164},"the-2026-options-compared","The 2026 options compared",[14,167,168],{},"Prices were checked on October 17, 2026. SMS prices vary by destination country, so check your own markets.",[36,170,171,190],{},[39,172,173],{},[42,174,175,178,181,184,187],{},[45,176,177],{},"Option",[45,179,180],{},"How it works",[45,182,183],{},"SMS cost",[45,185,186],{},"Free allowance",[45,188,189],{},"Fraud controls the vendor documents",[55,191,192,211,230,253,271],{},[42,193,194,199,202,205,208],{},[60,195,196],{},[17,197,198],{},"Firebase Authentication",[60,200,201],{},"Phone sign-in built into Firebase Auth, with SDKs for Flutter, iOS, Android and web. Android can sometimes verify the number automatically",[60,203,204],{},"Per SMS by country on the Blaze plan: US $0.01, UK $0.04, India $0.07, Germany $0.10",[60,206,207],{},"First 10 SMS a day. Not available on the free Spark plan",[60,209,210],{},"SMS region policy (new projects allow no regions), Play Integrity on Android, silent push on iOS, reCAPTCHA fallback",[42,212,213,218,221,224,227],{},[60,214,215],{},[17,216,217],{},"Twilio Verify",[60,219,220],{},"A verification API: you send and check codes from your backend",[60,222,223],{},"$0.05 per successful verification, plus $0.0083 per SMS in the US",[60,225,226],{},"Check Twilio's current trial terms",[60,228,229],{},"Fraud Guard (included), geo permissions, rate limits",[42,231,232,237,244,247,250],{},[60,233,234],{},[17,235,236],{},"Supabase Auth",[60,238,239,243],{},[240,241,242],"code",{},"signInWithOtp"," with a phone number, through an SMS provider you configure (MessageBird, Twilio, Vonage or TextLocal)",[60,245,246],{},"Paid to your SMS provider",[60,248,249],{},"Supabase's free plan covers 50,000 monthly active users; SMS is extra",[60,251,252],{},"CAPTCHA (hCaptcha or Turnstile) and SMS rate limits",[42,254,255,260,263,265,268],{},[60,256,257],{},[17,258,259],{},"Auth0",[60,261,262],{},"Passwordless sign-in with an SMS code, through Twilio or a custom phone provider",[60,264,246],{},[60,266,267],{},"Free plan up to 25,000 monthly active users",[60,269,270],{},"Not documented on the passwordless SMS page; add your own rate limits and CAPTCHA",[42,272,273,278,281,284,287],{},[60,274,275],{},[17,276,277],{},"Amazon Cognito",[60,279,280],{},"SMS sign-in through AWS End User Messaging",[60,282,283],{},"US about $0.0077 plus a $0.0042 carrier fee per SMS",[60,285,286],{},"Essentials: 10,000 monthly active users free, then $0.015 each",[60,288,289],{},"SMS Protect blocks artificially inflated traffic; new accounts start in an SMS sandbox",[14,291,292,293,296],{},"Firebase is also rolling out ",[17,294,295],{},"Phone Number Verification",", which confirms the number with the mobile carrier instead of sending an SMS. Its documentation currently covers Android only and carrier coverage is partial, so treat it as an addition to SMS rather than a replacement for now.",[31,298,300],{"id":299},"sms-pumping-the-cost-nobody-budgets-for","SMS pumping: the cost nobody budgets for",[14,302,303,304,309],{},"SMS pumping, also called toll fraud, is when bots hit your \"send code\" endpoint with numbers in countries where SMS is expensive, and someone shares the revenue from that traffic. ",[69,305,308],{"href":306,"rel":307},"https:\u002F\u002Fwww.twilio.com\u002Fdocs\u002Fverify\u002Fpreventing-toll-fraud",[73],"Twilio's guide"," describes it plainly: both attacks inflate traffic \"with the intent to make money\". It can run up a large bill overnight on an app with no real users.",[14,311,312],{},"What the vendors recommend, in order of impact:",[314,315,316,322,328,334,340],"ol",{},[144,317,318,321],{},[17,319,320],{},"Allow only the countries you serve."," In Firebase, set the SMS region policy. In Twilio, set geo permissions. In AWS, use the sandbox until you go live, then SMS Protect.",[144,323,324,327],{},[17,325,326],{},"Verify the app or the person before sending."," Firebase does this with Play Integrity, silent push and reCAPTCHA. Supabase supports CAPTCHA. On your own backend, add a CAPTCHA before the send call.",[144,329,330,333],{},[17,331,332],{},"Rate-limit per IP address, per phone number and per device,"," with growing delays between retries. Twilio notes that rate limits slow attackers rather than stop them, so combine them with the steps above.",[144,335,336,339],{},[17,337,338],{},"Watch codes sent versus codes verified."," A sudden drop in the verified share, or traffic from a country you don't serve, is the alarm. Google's pricing page suggests blocking regions with very low verification rates.",[144,341,342,345],{},[17,343,344],{},"Set a billing alert."," Google Cloud (which handles Firebase billing), AWS and Twilio all offer spend alerts, for example Twilio usage triggers. Put one in place before launch.",[31,347,349],{"id":348},"how-to-choose","How to choose",[314,351,352,364,370,376,382],{},[144,353,354,357,358,363],{},[17,355,356],{},"You're building with Flutter or Firebase already."," Use Firebase phone auth. It's the direct successor to Digits, and the ",[69,359,362],{"href":360,"rel":361},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fflutter\u002Fphone-auth",[73],"Flutter guide"," covers the full flow. Upgrade to Blaze, set the region policy, and add test numbers for development.",[144,365,366,369],{},[17,367,368],{},"Your backend isn't Firebase and you want one bill for verification."," Twilio Verify. The per-verification price includes fraud protection, and you only pay the $0.05 fee when a code is confirmed (the per-SMS fee is billed on every send).",[144,371,372,375],{},[17,373,374],{},"Your app runs on Supabase."," Use its phone login with Twilio or another supported provider, and turn on CAPTCHA before launch.",[144,377,378,381],{},[17,379,380],{},"You're on AWS already."," Cognito with End User Messaging, plus SMS Protect.",[144,383,384,387],{},[17,385,386],{},"Your users can receive email or use passkeys."," Consider making SMS optional. Email magic links work in Firebase and Supabase, passkeys are standard on Apple and Google devices, and Supabase's passkey support is still marked experimental. Fewer SMS sends means less cost and less fraud.",[31,389,391],{"id":390},"migration-checklist-for-an-app-still-built-around-digits","Migration checklist for an app still built around Digits",[314,393,394,412,418,428,434,440,446],{},[144,395,396,399,400,403,404,407,408,411],{},[17,397,398],{},"Find the old SDK."," Search the project for ",[240,401,402],{},"Digits",", ",[240,405,406],{},"Fabric"," and ",[240,409,410],{},"TwitterKit",". If they're still there, sign-in has been broken since 2017 or the code is dead weight.",[144,413,414,417],{},[17,415,416],{},"Pick the new provider"," from the table above, and create a test project first.",[144,419,420,423,424,427],{},[17,421,422],{},"Map existing users."," If your backend stored Digits user IDs, match accounts by phone number instead, and make sure the number is stored in E.164 format (",[240,425,426],{},"+15615551234",").",[144,429,430,433],{},[17,431,432],{},"Set the fraud controls before launch:"," a country allow list, rate limits, app verification and a billing alert.",[144,435,436,439],{},[17,437,438],{},"Add test numbers"," so development and app review don't send real SMS.",[144,441,442,445],{},[17,443,444],{},"Write the consent text."," Tell meers an SMS may be sent and that standard rates apply. Firebase's docs also ask you to tell users their number is sent to Google for spam and abuse prevention.",[144,447,448,451],{},[17,449,450],{},"Test on real devices,"," including a phone without Google Play services and a slow network, before you release.",[31,453,455],{"id":454},"need-a-hand","Need a hand?",[14,457,458,459,463,464,468],{},"If phone sign-in is stuck on one bug, such as codes that never arrive, a reCAPTCHA loop or a verification that fails on iOS only, the ",[69,460,462],{"href":461},"\u002Ffixed-price\u002Fflutter-firebase-bug-fix\u002F","App Fix Ticket"," is a fixed-price option. The Diagnosis is $450 in 3 business days and the Fix is $1,200 in 5 business days. It covers Flutter, Swift, Kotlin and Firebase among others, and it doesn't cover rewrites. For a full sign-in rebuild, ",[69,465,467],{"href":466},"\u002Fcontact\u002F","get in touch"," and I'll scope it.",{"title":470,"searchDepth":471,"depth":471,"links":472},"",2,[473,477,478,479,480,481],{"id":33,"depth":471,"text":34,"children":474},[475],{"id":135,"depth":476,"text":136},3,{"id":164,"depth":471,"text":165},{"id":299,"depth":471,"text":300},{"id":348,"depth":471,"text":349},{"id":390,"depth":471,"text":391},{"id":454,"depth":471,"text":455},"AI & Technology","2026-10-17T20:00:00.000Z","Twitter Digits shut down on September 30, 2017. How to add phone-number sign-in to a mobile app in 2026: Firebase, Twilio Verify, Supabase, Auth0 and Cognito compared, with SMS prices and fraud controls checked October 17, 2026.",null,"md",[488,491,494,497,500],{"question":489,"answer":490},"What replaced Twitter Digits?","Firebase Authentication's phone sign-in. The Digits team joined Google in January 2017, announced in May 2017 that Digits would become Firebase phone authentication, and shut Digits down on September 30, 2017. Apps that still used the Digits SDK after that could no longer sign users in.",{"question":492,"answer":493},"How much does SMS sign-in cost in 2026?","It depends on the vendor and the country. Firebase charges per SMS on the Blaze plan, from $0.01 in the US to $0.10 in Germany and more elsewhere, with the first 10 SMS a day free. Twilio Verify charges $0.05 per successful verification plus a channel fee, $0.0083 per SMS in the US. Supabase, Auth0 and Cognito pass the cost through to the SMS provider you connect.",{"question":495,"answer":496},"What is SMS pumping fraud?","Attackers trigger sign-in codes to premium or high-cost numbers they profit from, so your SMS bill grows with no real users behind it. Twilio, Google and AWS all document it. The usual defences are a country allow list, rate limits per IP and per number, app attestation or CAPTCHA before sending, and watching the ratio of codes sent to codes verified.",{"question":498,"answer":499},"Is phone-number sign-in secure enough on its own?","For low-risk apps it is common, but Firebase's own docs note that a phone number can be transferred between people, and AWS says SMS isn't a security best practice. For accounts with payments or private data, add a second factor or offer passkeys.",{"question":501,"answer":502},"Can I test phone sign-in without paying for SMS?","Yes. Firebase lets you add up to 10 test phone numbers with fixed codes that never send a real SMS. Use hard-to-guess numbers and codes, and change them regularly.","Boynton Beach, FL",{"readingTimeStats":505},{"text":506,"minutes":507,"time":508,"words":509},"6 min read",5.815,348900,1163,true,"\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026","twitter digits alternative","6 min",[515,516,517],"firebase-security-guide-2026","flutter-security-best-practices-2026","firebase-dynamic-links-shutdown-deep-linking-2026",{"title":5,"description":484},[520,522,524,526,528,530,533,536,538,541,544,547,550,552,555,558,561,564,567],{"title":521,"url":71},"Twitter blog, \"A better way to sign in with Digits\" (October 22, 2014), Wayback Machine copy",{"title":523,"url":102},"Digits blog, \"Introducing Firebase phone authentication\" (May 17, 2017), Wayback Machine copy",{"title":525,"url":121},"Digits blog, \"Digits is now shut down\" (October 2, 2017), Wayback Machine copy",{"title":527,"url":87},"Google Developers Blog, \"Welcoming Fabric to Google\" (January 18, 2017)",{"title":529,"url":128},"Fabric docs, \"Migrating from Digits\" (September 2017), Wayback Machine copy",{"title":531,"url":532},"Firebase blog, \"Firebase Phone Auth\" (2017)","https:\u002F\u002Ffirebase.blog\u002Fposts\u002F2017\u002F05\u002Ffirebase-phone-auth",{"title":534,"url":535},"Firebase docs, phone authentication on the web (SMS region policy, test numbers)","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fweb\u002Fphone-auth",{"title":537,"url":360},"Firebase docs, phone authentication in Flutter",{"title":539,"url":540},"Firebase Authentication limits","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Flimits",{"title":542,"url":543},"Google Cloud Identity Platform pricing (per-country SMS prices)","https:\u002F\u002Fcloud.google.com\u002Fidentity-platform\u002Fpricing",{"title":545,"url":546},"Firebase Phone Number Verification","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fphone-number-verification",{"title":548,"url":549},"Twilio Verify pricing","https:\u002F\u002Fwww.twilio.com\u002Fen-us\u002Fverify\u002Fpricing",{"title":551,"url":306},"Twilio docs, preventing toll fraud",{"title":553,"url":554},"Supabase docs, phone login","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fphone-login",{"title":556,"url":557},"Auth0 docs, passwordless with SMS","https:\u002F\u002Fauth0.com\u002Fdocs\u002Fauthenticate\u002Fpasswordless\u002Fauthentication-methods\u002Fsms-otp",{"title":559,"url":560},"Amazon Cognito docs, SMS message settings","https:\u002F\u002Fdocs.aws.amazon.com\u002Fcognito\u002Flatest\u002Fdeveloperguide\u002Fuser-pool-sms-settings.html",{"title":562,"url":563},"AWS End User Messaging pricing","https:\u002F\u002Faws.amazon.com\u002Fend-user-messaging\u002Fpricing\u002F",{"title":565,"url":566},"Apple Developer, passkeys","https:\u002F\u002Fdeveloper.apple.com\u002Fpasskeys\u002F",{"title":568,"url":569},"Supabase docs, passkeys (experimental)","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fauth-passkeys","blog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026","twitter digits alternative, digits shut down, phone number authentication, sms otp login, firebase phone auth pricing, twilio verify pricing, supabase phone login, sms pumping fraud, passwordless login mobile app, flutter phone auth",[573,574,575,576,577],"Twitter launched Digits, free SMS sign-in for apps, on October 22, 2014. After its team joined Google in 2017, Digits was replaced by Firebase phone authentication and shut down on September 30, 2017.","Phone sign-in is rarely free now. Firebase bills per SMS on the Blaze plan (US $0.01, UK $0.04, Germany $0.10 per message, first 10 a day free), and Twilio Verify charges $0.05 per successful verification plus $0.0083 per US SMS.","The biggest risk is SMS pumping fraud: bots request codes to expensive numbers and someone else earns from the traffic. Limit the countries you send to, rate-limit requests and verify the app before sending.","Firebase is the closest successor to Digits and the simplest choice for Flutter, iOS and Android apps. Supabase, Auth0 and Cognito work through an SMS provider you configure.","Vendors themselves warn that a phone number is a weak sole credential. Pair SMS with email, or offer passkeys, for accounts that hold money or private data.","https:\u002F\u002Fapp369.com\u002Fblog\u002Ftwitter-digits-phone-number-sign-in-mobile-apps-2026\u002F","nHPRcNnsNJAivZb8Ir9zqafR3EVIvS7uAAisaw-ExOs",[581,1981,2757],{"id":582,"title":583,"author":584,"body":586,"category":482,"createdAt":1961,"description":1962,"disclosure":485,"extension":486,"faq":485,"howTo":485,"location":503,"meta":1963,"navigation":510,"ogImage":485,"path":1969,"primaryKeyword":485,"readingTime":1970,"related":1971,"seo":1975,"sources":485,"stem":1976,"tags":1977,"takeaways":485,"updatedAt":1978,"url":1979,"__hash__":1980},"blog\u002Fblog\u002Ffirebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":585},{"twitter":9},{"type":11,"value":587,"toc":1945},[588,600,608,612,615,624,627,647,650,654,670,673,729,732,738,776,779,792,796,805,808,945,948,1010,1013,1017,1026,1029,1082,1085,1137,1146,1149,1173,1177,1180,1183,1286,1289,1367,1370,1374,1383,1391,1396,1420,1429,1541,1544,1587,1591,1594,1597,1603,1692,1698,1748,1754,1819,1822,1826,1832,1876,1887,1891,1895,1903,1907,1910,1914,1929,1933,1941],[14,589,590,593,594,599],{},[17,591,592],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[69,595,598],{"href":596,"rel":597},"https:\u002F\u002Fwww.audityour.app\u002Fguides\u002Ffirebase-security-rules-guide",[73],"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\u002Fproduction mode selection, so every permission must be explicitly defined by the developer.",[14,601,602,603,607],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[69,604,606],{"href":605},"\u002Fservice\u002Fmobile-apps\u002F","App369"," uses to secure Firebase backends across production applications.",[31,609,611],{"id":610},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,613,614],{},"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,616,617,618,623],{},"According to ",[69,619,622],{"href":620,"rel":621},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002F",[73],"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,625,626],{},"Rules apply to three Firebase services independently:",[141,628,629,635,641],{},[144,630,631,634],{},[17,632,633],{},"Cloud Firestore",": document-level read\u002Fwrite control with path-based matching",[144,636,637,640],{},[17,638,639],{},"Realtime Database",": node-level access with cascading permissions",[144,642,643,646],{},[17,644,645],{},"Cloud Storage",": file-level access based on path, metadata, and authentication state",[14,648,649],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[31,651,653],{"id":652},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,655,656,657,407,660,663,664,669],{},"Firebase's setup wizard offers two starting configurations: ",[17,658,659],{},"test mode",[17,661,662],{},"locked mode",". Test mode grants public read and write access to the entire database for 30 days. According to ",[69,665,668],{"href":666,"rel":667},"https:\u002F\u002Fvibeappscanner.com\u002Fis-firebase-safe",[73],"VibeAppScanner's Firebase safety analysis",", deploying with test mode rules is the single most common Firebase security failure in production.",[14,671,672],{},"Test mode rules look like this:",[674,675,679],"pre",{"className":676,"code":677,"language":678,"meta":470,"style":470},"language-javascript shiki shiki-themes github-light github-dark","rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n    match \u002F{document=**} {\n      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n    }\n  }\n}\n","javascript",[240,680,681,689,694,699,705,711,717,723],{"__ignoreMap":470},[682,683,686],"span",{"class":684,"line":685},"line",1,[682,687,688],{},"rules_version = '2';\n",[682,690,691],{"class":684,"line":471},[682,692,693],{},"service cloud.firestore {\n",[682,695,696],{"class":684,"line":476},[682,697,698],{},"  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n",[682,700,702],{"class":684,"line":701},4,[682,703,704],{},"    match \u002F{document=**} {\n",[682,706,708],{"class":684,"line":707},5,[682,709,710],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[682,712,714],{"class":684,"line":713},6,[682,715,716],{},"    }\n",[682,718,720],{"class":684,"line":719},7,[682,721,722],{},"  }\n",[682,724,726],{"class":684,"line":725},8,[682,727,728],{},"}\n",[14,730,731],{},"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,733,734,735,737],{},"The correct starting point is ",[17,736,662],{},", where all access is denied by default:",[674,739,741],{"className":676,"code":740,"language":678,"meta":470,"style":470},"rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n    match \u002F{document=**} {\n      allow read, write: if false;\n    }\n  }\n}\n",[240,742,743,747,751,755,759,764,768,772],{"__ignoreMap":470},[682,744,745],{"class":684,"line":685},[682,746,688],{},[682,748,749],{"class":684,"line":471},[682,750,693],{},[682,752,753],{"class":684,"line":476},[682,754,698],{},[682,756,757],{"class":684,"line":701},[682,758,704],{},[682,760,761],{"class":684,"line":707},[682,762,763],{},"      allow read, write: if false;\n",[682,765,766],{"class":684,"line":713},[682,767,716],{},[682,769,770],{"class":684,"line":719},[682,771,722],{},[682,773,774],{"class":684,"line":725},[682,775,728],{},[14,777,778],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,780,781,782,786,787,791],{},"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 ",[69,783,785],{"href":784},"\u002Ffixed-price\u002Fai-app-rescue\u002F","AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[69,788,790],{"href":789},"\u002Ftools\u002Fsupabase-oct-30-checker\u002F","Supabase October 30 grants checker"," lists the tables that will need explicit grants after that date.",[31,793,795],{"id":794},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,797,798,799,804],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[69,800,803],{"href":801,"rel":802},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Fget-started",[73],"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,806,807],{},"Here is a production-ready pattern for a user-owned data collection:",[674,809,811],{"className":676,"code":810,"language":678,"meta":470,"style":470},"rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n\n    \u002F\u002F Users can only read and write their own profile\n    match \u002Fusers\u002F{userId} {\n      allow read, write: if request.auth != null && request.auth.uid == userId;\n    }\n\n    \u002F\u002F Posts are publicly readable, but only the author can write\n    match \u002Fposts\u002F{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    \u002F\u002F Admin-only collection\n    match \u002FadminConfig\u002F{docId} {\n      allow read, write: if request.auth != null\n                         && request.auth.token.admin == true;\n    }\n  }\n}\n",[240,812,813,817,821,825,830,835,840,845,849,854,860,866,872,878,884,890,896,901,906,912,918,924,930,935,940],{"__ignoreMap":470},[682,814,815],{"class":684,"line":685},[682,816,688],{},[682,818,819],{"class":684,"line":471},[682,820,693],{},[682,822,823],{"class":684,"line":476},[682,824,698],{},[682,826,827],{"class":684,"line":701},[682,828,829],{"emptyLinePlaceholder":510},"\n",[682,831,832],{"class":684,"line":707},[682,833,834],{},"    \u002F\u002F Users can only read and write their own profile\n",[682,836,837],{"class":684,"line":713},[682,838,839],{},"    match \u002Fusers\u002F{userId} {\n",[682,841,842],{"class":684,"line":719},[682,843,844],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[682,846,847],{"class":684,"line":725},[682,848,716],{},[682,850,852],{"class":684,"line":851},9,[682,853,829],{"emptyLinePlaceholder":510},[682,855,857],{"class":684,"line":856},10,[682,858,859],{},"    \u002F\u002F Posts are publicly readable, but only the author can write\n",[682,861,863],{"class":684,"line":862},11,[682,864,865],{},"    match \u002Fposts\u002F{postId} {\n",[682,867,869],{"class":684,"line":868},12,[682,870,871],{},"      allow read: if true;\n",[682,873,875],{"class":684,"line":874},13,[682,876,877],{},"      allow create: if request.auth != null\n",[682,879,881],{"class":684,"line":880},14,[682,882,883],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[682,885,887],{"class":684,"line":886},15,[682,888,889],{},"      allow update, delete: if request.auth != null\n",[682,891,893],{"class":684,"line":892},16,[682,894,895],{},"                            && resource.data.authorId == request.auth.uid;\n",[682,897,899],{"class":684,"line":898},17,[682,900,716],{},[682,902,904],{"class":684,"line":903},18,[682,905,829],{"emptyLinePlaceholder":510},[682,907,909],{"class":684,"line":908},19,[682,910,911],{},"    \u002F\u002F Admin-only collection\n",[682,913,915],{"class":684,"line":914},20,[682,916,917],{},"    match \u002FadminConfig\u002F{docId} {\n",[682,919,921],{"class":684,"line":920},21,[682,922,923],{},"      allow read, write: if request.auth != null\n",[682,925,927],{"class":684,"line":926},22,[682,928,929],{},"                         && request.auth.token.admin == true;\n",[682,931,933],{"class":684,"line":932},23,[682,934,716],{},[682,936,938],{"class":684,"line":937},24,[682,939,722],{},[682,941,943],{"class":684,"line":942},25,[682,944,728],{},[14,946,947],{},"Key principles for effective rules:",[141,949,950,974,983,996],{},[144,951,952,955,956,407,959,962,963,403,966,969,970,973],{},[17,953,954],{},"Separate read and write permissions."," Use ",[240,957,958],{},"allow read",[240,960,961],{},"allow write"," independently. Break write into ",[240,964,965],{},"create",[240,967,968],{},"update",", and ",[240,971,972],{},"delete"," when different logic applies.",[144,975,976,982],{},[17,977,978,979,131],{},"Always check ",[240,980,981],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[144,984,985,407,992,995],{},[17,986,987,988,991],{},"Use ",[240,989,990],{},"resource.data"," for existing document fields",[240,993,994],{},"request.resource.data"," for incoming data in create\u002Fupdate operations.",[144,997,998,1001,1002,1005,1006,1009],{},[17,999,1000],{},"Avoid wildcards at the root level."," The pattern ",[240,1003,1004],{},"match \u002F{document=**}"," applies to every document in the database. Use it only with ",[240,1007,1008],{},"if false"," as a catch-all deny rule.",[14,1011,1012],{},"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.",[31,1014,1016],{"id":1015},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,1018,1019,1020,1025],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[69,1021,1024],{"href":1022,"rel":1023},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fadmin\u002Fcustom-claims",[73],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,1027,1028],{},"Set custom claims from a Cloud Function or admin server:",[674,1030,1032],{"className":676,"code":1031,"language":678,"meta":470,"style":470},"const admin = require('firebase-admin');\n\n\u002F\u002F Assign admin role\nawait admin.auth().setCustomUserClaims(userId, { admin: true });\n\n\u002F\u002F Assign editor role with department\nawait admin.auth().setCustomUserClaims(userId, {\n  role: 'editor',\n  department: 'marketing'\n});\n",[240,1033,1034,1039,1043,1048,1053,1057,1062,1067,1072,1077],{"__ignoreMap":470},[682,1035,1036],{"class":684,"line":685},[682,1037,1038],{},"const admin = require('firebase-admin');\n",[682,1040,1041],{"class":684,"line":471},[682,1042,829],{"emptyLinePlaceholder":510},[682,1044,1045],{"class":684,"line":476},[682,1046,1047],{},"\u002F\u002F Assign admin role\n",[682,1049,1050],{"class":684,"line":701},[682,1051,1052],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[682,1054,1055],{"class":684,"line":707},[682,1056,829],{"emptyLinePlaceholder":510},[682,1058,1059],{"class":684,"line":713},[682,1060,1061],{},"\u002F\u002F Assign editor role with department\n",[682,1063,1064],{"class":684,"line":719},[682,1065,1066],{},"await admin.auth().setCustomUserClaims(userId, {\n",[682,1068,1069],{"class":684,"line":725},[682,1070,1071],{},"  role: 'editor',\n",[682,1073,1074],{"class":684,"line":851},[682,1075,1076],{},"  department: 'marketing'\n",[682,1078,1079],{"class":684,"line":856},[682,1080,1081],{},"});\n",[14,1083,1084],{},"Reference claims in Security Rules:",[674,1086,1088],{"className":676,"code":1087,"language":678,"meta":470,"style":470},"match \u002Fcontent\u002F{docId} {\n  \u002F\u002F Admins have full access\n  allow read, write: if request.auth.token.admin == true;\n\n  \u002F\u002F Editors can read and update, but not delete\n  allow read, update: if request.auth.token.role == 'editor';\n\n  \u002F\u002F All authenticated users can read\n  allow read: if request.auth != null;\n}\n",[240,1089,1090,1095,1100,1105,1109,1114,1119,1123,1128,1133],{"__ignoreMap":470},[682,1091,1092],{"class":684,"line":685},[682,1093,1094],{},"match \u002Fcontent\u002F{docId} {\n",[682,1096,1097],{"class":684,"line":471},[682,1098,1099],{},"  \u002F\u002F Admins have full access\n",[682,1101,1102],{"class":684,"line":476},[682,1103,1104],{},"  allow read, write: if request.auth.token.admin == true;\n",[682,1106,1107],{"class":684,"line":701},[682,1108,829],{"emptyLinePlaceholder":510},[682,1110,1111],{"class":684,"line":707},[682,1112,1113],{},"  \u002F\u002F Editors can read and update, but not delete\n",[682,1115,1116],{"class":684,"line":713},[682,1117,1118],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[682,1120,1121],{"class":684,"line":719},[682,1122,829],{"emptyLinePlaceholder":510},[682,1124,1125],{"class":684,"line":725},[682,1126,1127],{},"  \u002F\u002F All authenticated users can read\n",[682,1129,1130],{"class":684,"line":851},[682,1131,1132],{},"  allow read: if request.auth != null;\n",[682,1134,1135],{"class":684,"line":856},[682,1136,728],{},[14,1138,1139,1140,1145],{},"As noted in ",[69,1141,1144],{"href":1142,"rel":1143},"https:\u002F\u002Fmedium.com\u002F@NadunKulatunge\u002Fadvanced-use-cases-and-best-practices-of-firebase-security-rules-cb5d3339d8a7",[73],"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,1147,1148],{},"Implementation guidelines:",[141,1150,1151,1157,1167],{},[144,1152,1153,1156],{},[17,1154,1155],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[144,1158,1159,1162,1163,1166],{},[17,1160,1161],{},"Force token refresh after claim changes."," Call ",[240,1164,1165],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[144,1168,1169,1172],{},[17,1170,1171],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[31,1174,1176],{"id":1175},"data-validation-in-security-rules","Data Validation in Security Rules",[14,1178,1179],{},"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,1181,1182],{},"Validation patterns for common requirements:",[674,1184,1186],{"className":676,"code":1185,"language":678,"meta":470,"style":470},"match \u002Fusers\u002F{userId} {\n  allow create: if request.auth.uid == userId\n    \u002F\u002F Required fields must exist\n    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n    \u002F\u002F 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    \u002F\u002F 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    \u002F\u002F createdAt must be a timestamp\n    && request.resource.data.createdAt is timestamp\n    \u002F\u002F 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    \u002F\u002F Prevent changing the email field\n    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n}\n",[240,1187,1188,1193,1198,1203,1208,1213,1218,1223,1228,1233,1238,1243,1248,1253,1258,1263,1267,1272,1277,1282],{"__ignoreMap":470},[682,1189,1190],{"class":684,"line":685},[682,1191,1192],{},"match \u002Fusers\u002F{userId} {\n",[682,1194,1195],{"class":684,"line":471},[682,1196,1197],{},"  allow create: if request.auth.uid == userId\n",[682,1199,1200],{"class":684,"line":476},[682,1201,1202],{},"    \u002F\u002F Required fields must exist\n",[682,1204,1205],{"class":684,"line":701},[682,1206,1207],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[682,1209,1210],{"class":684,"line":707},[682,1211,1212],{},"    \u002F\u002F Name must be a string between 2 and 100 characters\n",[682,1214,1215],{"class":684,"line":713},[682,1216,1217],{},"    && request.resource.data.name is string\n",[682,1219,1220],{"class":684,"line":719},[682,1221,1222],{},"    && request.resource.data.name.size() >= 2\n",[682,1224,1225],{"class":684,"line":725},[682,1226,1227],{},"    && request.resource.data.name.size() \u003C= 100\n",[682,1229,1230],{"class":684,"line":851},[682,1231,1232],{},"    \u002F\u002F Email must be a string and match the auth email\n",[682,1234,1235],{"class":684,"line":856},[682,1236,1237],{},"    && request.resource.data.email is string\n",[682,1239,1240],{"class":684,"line":862},[682,1241,1242],{},"    && request.resource.data.email == request.auth.token.email\n",[682,1244,1245],{"class":684,"line":868},[682,1246,1247],{},"    \u002F\u002F createdAt must be a timestamp\n",[682,1249,1250],{"class":684,"line":874},[682,1251,1252],{},"    && request.resource.data.createdAt is timestamp\n",[682,1254,1255],{"class":684,"line":880},[682,1256,1257],{},"    \u002F\u002F No unexpected fields allowed\n",[682,1259,1260],{"class":684,"line":886},[682,1261,1262],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[682,1264,1265],{"class":684,"line":892},[682,1266,829],{"emptyLinePlaceholder":510},[682,1268,1269],{"class":684,"line":898},[682,1270,1271],{},"  allow update: if request.auth.uid == userId\n",[682,1273,1274],{"class":684,"line":903},[682,1275,1276],{},"    \u002F\u002F Prevent changing the email field\n",[682,1278,1279],{"class":684,"line":908},[682,1280,1281],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[682,1283,1284],{"class":684,"line":914},[682,1285,728],{},[14,1287,1288],{},"Validation capabilities in Firestore rules include:",[141,1290,1291,1319,1329,1343,1352,1358],{},[144,1292,1293,1296,1297,403,1300,403,1303,403,1306,403,1309,403,1312,1315,1316],{},[17,1294,1295],{},"Type checking",": verify that fields are ",[240,1298,1299],{},"string",[240,1301,1302],{},"int",[240,1304,1305],{},"float",[240,1307,1308],{},"bool",[240,1310,1311],{},"timestamp",[240,1313,1314],{},"list",", or ",[240,1317,1318],{},"map",[144,1320,1321,1324,1325,1328],{},[17,1322,1323],{},"String length",": use ",[240,1326,1327],{},".size()"," to enforce minimum and maximum lengths",[144,1330,1331,1334,1335,1338,1339,1342],{},[17,1332,1333],{},"Field existence",": ",[240,1336,1337],{},"hasAll()"," for required fields, ",[240,1340,1341],{},"hasOnly()"," to prevent extra fields",[144,1344,1345,1324,1348,1351],{},[17,1346,1347],{},"Field immutability",[240,1349,1350],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[144,1353,1354,1357],{},[17,1355,1356],{},"Numeric ranges",": enforce minimum and maximum values for numbers",[144,1359,1360,1363,1364,1366],{},[17,1361,1362],{},"List constraints",": check ",[240,1365,1327],{}," on lists to limit array lengths",[14,1368,1369],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[31,1371,1373],{"id":1372},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,1375,1376,1377,1382],{},"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; ",[69,1378,1381],{"href":1379,"rel":1380},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002Funit-tests",[73],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,1384,1385,1386,1390],{},"For a quick first pass, paste your rules into App369's free ",[69,1387,1389],{"href":1388},"\u002Ftools\u002Ffirebase-rules-checker\u002F","Firebase Security Rules checker",". It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.",[14,1392,1393],{},[17,1394,1395],{},"Use the Firebase Emulator Suite for local testing:",[674,1397,1401],{"className":1398,"code":1399,"language":1400,"meta":470,"style":470},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[240,1402,1403],{"__ignoreMap":470},[682,1404,1405,1409,1413,1417],{"class":684,"line":685},[682,1406,1408],{"class":1407},"sScJk","firebase",[682,1410,1412],{"class":1411},"sZZnC"," emulators:start",[682,1414,1416],{"class":1415},"sj4cs"," --only",[682,1418,1419],{"class":1411}," firestore\n",[14,1421,1422],{},[17,1423,1424,1425,1428],{},"Write unit tests with the ",[240,1426,1427],{},"@firebase\u002Frules-unit-testing"," package:",[674,1430,1432],{"className":676,"code":1431,"language":678,"meta":470,"style":470},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n\nconst testEnv = await initializeTestEnvironment({\n  projectId: 'your-project-id',\n  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n});\n\n\u002F\u002F 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\u002F\u002F Test: user cannot read another user's document\nawait assertFails(\n  userContext.firestore().collection('users').doc('other-user').get()\n);\n\n\u002F\u002F Test: unauthenticated user cannot write\nconst unauthedContext = testEnv.unauthenticatedContext();\nawait assertFails(\n  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n);\n",[240,1433,1434,1439,1443,1448,1453,1458,1462,1466,1471,1476,1481,1486,1491,1495,1500,1505,1510,1514,1518,1523,1528,1532,1537],{"__ignoreMap":470},[682,1435,1436],{"class":684,"line":685},[682,1437,1438],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n",[682,1440,1441],{"class":684,"line":471},[682,1442,829],{"emptyLinePlaceholder":510},[682,1444,1445],{"class":684,"line":476},[682,1446,1447],{},"const testEnv = await initializeTestEnvironment({\n",[682,1449,1450],{"class":684,"line":701},[682,1451,1452],{},"  projectId: 'your-project-id',\n",[682,1454,1455],{"class":684,"line":707},[682,1456,1457],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[682,1459,1460],{"class":684,"line":713},[682,1461,1081],{},[682,1463,1464],{"class":684,"line":719},[682,1465,829],{"emptyLinePlaceholder":510},[682,1467,1468],{"class":684,"line":725},[682,1469,1470],{},"\u002F\u002F Test: authenticated user can read their own document\n",[682,1472,1473],{"class":684,"line":851},[682,1474,1475],{},"const userContext = testEnv.authenticatedContext('user123');\n",[682,1477,1478],{"class":684,"line":856},[682,1479,1480],{},"await assertSucceeds(\n",[682,1482,1483],{"class":684,"line":862},[682,1484,1485],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[682,1487,1488],{"class":684,"line":868},[682,1489,1490],{},");\n",[682,1492,1493],{"class":684,"line":874},[682,1494,829],{"emptyLinePlaceholder":510},[682,1496,1497],{"class":684,"line":880},[682,1498,1499],{},"\u002F\u002F Test: user cannot read another user's document\n",[682,1501,1502],{"class":684,"line":886},[682,1503,1504],{},"await assertFails(\n",[682,1506,1507],{"class":684,"line":892},[682,1508,1509],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[682,1511,1512],{"class":684,"line":898},[682,1513,1490],{},[682,1515,1516],{"class":684,"line":903},[682,1517,829],{"emptyLinePlaceholder":510},[682,1519,1520],{"class":684,"line":908},[682,1521,1522],{},"\u002F\u002F Test: unauthenticated user cannot write\n",[682,1524,1525],{"class":684,"line":914},[682,1526,1527],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[682,1529,1530],{"class":684,"line":920},[682,1531,1504],{},[682,1533,1534],{"class":684,"line":926},[682,1535,1536],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[682,1538,1539],{"class":684,"line":932},[682,1540,1490],{},[14,1542,1543],{},"Deployment workflow:",[314,1545,1546,1561,1567,1575,1581],{},[144,1547,1548,1551,1552,403,1555,969,1558],{},[17,1549,1550],{},"Write rules locally"," in ",[240,1553,1554],{},"firestore.rules",[240,1556,1557],{},"database.rules.json",[240,1559,1560],{},"storage.rules",[144,1562,1563,1566],{},[17,1564,1565],{},"Test with the emulator",": run the full test suite against the local emulator before every deployment",[144,1568,1569,1574],{},[17,1570,987,1571],{},[240,1572,1573],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[144,1576,1577,1580],{},[17,1578,1579],{},"Version control your rules files",": commit them to Git and review changes in pull requests",[144,1582,1583,1586],{},[17,1584,1585],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[31,1588,1590],{"id":1589},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,1592,1593],{},"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,1595,1596],{},"Use Cloud Functions as a second validation layer for:",[14,1598,1599,1602],{},[17,1600,1601],{},"Financial transactions",": verify amounts, check account balances, and enforce rate limits server-side:",[674,1604,1606],{"className":676,"code":1605,"language":678,"meta":470,"style":470},"exports.processPayment = functions.https.onCall(async (data, context) => {\n  \u002F\u002F Verify authentication\n  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n\n  \u002F\u002F 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  \u002F\u002F 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  \u002F\u002F Process the transaction with admin privileges\n  \u002F\u002F ...\n});\n",[240,1607,1608,1613,1618,1623,1627,1632,1637,1642,1646,1650,1655,1660,1665,1670,1674,1678,1683,1688],{"__ignoreMap":470},[682,1609,1610],{"class":684,"line":685},[682,1611,1612],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[682,1614,1615],{"class":684,"line":471},[682,1616,1617],{},"  \u002F\u002F Verify authentication\n",[682,1619,1620],{"class":684,"line":476},[682,1621,1622],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[682,1624,1625],{"class":684,"line":701},[682,1626,829],{"emptyLinePlaceholder":510},[682,1628,1629],{"class":684,"line":707},[682,1630,1631],{},"  \u002F\u002F Server-side amount validation\n",[682,1633,1634],{"class":684,"line":713},[682,1635,1636],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[682,1638,1639],{"class":684,"line":719},[682,1640,1641],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[682,1643,1644],{"class":684,"line":725},[682,1645,722],{},[682,1647,1648],{"class":684,"line":851},[682,1649,829],{"emptyLinePlaceholder":510},[682,1651,1652],{"class":684,"line":856},[682,1653,1654],{},"  \u002F\u002F Check user's balance from a trusted source\n",[682,1656,1657],{"class":684,"line":862},[682,1658,1659],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[682,1661,1662],{"class":684,"line":868},[682,1663,1664],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[682,1666,1667],{"class":684,"line":874},[682,1668,1669],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[682,1671,1672],{"class":684,"line":880},[682,1673,722],{},[682,1675,1676],{"class":684,"line":886},[682,1677,829],{"emptyLinePlaceholder":510},[682,1679,1680],{"class":684,"line":892},[682,1681,1682],{},"  \u002F\u002F Process the transaction with admin privileges\n",[682,1684,1685],{"class":684,"line":898},[682,1686,1687],{},"  \u002F\u002F ...\n",[682,1689,1690],{"class":684,"line":903},[682,1691,1081],{},[14,1693,1694,1697],{},[17,1695,1696],{},"Data aggregation and denormalization",": use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[674,1699,1701],{"className":676,"code":1700,"language":678,"meta":470,"style":470},"exports.onPostCreated = functions.firestore\n  .document('posts\u002F{postId}')\n  .onCreate(async (snap, context) => {\n    \u002F\u002F 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",[240,1702,1703,1708,1713,1718,1723,1728,1733,1738,1743],{"__ignoreMap":470},[682,1704,1705],{"class":684,"line":685},[682,1706,1707],{},"exports.onPostCreated = functions.firestore\n",[682,1709,1710],{"class":684,"line":471},[682,1711,1712],{},"  .document('posts\u002F{postId}')\n",[682,1714,1715],{"class":684,"line":476},[682,1716,1717],{},"  .onCreate(async (snap, context) => {\n",[682,1719,1720],{"class":684,"line":701},[682,1721,1722],{},"    \u002F\u002F Increment the author's post count atomically\n",[682,1724,1725],{"class":684,"line":707},[682,1726,1727],{},"    const authorId = snap.data().authorId;\n",[682,1729,1730],{"class":684,"line":713},[682,1731,1732],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[682,1734,1735],{"class":684,"line":719},[682,1736,1737],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[682,1739,1740],{"class":684,"line":725},[682,1741,1742],{},"    });\n",[682,1744,1745],{"class":684,"line":851},[682,1746,1747],{},"  });\n",[14,1749,1750,1753],{},[17,1751,1752],{},"Rate limiting",": prevent abuse by tracking request frequency per user:",[674,1755,1757],{"className":676,"code":1756,"language":678,"meta":470,"style":470},"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) { \u002F\u002F 1 minute cooldown\n      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n    }\n  }\n\n  \u002F\u002F Process the submission...\n});\n",[240,1758,1759,1764,1769,1774,1778,1783,1788,1793,1798,1802,1806,1810,1815],{"__ignoreMap":470},[682,1760,1761],{"class":684,"line":685},[682,1762,1763],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[682,1765,1766],{"class":684,"line":471},[682,1767,1768],{},"  const userId = context.auth.uid;\n",[682,1770,1771],{"class":684,"line":476},[682,1772,1773],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[682,1775,1776],{"class":684,"line":701},[682,1777,829],{"emptyLinePlaceholder":510},[682,1779,1780],{"class":684,"line":707},[682,1781,1782],{},"  if (lastSubmission.exists) {\n",[682,1784,1785],{"class":684,"line":713},[682,1786,1787],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[682,1789,1790],{"class":684,"line":719},[682,1791,1792],{},"    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n",[682,1794,1795],{"class":684,"line":725},[682,1796,1797],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[682,1799,1800],{"class":684,"line":851},[682,1801,716],{},[682,1803,1804],{"class":684,"line":856},[682,1805,722],{},[682,1807,1808],{"class":684,"line":862},[682,1809,829],{"emptyLinePlaceholder":510},[682,1811,1812],{"class":684,"line":868},[682,1813,1814],{},"  \u002F\u002F Process the submission...\n",[682,1816,1817],{"class":684,"line":874},[682,1818,1081],{},[14,1820,1821],{},"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.",[31,1823,1825],{"id":1824},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,1827,1828,1831],{},[69,1829,606],{"href":1830},"\u002Fservice\u002Fflutter-development\u002F"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[141,1833,1834,1840,1846,1852,1858,1864],{},[144,1835,1836,1839],{},[17,1837,1838],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[144,1841,1842,1845],{},[17,1843,1844],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[144,1847,1848,1851],{},[17,1849,1850],{},"Comprehensive data validation"," in rules for every writable collection: type checking, field constraints, and immutability guards",[144,1853,1854,1857],{},[17,1855,1856],{},"Automated rules testing"," in CI\u002FCD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[144,1859,1860,1863],{},[17,1861,1862],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[144,1865,1866,1869,1870,1875],{},[17,1867,1868],{},"Security audits"," against the ",[69,1871,1874],{"href":1872,"rel":1873},"https:\u002F\u002Fowasp.org\u002Fwww-project-mobile-top-10\u002F",[73],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,1877,1878,1879,1882,1883,131],{},"App369 builds ",[69,1880,1881],{"href":1830},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[69,1884,1886],{"href":1885},"\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","Flutter security best practices",[31,1888,1890],{"id":1889},"faq","FAQ",[133,1892,1894],{"id":1893},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,1896,1897,1898,1902],{},"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 ",[69,1899,1901],{"href":596,"rel":1900},[73],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[133,1904,1906],{"id":1905},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,1908,1909],{},"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.",[133,1911,1913],{"id":1912},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,1915,1916,1917,1920,1921,1924,1925,1928],{},"Use Firebase Authentication custom claims. Set ",[240,1918,1919],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[240,1922,1923],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[240,1926,1927],{},"users\u002F{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.",[133,1930,1932],{"id":1931},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,1934,1935,1936,1940],{},"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 ",[69,1937,1939],{"href":620,"rel":1938},[73],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[1942,1943,1944],"style",{},"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":470,"searchDepth":471,"depth":471,"links":1946},[1947,1948,1949,1950,1951,1952,1953,1954,1955],{"id":610,"depth":471,"text":611},{"id":652,"depth":471,"text":653},{"id":794,"depth":471,"text":795},{"id":1015,"depth":471,"text":1016},{"id":1175,"depth":471,"text":1176},{"id":1372,"depth":471,"text":1373},{"id":1589,"depth":471,"text":1590},{"id":1824,"depth":471,"text":1825},{"id":1889,"depth":471,"text":1890,"children":1956},[1957,1958,1959,1960],{"id":1893,"depth":476,"text":1894},{"id":1905,"depth":476,"text":1906},{"id":1912,"depth":476,"text":1913},{"id":1931,"depth":476,"text":1932},"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":1964},{"text":1965,"minutes":1966,"time":1967,"words":1968},"11 min read",10.305,618300,2061,"\u002Fblog\u002Ffirebase-security-guide-2026","11 min",[1972,1973,1974],"supabase-permission-denied-new-tables-october-30-2026","ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":583,"description":1962},"blog\u002Ffirebase-security-guide-2026","firebase security rules, firestore security, firebase authentication, cloud functions, role-based access control, firebase guide","2026-10-12T12:00:00.000Z","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-security-guide-2026\u002F","ArpiaSkjZy0lmwYjm3PwPtDCC-iwUHgE18ym-joXZaA",{"id":1982,"title":1983,"author":1984,"body":1986,"category":482,"createdAt":1961,"description":2743,"disclosure":485,"extension":486,"faq":485,"howTo":485,"location":503,"meta":2744,"navigation":510,"ogImage":485,"path":2750,"primaryKeyword":485,"readingTime":2751,"related":485,"seo":2752,"sources":485,"stem":2753,"tags":2754,"takeaways":485,"updatedAt":1961,"url":2755,"__hash__":2756},"blog\u002Fblog\u002Fflutter-security-best-practices-2026.md","Flutter Security Best Practices: Protecting Your App in 2026",{"name":7,"social":1985},{"twitter":9},{"type":11,"value":1987,"toc":2727},[1988,1999,2005,2009,2018,2021,2028,2032,2051,2054,2117,2120,2143,2151,2155,2164,2167,2197,2200,2209,2212,2232,2239,2243,2251,2254,2263,2277,2306,2316,2319,2323,2326,2337,2343,2391,2394,2424,2428,2437,2468,2478,2481,2513,2520,2524,2527,2537,2585,2588,2616,2619,2623,2628,2669,2674,2676,2680,2693,2697,2703,2707,2713,2717,2725],[14,1989,1990,1993,1994,1998],{},[17,1991,1992],{},"Flutter security requires deliberate architecture decisions at every layer of the app",", from how credentials are stored on-device to how data moves across the network. The ",[69,1995,1997],{"href":1872,"rel":1996},[73],"OWASP Mobile Top 10 (2024)"," identifies insecure data storage, insufficient cryptography, and inadequate authentication as the most exploited vulnerabilities in production mobile apps. Flutter's single-codebase model means a security flaw ships to both iOS and Android simultaneously, doubling the exposure surface.",[14,2000,2001,2002,2004],{},"This guide covers the specific techniques, packages, and build flags that ",[69,2003,606],{"href":1830}," uses to harden Flutter applications before they reach production.",[31,2006,2008],{"id":2007},"why-flutter-apps-need-a-security-first-approach","Why Flutter Apps Need a Security-First Approach",[14,2010,2011,2012,2017],{},"Flutter compiles to native ARM code and does not expose raw source the way JavaScript-based frameworks do, but this does not make it secure by default. According to ",[69,2013,2016],{"href":2014,"rel":2015},"https:\u002F\u002Fdocs.flutter.dev\u002Fsecurity",[73],"Flutter's official security documentation",", the framework delegates sensitive operations, keychain access, certificate validation, biometric enrollment, to platform-level APIs. Developers are responsible for invoking those APIs correctly.",[14,2019,2020],{},"A misconfigured Firebase instance, an API key committed to version control, or a missing SSL pin can each independently compromise an entire user base. Security must be addressed in code architecture, not bolted on after feature development is complete.",[14,2022,2023,2024,2027],{},"The ",[69,2025,1997],{"href":1872,"rel":2026},[73]," provides the standard framework for assessing mobile app risk. The top three categories, Improper Credential Usage, Inadequate Supply Chain Security, and Insecure Authentication\u002FAuthorization, are all directly relevant to Flutter applications.",[31,2029,2031],{"id":2030},"secure-data-storage-with-flutter_secure_storage","Secure Data Storage with flutter_secure_storage",[14,2033,2034,2035,2038,2039,2046,2047,131],{},"Storing sensitive data in ",[240,2036,2037],{},"SharedPreferences"," or plain SQLite databases exposes it to any process with device-level access. The ",[69,2040,2043],{"href":2041,"rel":2042},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fflutter_secure_storage",[73],[240,2044,2045],{},"flutter_secure_storage"," package solves this by writing to the iOS Keychain and Android EncryptedSharedPreferences (backed by the Android Keystore system), as documented on ",[69,2048,2050],{"href":2041,"rel":2049},[73],"pub.dev",[14,2052,2053],{},"Here is the correct pattern for storing and retrieving tokens:",[674,2055,2059],{"className":2056,"code":2057,"language":2058,"meta":470,"style":470},"language-dart shiki shiki-themes github-light github-dark","import 'package:flutter_secure_storage\u002Fflutter_secure_storage.dart';\n\nfinal storage = FlutterSecureStorage();\n\n\u002F\u002F Store a token\nawait storage.write(key: 'auth_token', value: tokenValue);\n\n\u002F\u002F Retrieve a token\nfinal token = await storage.read(key: 'auth_token');\n\n\u002F\u002F Delete on logout\nawait storage.delete(key: 'auth_token');\n","dart",[240,2060,2061,2066,2070,2075,2079,2084,2089,2093,2098,2103,2107,2112],{"__ignoreMap":470},[682,2062,2063],{"class":684,"line":685},[682,2064,2065],{},"import 'package:flutter_secure_storage\u002Fflutter_secure_storage.dart';\n",[682,2067,2068],{"class":684,"line":471},[682,2069,829],{"emptyLinePlaceholder":510},[682,2071,2072],{"class":684,"line":476},[682,2073,2074],{},"final storage = FlutterSecureStorage();\n",[682,2076,2077],{"class":684,"line":701},[682,2078,829],{"emptyLinePlaceholder":510},[682,2080,2081],{"class":684,"line":707},[682,2082,2083],{},"\u002F\u002F Store a token\n",[682,2085,2086],{"class":684,"line":713},[682,2087,2088],{},"await storage.write(key: 'auth_token', value: tokenValue);\n",[682,2090,2091],{"class":684,"line":719},[682,2092,829],{"emptyLinePlaceholder":510},[682,2094,2095],{"class":684,"line":725},[682,2096,2097],{},"\u002F\u002F Retrieve a token\n",[682,2099,2100],{"class":684,"line":851},[682,2101,2102],{},"final token = await storage.read(key: 'auth_token');\n",[682,2104,2105],{"class":684,"line":856},[682,2106,829],{"emptyLinePlaceholder":510},[682,2108,2109],{"class":684,"line":862},[682,2110,2111],{},"\u002F\u002F Delete on logout\n",[682,2113,2114],{"class":684,"line":868},[682,2115,2116],{},"await storage.delete(key: 'auth_token');\n",[14,2118,2119],{},"Key implementation rules:",[141,2121,2122,2128,2137],{},[144,2123,2124,2127],{},[17,2125,2126],{},"Never store tokens in SharedPreferences."," SharedPreferences writes to an unencrypted XML file on Android.",[144,2129,2130,955,2133,2136],{},[17,2131,2132],{},"Set access control on iOS.",[240,2134,2135],{},"IOSOptions(accessibility: KeychainAccessibility.first_unlock)"," to prevent access before the first device unlock.",[144,2138,2139,2142],{},[17,2140,2141],{},"Wipe storage on app uninstall detection."," On Android, Keystore entries persist after uninstall. Check for a sentinel value on first launch and clear stale credentials.",[14,2144,617,2145,2150],{},[69,2146,2149],{"href":2147,"rel":2148},"https:\u002F\u002Ftouchlane.com\u002F5-overlooked-flutter-security-vulnerabilities-and-how-to-address-them\u002F",[73],"Touchlane's security analysis",", insecure local storage remains one of the five most overlooked Flutter vulnerabilities in production apps.",[31,2152,2154],{"id":2153},"api-key-protection-and-environment-variables","API Key Protection and Environment Variables",[14,2156,2157,2158,2163],{},"Hardcoding API keys in Dart source files is the most common security mistake in Flutter projects. Even with code obfuscation, string literals can be extracted from compiled binaries using basic reverse-engineering tools. According to ",[69,2159,2162],{"href":2160,"rel":2161},"https:\u002F\u002Fhackernoon.com\u002F10-best-practices-for-securing-your-flutter-mobile-app-in-2025",[73],"HackerNoon's Flutter security guide",", exposed API keys account for a significant share of reported mobile app breaches.",[14,2165,2166],{},"Flutter provides a built-in mechanism for compile-time variable injection:",[674,2168,2170],{"className":1398,"code":2169,"language":1400,"meta":470,"style":470},"flutter build apk --dart-define=API_KEY=your_key_here\nflutter build ios --dart-define=API_KEY=your_key_here\n",[240,2171,2172,2186],{"__ignoreMap":470},[682,2173,2174,2177,2180,2183],{"class":684,"line":685},[682,2175,2176],{"class":1407},"flutter",[682,2178,2179],{"class":1411}," build",[682,2181,2182],{"class":1411}," apk",[682,2184,2185],{"class":1415}," --dart-define=API_KEY=your_key_here\n",[682,2187,2188,2190,2192,2195],{"class":684,"line":471},[682,2189,2176],{"class":1407},[682,2191,2179],{"class":1411},[682,2193,2194],{"class":1411}," ios",[682,2196,2185],{"class":1415},[14,2198,2199],{},"Access the value in Dart:",[674,2201,2203],{"className":2056,"code":2202,"language":2058,"meta":470,"style":470},"const apiKey = String.fromEnvironment('API_KEY');\n",[240,2204,2205],{"__ignoreMap":470},[682,2206,2207],{"class":684,"line":685},[682,2208,2202],{},[14,2210,2211],{},"This approach keeps keys out of version control and out of the source tree entirely. For additional protection:",[141,2213,2214,2220,2226],{},[144,2215,2216,2219],{},[17,2217,2218],{},"Use a backend proxy."," Route API calls through your own server so third-party keys never reach the client.",[144,2221,2222,2225],{},[17,2223,2224],{},"Scope keys narrowly."," Use API key restrictions (IP allowlists, bundle ID restrictions) provided by services like Google Cloud and Firebase.",[144,2227,2228,2231],{},[17,2229,2230],{},"Rotate keys on a schedule."," Automate key rotation in CI\u002FCD so compromised keys have a limited exposure window.",[14,2233,2234,2235,2238],{},"For projects that use multiple environments (development, staging, production), maintain separate ",[240,2236,2237],{},".env"," files and inject them via the build pipeline, never commit them to Git.",[31,2240,2242],{"id":2241},"authentication-oauth-20-biometrics-and-mfa","Authentication: OAuth 2.0, Biometrics, and MFA",[14,2244,2245,2246,131],{},"Proper authentication combines secure token management with multi-factor verification. OAuth 2.0 and OpenID Connect remain the standard protocols for mobile authentication in 2026, as documented by the ",[69,2247,2250],{"href":2248,"rel":2249},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc8252",[73],"OAuth 2.0 for Mobile and Native Apps RFC (RFC 8252)",[14,2252,2253],{},"Implementation requirements for Flutter apps:",[14,2255,2256,2259,2260,2262],{},[17,2257,2258],{},"Short-lived access tokens with refresh tokens."," Access tokens should expire within 15-60 minutes. Use refresh tokens stored in ",[240,2261,2045],{}," to obtain new access tokens without requiring re-authentication. This limits the damage window if a token is intercepted.",[14,2264,2265,2268,2269,2276],{},[17,2266,2267],{},"Biometric authentication as a second factor."," The ",[69,2270,2273],{"href":2271,"rel":2272},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Flocal_auth",[73],[240,2274,2275],{},"local_auth"," package provides fingerprint and face recognition support on both platforms. Use it to gate sensitive operations (payments, profile changes) rather than as a standalone login method.",[674,2278,2280],{"className":2056,"code":2279,"language":2058,"meta":470,"style":470},"final localAuth = LocalAuthentication();\nfinal authenticated = await localAuth.authenticate(\n  localizedReason: 'Verify your identity to continue',\n  options: const AuthenticationOptions(biometricOnly: true),\n);\n",[240,2281,2282,2287,2292,2297,2302],{"__ignoreMap":470},[682,2283,2284],{"class":684,"line":685},[682,2285,2286],{},"final localAuth = LocalAuthentication();\n",[682,2288,2289],{"class":684,"line":471},[682,2290,2291],{},"final authenticated = await localAuth.authenticate(\n",[682,2293,2294],{"class":684,"line":476},[682,2295,2296],{},"  localizedReason: 'Verify your identity to continue',\n",[682,2298,2299],{"class":684,"line":701},[682,2300,2301],{},"  options: const AuthenticationOptions(biometricOnly: true),\n",[682,2303,2304],{"class":684,"line":707},[682,2305,1490],{},[14,2307,2308,2311,2312,2315],{},[17,2309,2310],{},"Multi-factor authentication (MFA)."," Combine password-based login with either biometric verification or a TOTP (Time-Based One-Time Password) code. Firebase Authentication supports MFA enrollment natively through the ",[240,2313,2314],{},"firebase_auth"," package.",[14,2317,2318],{},"Never store passwords locally, even in encrypted form. Delegate credential verification entirely to the authentication server.",[31,2320,2322],{"id":2321},"network-security-ssl-pinning-and-certificate-validation","Network Security: SSL Pinning and Certificate Validation",[14,2324,2325],{},"SSL\u002FTLS encryption protects data in transit, but standard HTTPS alone does not prevent man-in-the-middle (MITM) attacks on compromised networks. SSL pinning ties your app to a specific server certificate or public key, rejecting connections even if a trusted certificate authority has been compromised.",[14,2327,617,2328,2333,2334,131],{},[69,2329,2332],{"href":2330,"rel":2331},"https:\u002F\u002Fsolguruz.com\u002Fblog\u002Fflutter-security-best-practices\u002F",[73],"SolGuruz's Flutter security analysis",", SSL pinning is one of the most effective defenses against MITM attacks on ",[69,2335,2336],{"href":605},"mobile applications",[14,2338,2339,2340,1428],{},"Implementation with the ",[240,2341,2342],{},"http_certificate_pinning",[674,2344,2346],{"className":2056,"code":2345,"language":2058,"meta":470,"style":470},"import 'package:http_certificate_pinning\u002Fhttp_certificate_pinning.dart';\n\nfinal result = await HttpCertificatePinning.check(\n  serverURL: 'https:\u002F\u002Fapi.yourapp.com',\n  headerHttp: {},\n  sha: SHA.SHA256,\n  allowedSHAFingerprints: ['YOUR_CERTIFICATE_SHA256_FINGERPRINT'],\n  timeout: 50,\n);\n",[240,2347,2348,2353,2357,2362,2367,2372,2377,2382,2387],{"__ignoreMap":470},[682,2349,2350],{"class":684,"line":685},[682,2351,2352],{},"import 'package:http_certificate_pinning\u002Fhttp_certificate_pinning.dart';\n",[682,2354,2355],{"class":684,"line":471},[682,2356,829],{"emptyLinePlaceholder":510},[682,2358,2359],{"class":684,"line":476},[682,2360,2361],{},"final result = await HttpCertificatePinning.check(\n",[682,2363,2364],{"class":684,"line":701},[682,2365,2366],{},"  serverURL: 'https:\u002F\u002Fapi.yourapp.com',\n",[682,2368,2369],{"class":684,"line":707},[682,2370,2371],{},"  headerHttp: {},\n",[682,2373,2374],{"class":684,"line":713},[682,2375,2376],{},"  sha: SHA.SHA256,\n",[682,2378,2379],{"class":684,"line":719},[682,2380,2381],{},"  allowedSHAFingerprints: ['YOUR_CERTIFICATE_SHA256_FINGERPRINT'],\n",[682,2383,2384],{"class":684,"line":725},[682,2385,2386],{},"  timeout: 50,\n",[682,2388,2389],{"class":684,"line":851},[682,2390,1490],{},[14,2392,2393],{},"Additional network security measures:",[141,2395,2396,2412,2418],{},[144,2397,2398,2401,2402,1551,2405,2408,2409,131],{},[17,2399,2400],{},"Disable cleartext traffic."," On Android, set ",[240,2403,2404],{},"android:usesCleartextTraffic=\"false\"",[240,2406,2407],{},"AndroidManifest.xml",". On iOS, enforce App Transport Security (ATS) in ",[240,2410,2411],{},"Info.plist",[144,2413,2414,2417],{},[17,2415,2416],{},"Validate all server responses."," Check HTTP status codes and response structures before processing data. Reject unexpected content types.",[144,2419,2420,2423],{},[17,2421,2422],{},"Implement request signing."," Add HMAC signatures to API requests so the server can verify request authenticity and detect tampering.",[31,2425,2427],{"id":2426},"code-obfuscation-and-reverse-engineering-prevention","Code Obfuscation and Reverse Engineering Prevention",[14,2429,2430,2431,2436],{},"Flutter's Dart code compiles to native ARM binaries, which provides more protection than interpreted code but does not prevent determined reverse engineering. The Flutter toolchain includes built-in obfuscation support, as documented on ",[69,2432,2435],{"href":2433,"rel":2434},"https:\u002F\u002Fdocs.flutter.dev\u002Fdeployment\u002Fobfuscate",[73],"Flutter's official docs",":",[674,2438,2440],{"className":1398,"code":2439,"language":1400,"meta":470,"style":470},"flutter build apk --obfuscate --split-debug-info=debug-info\u002F\nflutter build ios --obfuscate --split-debug-info=debug-info\u002F\n",[240,2441,2442,2456],{"__ignoreMap":470},[682,2443,2444,2446,2448,2450,2453],{"class":684,"line":685},[682,2445,2176],{"class":1407},[682,2447,2179],{"class":1411},[682,2449,2182],{"class":1411},[682,2451,2452],{"class":1415}," --obfuscate",[682,2454,2455],{"class":1415}," --split-debug-info=debug-info\u002F\n",[682,2457,2458,2460,2462,2464,2466],{"class":684,"line":471},[682,2459,2176],{"class":1407},[682,2461,2179],{"class":1411},[682,2463,2194],{"class":1411},[682,2465,2452],{"class":1415},[682,2467,2455],{"class":1415},[14,2469,2023,2470,2473,2474,2477],{},[240,2471,2472],{},"--obfuscate"," flag renames classes, methods, and fields to meaningless identifiers. The ",[240,2475,2476],{},"--split-debug-info"," flag extracts debug symbols into a separate directory, which must be stored securely for crash report symbolication but never shipped with the app.",[14,2479,2480],{},"Additional anti-reverse-engineering measures:",[141,2482,2483,2497,2503],{},[144,2484,2485,2488,2489,2496],{},[17,2486,2487],{},"Detect rooted\u002Fjailbroken devices."," Use the ",[69,2490,2493],{"href":2491,"rel":2492},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fflutter_jailbreak_detection",[73],[240,2494,2495],{},"flutter_jailbreak_detection"," package to identify compromised devices and restrict functionality accordingly.",[144,2498,2499,2502],{},[17,2500,2501],{},"Implement runtime integrity checks."," Verify that the app binary has not been modified by checking code signatures at startup.",[144,2504,2505,2508,2509,2512],{},[17,2506,2507],{},"Use ProGuard\u002FR8 on Android."," Enable minification in ",[240,2510,2511],{},"build.gradle"," to shrink and obfuscate the Java\u002FKotlin layer of your Flutter app.",[14,2514,2515,2516,2519],{},"Store the ",[240,2517,2518],{},"debug-info\u002F"," directory in a secure, access-controlled location. These files are required to decode stack traces from production crash reports.",[31,2521,2523],{"id":2522},"encryption-aes-256-and-rsa-implementation","Encryption: AES-256 and RSA Implementation",[14,2525,2526],{},"Data that must be stored or transmitted in encrypted form requires proper cryptographic implementation. AES-256 is the standard for symmetric encryption of data at rest, while RSA handles asymmetric key exchange.",[14,2528,2023,2529,2536],{},[69,2530,2533],{"href":2531,"rel":2532},"https:\u002F\u002Fpub.dev\u002Fpackages\u002Fencrypt",[73],[240,2534,2535],{},"encrypt"," package provides both AES and RSA implementations for Flutter:",[674,2538,2540],{"className":2056,"code":2539,"language":2058,"meta":470,"style":470},"import 'package:encrypt\u002Fencrypt.dart';\n\n\u002F\u002F AES-256 encryption\nfinal key = Key.fromSecureRandom(32); \u002F\u002F 256-bit key\nfinal iv = IV.fromSecureRandom(16);   \u002F\u002F Initialization vector\nfinal encrypter = Encrypter(AES(key, mode: AESMode.cbc));\n\nfinal encrypted = encrypter.encrypt('sensitive data', iv: iv);\nfinal decrypted = encrypter.decrypt(encrypted, iv: iv);\n",[240,2541,2542,2547,2551,2556,2561,2566,2571,2575,2580],{"__ignoreMap":470},[682,2543,2544],{"class":684,"line":685},[682,2545,2546],{},"import 'package:encrypt\u002Fencrypt.dart';\n",[682,2548,2549],{"class":684,"line":471},[682,2550,829],{"emptyLinePlaceholder":510},[682,2552,2553],{"class":684,"line":476},[682,2554,2555],{},"\u002F\u002F AES-256 encryption\n",[682,2557,2558],{"class":684,"line":701},[682,2559,2560],{},"final key = Key.fromSecureRandom(32); \u002F\u002F 256-bit key\n",[682,2562,2563],{"class":684,"line":707},[682,2564,2565],{},"final iv = IV.fromSecureRandom(16);   \u002F\u002F Initialization vector\n",[682,2567,2568],{"class":684,"line":713},[682,2569,2570],{},"final encrypter = Encrypter(AES(key, mode: AESMode.cbc));\n",[682,2572,2573],{"class":684,"line":719},[682,2574,829],{"emptyLinePlaceholder":510},[682,2576,2577],{"class":684,"line":725},[682,2578,2579],{},"final encrypted = encrypter.encrypt('sensitive data', iv: iv);\n",[682,2581,2582],{"class":684,"line":851},[682,2583,2584],{},"final decrypted = encrypter.decrypt(encrypted, iv: iv);\n",[14,2586,2587],{},"Implementation rules:",[141,2589,2590,2596,2602,2610],{},[144,2591,2592,2595],{},[17,2593,2594],{},"Never reuse initialization vectors (IVs)."," Generate a new random IV for each encryption operation. Reusing IVs with the same key allows attackers to derive plaintext.",[144,2597,2598,2601],{},[17,2599,2600],{},"Use AES-256-CBC or AES-256-GCM."," GCM mode provides both encryption and authentication, detecting tampering automatically. Prefer GCM when available.",[144,2603,2604,955,2607,2609],{},[17,2605,2606],{},"Store encryption keys in the platform keystore.",[240,2608,2045],{}," to persist keys, never embed them in source code or configuration files.",[144,2611,2612,2615],{},[17,2613,2614],{},"Use RSA for key exchange only."," RSA is computationally expensive and has message size limits. Use it to encrypt and exchange AES session keys, then use AES for the actual data encryption.",[14,2617,2618],{},"For applications handling financial or health data, consider using the platform's native cryptographic APIs through method channels for FIPS 140-2 compliance.",[31,2620,2622],{"id":2621},"how-app369-secures-flutter-applications","How App369 Secures Flutter Applications",[14,2624,2625,2627],{},[69,2626,606],{"href":1830}," applies these security practices as standard procedure across every Flutter project:",[141,2629,2630,2636,2642,2648,2654,2660],{},[144,2631,2632,2635],{},[17,2633,2634],{},"Security architecture review"," before development begins, mapping data flows and identifying threat surfaces specific to the application's domain",[144,2637,2638,2641],{},[17,2639,2640],{},"Automated dependency scanning"," in CI\u002FCD pipelines to flag packages with known vulnerabilities before they reach production",[144,2643,2644,2647],{},[17,2645,2646],{},"Mandatory code obfuscation"," on all release builds, with debug symbols stored in access-controlled cloud storage",[144,2649,2650,2653],{},[17,2651,2652],{},"SSL pinning"," configured for all API endpoints, with certificate rotation procedures documented in runbooks",[144,2655,2656,2659],{},[17,2657,2658],{},"Penetration testing"," on release candidates, using both automated tools and manual review against the OWASP Mobile Top 10",[144,2661,2662,2665,2666,2668],{},[17,2663,2664],{},"Secure storage enforcement",": all tokens, keys, and user credentials stored exclusively through ",[240,2667,2045],{},", never in SharedPreferences or plain databases",[14,2670,1878,2671,2673],{},[69,2672,2336],{"href":605}," with security requirements ranging from startup MVPs to enterprise applications handling regulated financial data.",[31,2675,1890],{"id":1889},[133,2677,2679],{"id":2678},"what-is-the-most-common-flutter-security-vulnerability","What is the most common Flutter security vulnerability?",[14,2681,2682,2683,2685,2686,2688,2689,2692],{},"Insecure data storage is the most common vulnerability. Developers frequently store authentication tokens, API keys, and user data in ",[240,2684,2037],{}," or unencrypted SQLite databases instead of using ",[240,2687,2045],{},". This exposes sensitive data to any process with root access on the device. The ",[69,2690,1997],{"href":1872,"rel":2691},[73]," lists Improper Credential Usage as the top mobile security risk.",[133,2694,2696],{"id":2695},"does-flutters-compilation-to-native-code-make-it-secure","Does Flutter's compilation to native code make it secure?",[14,2698,2699,2700,2702],{},"No. Flutter compiles Dart to native ARM code, which makes casual reverse engineering harder than with JavaScript frameworks, but it does not prevent determined attackers from decompiling the binary. String literals (including hardcoded API keys) remain extractable. Code obfuscation with ",[240,2701,2472],{}," and proper key management are still required.",[133,2704,2706],{"id":2705},"how-do-i-protect-api-keys-in-a-flutter-app","How do I protect API keys in a Flutter app?",[14,2708,987,2709,2712],{},[240,2710,2711],{},"--dart-define"," to inject keys at compile time instead of hardcoding them in source files. For stronger protection, route API calls through a backend proxy server so third-party keys never exist on the client device. Apply API key restrictions (bundle ID, IP allowlists) on the service provider side, and rotate keys regularly through your CI\u002FCD pipeline.",[133,2714,2716],{"id":2715},"is-ssl-pinning-necessary-for-flutter-apps","Is SSL pinning necessary for Flutter apps?",[14,2718,2719,2720,2724],{},"Yes, if your app transmits sensitive data. Standard HTTPS validates that the server has a certificate signed by a trusted authority, but it does not prevent MITM attacks using a rogue CA certificate installed on the device. SSL pinning binds your app to specific certificate fingerprints, rejecting all others. According to ",[69,2721,2723],{"href":2330,"rel":2722},[73],"SolGuruz",", SSL pinning is one of the most effective network security measures for mobile applications.",[1942,2726,1944],{},{"title":470,"searchDepth":471,"depth":471,"links":2728},[2729,2730,2731,2732,2733,2734,2735,2736,2737],{"id":2007,"depth":471,"text":2008},{"id":2030,"depth":471,"text":2031},{"id":2153,"depth":471,"text":2154},{"id":2241,"depth":471,"text":2242},{"id":2321,"depth":471,"text":2322},{"id":2426,"depth":471,"text":2427},{"id":2522,"depth":471,"text":2523},{"id":2621,"depth":471,"text":2622},{"id":1889,"depth":471,"text":1890,"children":2738},[2739,2740,2741,2742],{"id":2678,"depth":476,"text":2679},{"id":2695,"depth":476,"text":2696},{"id":2705,"depth":476,"text":2706},{"id":2715,"depth":476,"text":2716},"Flutter security best practices for 2026: secure storage, API key protection, SSL pinning, code obfuscation, and AES-256 encryption explained in detail.",{"readingTimeStats":2745},{"text":2746,"minutes":2747,"time":2748,"words":2749},"8 min read",7.865,471900,1573,"\u002Fblog\u002Fflutter-security-best-practices-2026","10 min",{"title":1983,"description":2743},"blog\u002Fflutter-security-best-practices-2026","flutter security, mobile app security, flutter secure storage, SSL pinning, code obfuscation, API key protection","https:\u002F\u002Fapp369.com\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","pDFoEerNjIoiNqqSEcqzWaLBF2Y89rUiyclWyHz3YTU",{"id":2758,"title":2759,"author":2760,"body":2762,"category":482,"createdAt":3442,"description":3443,"disclosure":485,"extension":486,"faq":3444,"howTo":485,"location":503,"meta":3460,"navigation":510,"ogImage":485,"path":3465,"primaryKeyword":3466,"readingTime":3467,"related":3468,"seo":3471,"sources":3472,"stem":3517,"tags":3518,"takeaways":3519,"updatedAt":3442,"url":3525,"__hash__":3526},"blog\u002Fblog\u002Ffirebase-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":2761},{"twitter":9},{"type":11,"value":2763,"toc":3433},[2764,2770,2773,2777,2817,2820,2824,2827,2848,2851,2868,2871,2875,3003,3006,3008,3043,3046,3050,3053,3083,3168,3193,3269,3272,3345,3348,3352,3378,3381,3406,3409,3411,3414,3430],[14,2765,2766,2769],{},[17,2767,2768],{},"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,2771,2772],{},"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.",[31,2774,2776],{"id":2775},"what-deep-linking-means-in-plain-words","What deep linking means, in plain words",[141,2778,2779,2785,2795,2805,2811],{},[144,2780,2781,2784],{},[17,2782,2783],{},"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.",[144,2786,2787,2790,2791,2794],{},[17,2788,2789],{},"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 ",[240,2792,2793],{},"apple-app-site-association",", to confirm the link belongs to your app.",[144,2796,2797,2800,2801,2804],{},[17,2798,2799],{},"Android App Links."," Google's version. Same idea. Android checks a file called ",[240,2802,2803],{},"assetlinks.json"," on your website.",[144,2806,2807,2810],{},[17,2808,2809],{},"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.",[144,2812,2813,2816],{},[17,2814,2815],{},"Click analytics."," Counts of who clicked which link. Dynamic Links had some. Google's migration guide offers no replacement for it.",[14,2818,2819],{},"Marketers mostly care about the last two. Developers mostly care about the first three.",[31,2821,2823],{"id":2822},"what-happened-to-firebase-dynamic-links","What happened to Firebase Dynamic Links",[14,2825,2826],{},"Google's own FAQ says Dynamic Links shut down on August 25, 2025. This is what that means for old links:",[141,2828,2829,2832,2835,2842,2845],{},[144,2830,2831],{},"Every clicked link returns an HTTP 404 to the user.",[144,2833,2834],{},"Short link creation and link stats API calls return 400 or 403 errors.",[144,2836,2837,2838,2841],{},"Existing ",[240,2839,2840],{},".page.link"," domains cannot be kept or transferred. Domain and link data is marked for deletion on the shutdown date.",[144,2843,2844],{},"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.",[144,2846,2847],{},"Email link sign-in, password reset and email verification on mobile needed updated SDKs. Google has separate guides for iOS and Android.",[14,2849,2850],{},"Google's own advice, in its FAQ:",[314,2852,2853,2856,2859,2862],{},[144,2854,2855],{},"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.",[144,2857,2858],{},"If you only need to open an installed app, use Universal Links and App Links. The official migration guide covers this path.",[144,2860,2861],{},"If you no longer need the links, review them, delete the URL prefixes and remove the SDK.",[144,2863,2864,2865,131],{},"Export your link metadata, and if you keep a custom domain, update your rewrite rules in ",[240,2866,2867],{},"firebase.json",[14,2869,2870],{},"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.",[31,2872,2874],{"id":2873},"what-to-use-now","What to use now",[36,2876,2877,2892],{},[39,2878,2879],{},[42,2880,2881,2883,2886,2889],{},[45,2882,177],{},[45,2884,2885],{},"Deferred deep linking",[45,2887,2888],{},"Click analytics",[45,2890,2891],{},"Price (checked October 17, 2026)",[55,2893,2894,2909,2929,2949,2969,2988],{},[42,2895,2896,2901,2904,2906],{},[60,2897,2898],{},[17,2899,2900],{},"Universal Links + App Links on your own domain",[60,2902,2903],{},"No",[60,2905,2903],{},[60,2907,2908],{},"Free. You pay for your domain and hosting.",[42,2910,2911,2916,2919,2921],{},[60,2912,2913],{},[17,2914,2915],{},"Branch",[60,2917,2918],{},"Yes",[60,2920,2918],{},[60,2922,2923,2924],{},"Basics plan: free trial, no price shown. Essentials and Enterprise: \"Request a demo.\" See ",[69,2925,2928],{"href":2926,"rel":2927},"https:\u002F\u002Fwww.branch.io\u002Fpricing\u002F",[73],"branch.io\u002Fpricing",[42,2930,2931,2936,2939,2941],{},[60,2932,2933],{},[17,2934,2935],{},"AppsFlyer (OneLink)",[60,2937,2938],{},"Yes, from the Standard plan (Zero is free but has no deferred linking)",[60,2940,2918],{},[60,2942,2943,2944],{},"Deep Linking plans: Zero is labelled \"Free.\" Standard, Advanced and Premium show \"Custom pricing\" with \"Request a demo.\" See ",[69,2945,2948],{"href":2946,"rel":2947},"https:\u002F\u002Fwww.appsflyer.com\u002Fpricing\u002Fdeep-linking",[73],"appsflyer.com\u002Fpricing\u002Fdeep-linking",[42,2950,2951,2956,2958,2960],{},[60,2952,2953],{},[17,2954,2955],{},"Adjust",[60,2957,2918],{},[60,2959,2918],{},[60,2961,2962,2963,2968],{},"See ",[69,2964,2967],{"href":2965,"rel":2966},"https:\u002F\u002Fwww.adjust.com\u002Fpricing\u002F",[73],"adjust.com\u002Fpricing",". Its pricing page would not load for me on October 17, so I am not quoting a number.",[42,2970,2971,2976,2978,2980],{},[60,2972,2973],{},[17,2974,2975],{},"Airbridge",[60,2977,2918],{},[60,2979,2918],{},[60,2981,2982,2983],{},"Core plan: $40+ a month after a 30-day free trial, deep linking included. Growth plan: custom. See ",[69,2984,2987],{"href":2985,"rel":2986},"https:\u002F\u002Fwww.airbridge.io\u002Fpricing",[73],"airbridge.io\u002Fpricing",[42,2989,2990,2995,2998,3000],{},[60,2991,2992],{},[17,2993,2994],{},"Bitly, Kochava, Singular",[60,2996,2997],{},"Check each vendor",[60,2999,2997],{},[60,3001,3002],{},"Named in Google's FAQ. Prices not checked here, so see each vendor's pricing page.",[14,3004,3005],{},"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.",[31,3007,349],{"id":348},[314,3009,3010,3016,3022,3028,3034],{},[144,3011,3012,3015],{},[17,3013,3014],{},"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.",[144,3017,3018,3021],{},[17,3019,3020],{},"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.",[144,3023,3024,3027],{},[17,3025,3026],{},"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.",[144,3029,3030,3033],{},[17,3031,3032],{},"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.",[144,3035,3036,3039,3040,3042],{},[17,3037,3038],{},"You have old Dynamic Links printed or published."," Links on a ",[240,3041,2840],{}," 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,3044,3045],{},"If you are unsure, start with option 1. You can add a provider later without changing your domain files.",[31,3047,3049],{"id":3048},"set-up-universal-links-and-app-links-on-your-own-domain","Set up Universal Links and App Links on your own domain",[14,3051,3052],{},"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.",[314,3054,3055,3069],{},[144,3056,3057,3060,3061,3064,3065,3068],{},[17,3058,3059],{},"Pick the links."," Decide which paths open the app, for example ",[240,3062,3063],{},"\u002Finvite\u002F*"," or ",[240,3066,3067],{},"\u002Finvoice\u002F*",". Keep the list short.",[144,3070,3071,3074,3075,3078,3079,3082],{},[17,3072,3073],{},"Create the iOS file."," Host it at ",[240,3076,3077],{},"https:\u002F\u002Fyourdomain.com\u002F.well-known\u002Fapple-app-site-association",". The name has no ",[240,3080,3081],{},".json"," extension. Apple's docs say to host it over HTTPS with a valid certificate and no redirects. A minimal version looks like this:",[674,3084,3088],{"className":3085,"code":3086,"language":3087,"meta":470,"style":470},"language-json shiki shiki-themes github-light github-dark","{\n  \"applinks\": {\n    \"details\": [\n      {\n        \"appIDs\": [\"TEAMID.com.example.app\"],\n        \"components\": [{ \"\u002F\": \"\u002Finvite\u002F*\" }]\n      }\n    ]\n  }\n}\n","json",[240,3089,3090,3096,3104,3112,3117,3131,3150,3155,3160,3164],{"__ignoreMap":470},[682,3091,3092],{"class":684,"line":685},[682,3093,3095],{"class":3094},"sVt8B","{\n",[682,3097,3098,3101],{"class":684,"line":471},[682,3099,3100],{"class":1415},"  \"applinks\"",[682,3102,3103],{"class":3094},": {\n",[682,3105,3106,3109],{"class":684,"line":476},[682,3107,3108],{"class":1415},"    \"details\"",[682,3110,3111],{"class":3094},": [\n",[682,3113,3114],{"class":684,"line":701},[682,3115,3116],{"class":3094},"      {\n",[682,3118,3119,3122,3125,3128],{"class":684,"line":707},[682,3120,3121],{"class":1415},"        \"appIDs\"",[682,3123,3124],{"class":3094},": [",[682,3126,3127],{"class":1411},"\"TEAMID.com.example.app\"",[682,3129,3130],{"class":3094},"],\n",[682,3132,3133,3136,3139,3142,3144,3147],{"class":684,"line":713},[682,3134,3135],{"class":1415},"        \"components\"",[682,3137,3138],{"class":3094},": [{ ",[682,3140,3141],{"class":1415},"\"\u002F\"",[682,3143,1334],{"class":3094},[682,3145,3146],{"class":1411},"\"\u002Finvite\u002F*\"",[682,3148,3149],{"class":3094}," }]\n",[682,3151,3152],{"class":684,"line":719},[682,3153,3154],{"class":3094},"      }\n",[682,3156,3157],{"class":684,"line":725},[682,3158,3159],{"class":3094},"    ]\n",[682,3161,3162],{"class":684,"line":851},[682,3163,722],{"class":3094},[682,3165,3166],{"class":684,"line":856},[682,3167,728],{"class":3094},[314,3169,3170,3180],{"start":476},[144,3171,3172,3175,3176,3179],{},[17,3173,3174],{},"Add the iOS entitlement."," In Xcode, open Signing & Capabilities, add Associated Domains, and add ",[240,3177,3178],{},"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).",[144,3181,3182,3074,3185,3188,3189,3192],{},[17,3183,3184],{},"Create the Android file.",[240,3186,3187],{},"https:\u002F\u002Fyourdomain.com\u002F.well-known\u002Fassetlinks.json",", with content type ",[240,3190,3191],{},"application\u002Fjson"," and no redirects:",[674,3194,3196],{"className":3085,"code":3195,"language":3087,"meta":470,"style":470},"[{\n  \"relation\": [\"delegate_permission\u002Fcommon.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",[240,3197,3198,3203,3215,3222,3235,3247,3260,3264],{"__ignoreMap":470},[682,3199,3200],{"class":684,"line":685},[682,3201,3202],{"class":3094},"[{\n",[682,3204,3205,3208,3210,3213],{"class":684,"line":471},[682,3206,3207],{"class":1415},"  \"relation\"",[682,3209,3124],{"class":3094},[682,3211,3212],{"class":1411},"\"delegate_permission\u002Fcommon.handle_all_urls\"",[682,3214,3130],{"class":3094},[682,3216,3217,3220],{"class":684,"line":476},[682,3218,3219],{"class":1415},"  \"target\"",[682,3221,3103],{"class":3094},[682,3223,3224,3227,3229,3232],{"class":684,"line":701},[682,3225,3226],{"class":1415},"    \"namespace\"",[682,3228,1334],{"class":3094},[682,3230,3231],{"class":1411},"\"android_app\"",[682,3233,3234],{"class":3094},",\n",[682,3236,3237,3240,3242,3245],{"class":684,"line":707},[682,3238,3239],{"class":1415},"    \"package_name\"",[682,3241,1334],{"class":3094},[682,3243,3244],{"class":1411},"\"com.example.app\"",[682,3246,3234],{"class":3094},[682,3248,3249,3252,3254,3257],{"class":684,"line":713},[682,3250,3251],{"class":1415},"    \"sha256_cert_fingerprints\"",[682,3253,3124],{"class":3094},[682,3255,3256],{"class":1411},"\"AA:BB:...:FF\"",[682,3258,3259],{"class":3094},"]\n",[682,3261,3262],{"class":684,"line":719},[682,3263,722],{"class":3094},[682,3265,3266],{"class":684,"line":725},[682,3267,3268],{"class":3094},"}]\n",[14,3270,3271],{},"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.",[314,3273,3274,3305,3311],{"start":707},[144,3275,3276,3279,3280,3282,3283,3286,3287,3290,3291,3294,3295,407,3298,3301,3302,131],{},[17,3277,3278],{},"Add the Android intent filter."," In ",[240,3281,2407],{},", add an ",[240,3284,3285],{},"intent-filter"," with ",[240,3288,3289],{},"android:autoVerify=\"true\"",", the ",[240,3292,3293],{},"VIEW"," action, the ",[240,3296,3297],{},"DEFAULT",[240,3299,3300],{},"BROWSABLE"," categories, and ",[240,3303,3304],{},"\u003Cdata android:scheme=\"https\" android:host=\"yourdomain.com\" \u002F>",[144,3306,3307,3310],{},[17,3308,3309],{},"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.",[144,3312,3313,3316],{},[17,3314,3315],{},"Test before you ship.",[141,3317,3318,3325,3342],{},[144,3319,3320,3321,3324],{},"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 ",[240,3322,3323],{},"https:\u002F\u002Fapp-site-association.cdn-apple.com\u002Fa\u002Fv1\u002Fyourdomain.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.",[144,3326,3327,3328,3331,3332,3335,3336,3341],{},"Android: run ",[240,3329,3330],{},"adb shell pm get-app-links com.example.app"," and look for ",[240,3333,3334],{},"verified"," next to your domain. Google's ",[69,3337,3340],{"href":3338,"rel":3339},"https:\u002F\u002Fdevelopers.google.com\u002Fdigital-asset-links\u002Ftools\u002Fgenerator",[73],"Statement List Generator and Tester"," can generate and check the file.",[144,3343,3344],{},"I 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,3346,3347],{},"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.",[31,3349,3351],{"id":3350},"flutter-specifics","Flutter specifics",[14,3353,3354,3355,407,3360,3365,3366,3369,3370,407,3372,3375,3376,131],{},"Flutter has official guides for both platforms: ",[69,3356,3359],{"href":3357,"rel":3358},"https:\u002F\u002Fdocs.flutter.dev\u002Fcookbook\u002Fnavigation\u002Fset-up-universal-links",[73],"set up universal links",[69,3361,3364],{"href":3362,"rel":3363},"https:\u002F\u002Fdocs.flutter.dev\u002Fcookbook\u002Fnavigation\u002Fset-up-app-links",[73],"set up app links",". Per those pages, Flutter 3.27 and later turn deep linking on by default. Older versions need ",[240,3367,3368],{},"FlutterDeepLinkingEnabled"," set to YES in ",[240,3371,2411],{},[240,3373,3374],{},"flutter_deeplinking_enabled"," set to true in ",[240,3377,2407],{},[14,3379,3380],{},"Two ways to receive links:",[141,3382,3383,3393],{},[144,3384,3385,3388,3389,3392],{},[17,3386,3387],{},"go_router."," Flutter's built-in handling feeds incoming links to the ",[240,3390,3391],{},"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.",[144,3394,3395,2268,3398,3401,3402,3405],{},[17,3396,3397],{},"app_links.",[240,3399,3400],{},"app_links"," package (7.2.2 today) handles Android App Links, iOS Universal Links and custom schemes. You subscribe to ",[240,3403,3404],{},"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,3407,3408],{},"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.",[31,3410,455],{"id":454},[14,3412,3413],{},"App369 is Simon Dziak's studio. He builds and runs Workspace369, a production Flutter and Firebase SaaS.",[141,3415,3416,3422],{},[144,3417,3418,3419,3421],{},"Links open the wrong screen, or the app never opens from a link: the ",[69,3420,462],{"href":461}," diagnoses one issue for $450 and fixes it for $1,200, both fixed price (the $450 is credited toward the fix).",[144,3423,3424,3425,3429],{},"Connecting a link provider, your backend and a CRM or Stripe: ",[69,3426,3428],{"href":3427},"\u002Ffixed-price\u002Fapi-integration\u002F","API Integration",", $1,200 fixed price, 5 business days.",[1942,3431,3432],{},"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":470,"searchDepth":471,"depth":471,"links":3434},[3435,3436,3437,3438,3439,3440,3441],{"id":2775,"depth":471,"text":2776},{"id":2822,"depth":471,"text":2823},{"id":2873,"depth":471,"text":2874},{"id":348,"depth":471,"text":349},{"id":3048,"depth":471,"text":3049},{"id":3350,"depth":471,"text":3351},{"id":454,"depth":471,"text":455},"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.",[3445,3448,3451,3454,3457],{"question":3446,"answer":3447},"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":3449,"answer":3450},"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":3452,"answer":3453},"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":3455,"answer":3456},"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":3458,"answer":3459},"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.",{"readingTimeStats":3461},{"text":2746,"minutes":3462,"time":3463,"words":3464},7.7,462000,1540,"\u002Fblog\u002Ffirebase-dynamic-links-shutdown-deep-linking-2026","firebase dynamic links alternative","9 min",[3469,515,3470],"mobile-app-analytics-tracking-guide-2026","how-to-debug-a-flutter-firebase-bug-2026",{"title":2759,"description":3443},[3473,3476,3479,3482,3485,3488,3491,3493,3496,3498,3500,3503,3506,3509,3511,3513,3515],{"title":3474,"url":3475},"Firebase Dynamic Links FAQ (shutdown date, link behaviour, recommended alternatives)","https:\u002F\u002Ffirebase.google.com\u002Fsupport\u002Fdynamic-links-faq",{"title":3477,"url":3478},"Firebase, Migrate to App Links and Universal Links (official migration guide)","https:\u002F\u002Ffirebase.google.com\u002Fsupport\u002Fguides\u002Fapp-links-universal-links",{"title":3480,"url":3481},"Apple, Supporting associated domains","https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxcode\u002Fsupporting-associated-domains",{"title":3483,"url":3484},"Apple CDN copy of an apple-app-site-association file (example: www.apple.com)","https:\u002F\u002Fapp-site-association.cdn-apple.com\u002Fa\u002Fv1\u002Fwww.apple.com",{"title":3486,"url":3487},"Android, Verify Android App Links","https:\u002F\u002Fdeveloper.android.com\u002Ftraining\u002Fapp-links\u002Fverify-android-applinks",{"title":3489,"url":3490},"Android, Configure website associations (assetlinks.json requirements)","https:\u002F\u002Fdeveloper.android.com\u002Ftraining\u002Fapp-links\u002Fconfigure-assetlinks",{"title":3492,"url":3338},"Google, Statement List Generator and Tester",{"title":3494,"url":3495},"Flutter, Deep linking","https:\u002F\u002Fdocs.flutter.dev\u002Fui\u002Fnavigation\u002Fdeep-linking",{"title":3497,"url":3357},"Flutter, Set up universal links (iOS)",{"title":3499,"url":3362},"Flutter, Set up app links (Android)",{"title":3501,"url":3502},"Flutter DevTools, Validate deep links","https:\u002F\u002Fdocs.flutter.dev\u002Ftools\u002Fdevtools\u002Fdeep-links",{"title":3504,"url":3505},"app_links package on pub.dev","https:\u002F\u002Fpub.dev\u002Fpackages\u002Fapp_links",{"title":3507,"url":3508},"go_router package on pub.dev","https:\u002F\u002Fpub.dev\u002Fpackages\u002Fgo_router",{"title":3510,"url":2926},"Branch pricing",{"title":3512,"url":2946},"AppsFlyer deep linking pricing",{"title":3514,"url":2965},"Adjust pricing",{"title":3516,"url":2985},"Airbridge pricing","blog\u002Ffirebase-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",[3520,3521,3522,3523,3524],"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: \u002F.well-known\u002Fapple-app-site-association on iOS and \u002F.well-known\u002Fassetlinks.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 I could read.","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-dynamic-links-shutdown-deep-linking-2026\u002F","_mWP_Q_ttaDkkX4vZxultP-iW3Ltk1LPCFIKgQWYXWM",[3528,3530,3533,3534,3537,3540],{"keyword":5,"url":578,"title":5,"relevance":3529},78,{"keyword":3531,"url":3532,"title":3531,"relevance":868},"Flutter vs Native in 2026: An Honest Decision Guide","https:\u002F\u002Fapp369.com\u002Fblog\u002Fflutter-vs-native-app-development-2026\u002F",{"keyword":2759,"url":3525,"title":2759,"relevance":725},{"keyword":3535,"url":3536,"title":3535,"relevance":725},"Supabase \"Permission Denied\" on New Tables: the October 30 Fix","https:\u002F\u002Fapp369.com\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026\u002F",{"keyword":3538,"url":3539,"title":3538,"relevance":725},"How to Debug a Flutter and Firebase Bug: Find the Root Cause (2026)","https:\u002F\u002Fapp369.com\u002Fblog\u002Fhow-to-debug-a-flutter-firebase-bug-2026\u002F",{"keyword":3541,"url":3542,"title":3541,"relevance":713},"Best Flutter App Development Companies in 2026","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbest-app-development-company-flutter-2026\u002F",1792271329325]