[{"data":1,"prerenderedAt":3128},["ShallowReactive",2],{"\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026\u002F":3,"\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026\u002F-related":508,"\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026\u002F-resources":3119},{"id":4,"title":5,"author":6,"body":10,"category":440,"createdAt":441,"description":442,"disclosure":443,"extension":444,"faq":445,"howTo":443,"location":464,"meta":465,"navigation":471,"ogImage":443,"path":472,"primaryKeyword":473,"readingTime":474,"related":475,"seo":479,"sources":480,"stem":498,"tags":499,"takeaways":500,"updatedAt":441,"url":506,"__hash__":507},"blog\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026.md","FCM Legacy API Shutdown: Fixing Push and the 2027 Deadline",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":429},"minimark",[13,21,24,30,35,137,140,145,148,186,190,291,295,298,405,408,412],[14,15,16,20],"p",{},[17,18,19],"strong",{},"If your app's push notifications stopped arriving in mid-2024, your server was probably still using the legacy FCM API, which Firebase began shutting down on July 22, 2024."," The fix is on the server: send through the FCM HTTP v1 API, ideally with the Firebase Admin SDK, and replace the old server key with a service account. Then check the second deadline: the InstanceId server APIs and device groups stop working after September 29, 2027.",[14,22,23],{},"If you landed here from a tutorial that links to Firebase's old migration guide, that page now returns 404. The dates and steps here come from Firebase's own documentation, as listed in the sources, including an archived copy of that guide. I tested the old endpoint and guide URL myself on October 18, 2026.",[14,25,26,29],{},[17,27,28],{},"Disclosure:"," App369 is my studio. I build and fix Flutter and Firebase apps for a living, including push notifications, and the last section mentions what I sell. Google doesn't pay me for anything on this page.",[31,32,34],"h2",{"id":33},"what-changed-and-when","What changed, 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,92,107,120],"tbody",{},[42,58,59,63,66],{},[60,61,62],"td",{},"June 20, 2023",[60,64,65],{},"Firebase deprecates sending through the legacy FCM HTTP and XMPP APIs, including upstream messages",[60,67,68],{},[69,70,74],"a",{"href":71,"rel":72},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F20260108211426\u002Fhttps:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fmigrate-v1",[73],"nofollow","Firebase (archived guide)",[42,76,77,82,87],{},[60,78,79],{},[17,80,81],{},"July 22, 2024",[60,83,84],{},[17,85,86],{},"Shutdown of the legacy send APIs begins",[60,88,89],{},[69,90,74],{"href":71,"rel":91},[73],[42,93,94,97,100],{},[60,95,96],{},"September 2026",[60,98,99],{},"Firebase deprecates all InstanceId server APIs and the device group feature",[60,101,102],{},[69,103,106],{"href":104,"rel":105},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Ftroubleshooting#fcm-26-deprecation",[73],"Firebase FAQ",[42,108,109,112,115],{},[60,110,111],{},"January 1, 2027",[60,113,114],{},"New users can no longer start using the deprecated APIs",[60,116,117],{},[69,118,106],{"href":104,"rel":119},[73],[42,121,122,127,132],{},[60,123,124],{},[17,125,126],{},"September 29, 2027",[60,128,129],{},[17,130,131],{},"Requests to the InstanceId server APIs and device group management start failing",[60,133,134],{},[69,135,106],{"href":104,"rel":136},[73],[14,138,139],{},"Firebase says there is no service downgrade before September 29, 2027, so the second change gives you roughly a year from the announcement. The first one is already done.",[141,142,144],"h3",{"id":143},"what-the-old-addresses-do-today","What the old addresses do today",[14,146,147],{},"Checked on October 18, 2026:",[149,150,151,166,180],"ul",{},[152,153,154,155,161,162,165],"li",{},"A POST to ",[17,156,157],{},[69,158,159],{"href":159,"rel":160},"https:\u002F\u002Ffcm.googleapis.com\u002Ffcm\u002Fsend",[73],", the legacy send endpoint, returned ",[17,163,164],{},"404",".",[152,167,168,171,172,174,175,179],{},[17,169,170],{},"firebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fmigrate-v1",", the migration guide most tutorials link to, returned ",[17,173,164],{},". The ",[69,176,178],{"href":71,"rel":177},[73],"Wayback Machine copy from January 2026"," still has the full text.",[152,181,182,185],{},[17,183,184],{},"firebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fhttp-server-ref",", the legacy protocol reference, redirects to Firebase's server environment page.",[31,187,189],{"id":188},"moving-sends-to-http-v1","Moving sends to HTTP v1",[191,192,193,212,218,236,273,279,285],"ol",{},[152,194,195,198,199,203,204,207,208,211],{},[17,196,197],{},"Find the old code."," Search your server, Cloud Functions and any scripts for ",[200,201,202],"code",{},"fcm.googleapis.com\u002Ffcm\u002Fsend",", an ",[200,205,206],{},"Authorization: key="," header, ",[200,209,210],{},"registration_ids",", or an XMPP connection to FCM. Also check third-party tools or plugins that send pushes for you: any that were set up with an FCM server key need updating or replacing.",[152,213,214,217],{},[17,215,216],{},"Use the Admin SDK if you can."," Firebase calls the Admin SDK the recommended method for sending from a server. It handles authorization and the new message format, and it can send the same message to a list of tokens for you.",[152,219,220,223,224,227,228,231,232,235],{},[17,221,222],{},"Or call HTTP v1 directly."," Send a POST to ",[200,225,226],{},"https:\u002F\u002Ffcm.googleapis.com\u002Fv1\u002Fprojects\u002FPROJECT_ID\u002Fmessages:send"," with an ",[200,229,230],{},"Authorization: Bearer \u003Caccess token>"," header. The token is a short-lived OAuth 2.0 access token from a service account, requested with the ",[200,233,234],{},"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Ffirebase.messaging"," scope. Google's auth libraries can request and refresh it.",[152,237,238,241,242,245,246,249,250,253,254,257,258,261,262,265,266,269,270,272],{},[17,239,240],{},"Change the message shape."," HTTP v1 wraps everything in a ",[200,243,244],{},"message"," object with exactly one target, such as a ",[200,247,248],{},"token",", a ",[200,251,252],{},"topic"," or a ",[200,255,256],{},"condition",". Platform settings move into ",[200,259,260],{},"android",", ",[200,263,264],{},"apns"," and ",[200,267,268],{},"webpush"," blocks. There's no ",[200,271,210],{}," list. To reach many devices, send one request per token, use a topic or condition, or use the Admin SDK's multi-token sending, which still makes one request per token behind the scenes.",[152,274,275,278],{},[17,276,277],{},"Enable the API."," If you get errors reaching the endpoint, check that \"Firebase Cloud Messaging API\" is enabled for the project in the Google Cloud console, as Firebase's guide says.",[152,280,281,284],{},[17,282,283],{},"Keep the service account key off devices."," It belongs on your server or in a secret manager, never in the mobile app or a public repository. If a key was ever committed to a repository, replace it.",[152,286,287,290],{},[17,288,289],{},"Test with real devices."," Send a test message to an Android phone and an iPhone (and a web client, if you use FCM on the web), with the app in the foreground, in the background and closed, before you remove the old code path. Once nothing uses them, delete or restrict the old server keys so they can't be leaked and reused.",[31,292,294],{"id":293},"before-september-29-2027","Before September 29, 2027",[14,296,297],{},"The second deprecation hits apps that used the older InstanceId server APIs or device groups, even if sending already moved to HTTP v1.",[36,299,300,313],{},[39,301,302],{},[42,303,304,307,310],{},[45,305,306],{},"If you use",[45,308,309],{},"It stops working",[45,311,312],{},"What to do instead",[55,314,315,334,360,374,394],{},[42,316,317,324,327],{},[60,318,319,320,323],{},"InstanceId info API (",[200,321,322],{},"iid.googleapis.com\u002Fiid\u002Finfo",") to check tokens",[60,325,326],{},"After September 29, 2027",[60,328,329,330,333],{},"To validate a token, send through HTTP v1 with ",[200,331,332],{},"validate_only"," set to true. For topic info, use the new topic subscription API",[42,335,336,349,351],{},[60,337,338,339,261,342,261,345,348],{},"InstanceId topic subscription APIs (",[200,340,341],{},"iid\u002Fv1\u002F...\u002Frel\u002Ftopics",[200,343,344],{},"batchAdd",[200,346,347],{},"batchRemove",")",[60,350,326],{},[60,352,353,354,359],{},"The new ",[69,355,358],{"href":356,"rel":357},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Freference\u002Ffcm\u002Ffcmtopics\u002Frest",[73],"FCM topic subscription API",", or a current Admin SDK",[42,361,362,369,371],{},[60,363,364,365,368],{},"InstanceId batch import (",[200,366,367],{},"iid\u002Fv1:batchImport",") to turn APNs tokens into FCM tokens",[60,370,326],{},[60,372,373],{},"Send through Apple's APNs directly, or add the FCM iOS SDK so the app registers itself",[42,375,376,383,385],{},[60,377,378,379,382],{},"Device group management (",[200,380,381],{},"fcm.googleapis.com\u002Ffcm\u002Fnotification"," and similar)",[60,384,326],{},[60,386,387,388,393],{},"Follow Firebase's ",[69,389,392],{"href":390,"rel":391},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fmigrate-off-device-groups",[73],"guide to migrating off device groups"," and send to individual tokens",[42,395,396,399,402],{},[60,397,398],{},"Sending to a device group key through HTTP v1",[60,400,401],{},"Group keys stop working after September 29, 2027",[60,403,404],{},"Send to each device's token instead",[14,406,407],{},"If you manage topic subscriptions with the Firebase Admin SDK, Firebase says earlier versions are affected and lists the versions to upgrade to: Node.js 14.5.0 or later, Python 7.7.0 or later, Java 9.11.0 or later, Go 4.22.0 or later, and .NET 3.7.0 or later.",[31,409,411],{"id":410},"need-a-hand","Need a hand?",[14,413,414,415,419,420,424,425,165],{},"If pushes still don't arrive after the move, or you're not sure which parts of your backend touch the old APIs, the ",[69,416,418],{"href":417},"\u002Ffixed-price\u002Fflutter-firebase-bug-fix\u002F","App Fix Ticket"," covers API, webhook and Cloud Function errors in two fixed-price steps. A $450 Diagnosis gives you the root cause with evidence and a fixed repair quote within 3 business days, credited toward the fix. If you go ahead, the $1,200 Fix takes 5 business days. For help with something else in a Firebase app, see ",[69,421,423],{"href":422},"\u002Fblog\u002Fhow-to-debug-a-flutter-firebase-bug-2026\u002F","how to debug a Flutter and Firebase bug",", or ",[69,426,428],{"href":427},"\u002Fcontact\u002F","send me a short description",{"title":430,"searchDepth":431,"depth":431,"links":432},"",2,[433,437,438,439],{"id":33,"depth":431,"text":34,"children":434},[435],{"id":143,"depth":436,"text":144},3,{"id":188,"depth":431,"text":189},{"id":293,"depth":431,"text":294},{"id":410,"depth":431,"text":411},"AI & Technology","2026-10-18T21:00:00.000Z","Firebase shut down the legacy FCM send APIs from July 22, 2024, and in September 2026 deprecated the InstanceId server APIs and device groups, with a September 29, 2027 deadline. How to move push notifications to HTTP v1 and what to change before 2027.",null,"md",[446,449,452,455,458,461],{"question":447,"answer":448},"Why did my Firebase push notifications stop working in 2024?","Most likely your server still sent through the legacy FCM API (fcm.googleapis.com\u002Ffcm\u002Fsend, with a server key). Firebase deprecated those APIs on June 20, 2023 and began shutting them down on July 22, 2024. Move your server to the HTTP v1 API or the Firebase Admin SDK.",{"question":450,"answer":451},"What replaced the FCM server key?","A short-lived OAuth 2.0 access token derived from a service account. The Firebase Admin SDK creates and refreshes it for you. If you call the HTTP v1 API directly, you request the token yourself and send it as a Bearer token.",{"question":453,"answer":454},"Is the FCM legacy API completely gone?","For sending, yes. On October 18, 2026, a POST to fcm.googleapis.com\u002Ffcm\u002Fsend returned 404. Some related legacy endpoints, such as the InstanceId server APIs and device group management, still work until September 29, 2027, and then start failing.",{"question":456,"answer":457},"What is changing on September 29, 2027?","Firebase is decommissioning all InstanceId server APIs (iid.googleapis.com, used for token info, topic subscriptions and batch import of APNs tokens) and the device group feature. After that date, requests to those APIs fail and device group tokens stop working with the HTTP v1 send API. New users can't start using them after January 1, 2027.",{"question":459,"answer":460},"Do I need to change my Flutter, Android or iOS app?","Usually not for the send migration, because the change is on your server. The exceptions: apps that sent upstream messages from the device, which were deprecated along with the legacy APIs; apps that relied on device groups; and iOS setups that used server-side batch import of APNs tokens, which the FCM iOS SDK replaces.",{"question":462,"answer":463},"Where is the official FCM migration guide now?","The old URL, firebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fmigrate-v1, returned 404 on October 18, 2026. The Wayback Machine has a copy from January 2026. The current Firebase pages for sending are the HTTP v1 send guide and the server environment page, both linked in the sources.","Boynton Beach, FL",{"readingTimeStats":466},{"text":467,"minutes":468,"time":469,"words":470},"5 min read",4.56,273600,912,true,"\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026","fcm legacy api shutdown","8 min",[476,477,478],"firebase-dynamic-links-shutdown-deep-linking-2026","how-to-debug-a-flutter-firebase-bug-2026","firebase-security-guide-2026",{"title":5,"description":442},[481,483,485,488,491,493,495],{"title":482,"url":71},"Firebase, \"Migrate from legacy FCM APIs to HTTP v1\" (Wayback Machine copy, January 8, 2026)",{"title":484,"url":104},"Firebase, Cloud Messaging FAQ and troubleshooting (FCM legacy API deprecations, September 2026)",{"title":486,"url":487},"Firebase, send messages with the HTTP v1 API","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fsend\u002Fv1-api",{"title":489,"url":490},"Firebase, FCM server environment","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcloud-messaging\u002Fserver-environment",{"title":492,"url":356},"Firebase, FCM topic subscription REST API",{"title":494,"url":390},"Firebase, migrate off device groups",{"title":496,"url":497},"Google, InstanceId server reference","https:\u002F\u002Fdevelopers.google.com\u002Finstance-id\u002Freference\u002Fserver","blog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026","fcm legacy api shutdown, fcm http v1 migration, fcm server key deprecated, fcm\u002Fsend 404, firebase push notifications stopped working, fcm device groups deprecated, instanceid api deprecated, iid.googleapis.com, fcm topic subscription api, firebase admin sdk messaging",[501,502,503,504,505],"Firebase deprecated sending through the legacy FCM HTTP and XMPP APIs on June 20, 2023, and began shutting them down on July 22, 2024. On October 18, 2026, the old fcm.googleapis.com\u002Ffcm\u002Fsend endpoint returned 404.","The replacement is the FCM HTTP v1 API, authorized with short-lived OAuth 2.0 access tokens from a service account instead of a server key. Firebase recommends the Admin SDK, which handles the tokens for you.","A second deadline is coming: in September 2026 Firebase deprecated all InstanceId server APIs and the device group feature. Requests to them start failing after September 29, 2027.","If you manage topic subscriptions with an older Firebase Admin SDK, you are affected by the 2027 change. Firebase lists the minimum versions to upgrade to, such as Node.js 14.5.0 and Python 7.7.0.","Firebase's old migration guide URL now returns 404, so many tutorials that link to it lead nowhere. This page collects the dates, the steps and the current Firebase pages in one place.","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffcm-legacy-api-shutdown-http-v1-migration-2026\u002F","A_ao7VryjLaWdcj-L_c1xjs6KP8d29Wvnc4d8yJV8dM",[509,1327,1737],{"id":510,"title":511,"author":512,"body":514,"category":440,"createdAt":1235,"description":1236,"disclosure":443,"extension":444,"faq":1237,"howTo":443,"location":464,"meta":1256,"navigation":471,"ogImage":443,"path":1262,"primaryKeyword":1263,"readingTime":1264,"related":1265,"seo":1267,"sources":1268,"stem":1315,"tags":1316,"takeaways":1317,"updatedAt":1324,"url":1325,"__hash__":1326},"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":513},{"twitter":9},{"type":11,"value":515,"toc":1224},[516,522,525,529,569,572,576,579,600,603,620,623,627,636,639,643,772,775,779,814,817,821,824,854,955,980,1056,1059,1133,1136,1140,1167,1170,1196,1199,1201,1204,1220],[14,517,518,521],{},[17,519,520],{},"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,523,524],{},"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,526,528],{"id":527},"what-deep-linking-means-in-plain-words","What deep linking means, in plain words",[149,530,531,537,547,557,563],{},[152,532,533,536],{},[17,534,535],{},"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.",[152,538,539,542,543,546],{},[17,540,541],{},"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 ",[200,544,545],{},"apple-app-site-association",", to confirm the link belongs to your app.",[152,548,549,552,553,556],{},[17,550,551],{},"Android App Links."," Google's version. Same idea. Android checks a file called ",[200,554,555],{},"assetlinks.json"," on your website.",[152,558,559,562],{},[17,560,561],{},"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.",[152,564,565,568],{},[17,566,567],{},"Click analytics."," Counts of who clicked which link. Dynamic Links had some. Google's migration guide offers no replacement for it.",[14,570,571],{},"Marketers mostly care about the last two. Developers mostly care about the first three.",[31,573,575],{"id":574},"what-happened-to-firebase-dynamic-links","What happened to Firebase Dynamic Links",[14,577,578],{},"Google's own FAQ says Dynamic Links shut down on August 25, 2025. This is what that means for old links:",[149,580,581,584,587,594,597],{},[152,582,583],{},"Every clicked link returns an HTTP 404 to the user.",[152,585,586],{},"Short link creation and link stats API calls return 400 or 403 errors.",[152,588,589,590,593],{},"Existing ",[200,591,592],{},".page.link"," domains cannot be kept or transferred. Domain and link data is marked for deletion on the shutdown date.",[152,595,596],{},"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.",[152,598,599],{},"Email link sign-in, password reset and email verification on mobile needed updated SDKs. Google has separate guides for iOS and Android.",[14,601,602],{},"Google's own advice, in its FAQ:",[191,604,605,608,611,614],{},[152,606,607],{},"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.",[152,609,610],{},"If you only need to open an installed app, use Universal Links and App Links. The official migration guide covers this path.",[152,612,613],{},"If you no longer need the links, review them, delete the URL prefixes and remove the SDK.",[152,615,616,617,165],{},"Export your link metadata, and if you keep a custom domain, update your rewrite rules in ",[200,618,619],{},"firebase.json",[14,621,622],{},"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.",[141,624,626],{"id":625},"firebase-invites-went-first-in-2020","Firebase Invites went first, in 2020",[14,628,629,630,635],{},"Firebase Invites was the layer on top of Dynamic Links that let users send an app invite by SMS or email. Firebase's ",[69,631,634],{"href":632,"rel":633},"https:\u002F\u002Fweb.archive.org\u002Fweb\u002F20201201222821\u002Fhttps:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Finvites\u002Fdeprecation",[73],"deprecation notice"," was online by March 31, 2019, the earliest Wayback Machine capture, and said support would be discontinued on January 24, 2020. Its advice was to keep Dynamic Links and build your own sharing screen. That path closed too when Dynamic Links shut down.",[14,637,638],{},"On October 18, 2026, firebase.google.com\u002Fdocs\u002Finvites and its sub-pages returned a 404. If an app still has an \"invite a friend\" feature from that era, the fix today is the same as for Dynamic Links: share a Universal Link or App Link through the system share sheet, and add a provider only if you need to know which invite led to an install.",[31,640,642],{"id":641},"what-to-use-now","What to use now",[36,644,645,661],{},[39,646,647],{},[42,648,649,652,655,658],{},[45,650,651],{},"Option",[45,653,654],{},"Deferred deep linking",[45,656,657],{},"Click analytics",[45,659,660],{},"Price (checked October 17, 2026)",[55,662,663,678,698,718,738,757],{},[42,664,665,670,673,675],{},[60,666,667],{},[17,668,669],{},"Universal Links + App Links on your own domain",[60,671,672],{},"No",[60,674,672],{},[60,676,677],{},"Free. You pay for your domain and hosting.",[42,679,680,685,688,690],{},[60,681,682],{},[17,683,684],{},"Branch",[60,686,687],{},"Yes",[60,689,687],{},[60,691,692,693],{},"Basics plan: free trial, no price shown. Essentials and Enterprise: \"Request a demo.\" See ",[69,694,697],{"href":695,"rel":696},"https:\u002F\u002Fwww.branch.io\u002Fpricing\u002F",[73],"branch.io\u002Fpricing",[42,699,700,705,708,710],{},[60,701,702],{},[17,703,704],{},"AppsFlyer (OneLink)",[60,706,707],{},"Yes, from the Standard plan (Zero is free but has no deferred linking)",[60,709,687],{},[60,711,712,713],{},"Deep Linking plans: Zero is labelled \"Free.\" Standard, Advanced and Premium show \"Custom pricing\" with \"Request a demo.\" See ",[69,714,717],{"href":715,"rel":716},"https:\u002F\u002Fwww.appsflyer.com\u002Fpricing\u002Fdeep-linking",[73],"appsflyer.com\u002Fpricing\u002Fdeep-linking",[42,719,720,725,727,729],{},[60,721,722],{},[17,723,724],{},"Adjust",[60,726,687],{},[60,728,687],{},[60,730,731,732,737],{},"See ",[69,733,736],{"href":734,"rel":735},"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,739,740,745,747,749],{},[60,741,742],{},[17,743,744],{},"Airbridge",[60,746,687],{},[60,748,687],{},[60,750,751,752],{},"Core plan: $40+ a month after a 30-day free trial, deep linking included. Growth plan: custom. See ",[69,753,756],{"href":754,"rel":755},"https:\u002F\u002Fwww.airbridge.io\u002Fpricing",[73],"airbridge.io\u002Fpricing",[42,758,759,764,767,769],{},[60,760,761],{},[17,762,763],{},"Bitly, Kochava, Singular",[60,765,766],{},"Check each vendor",[60,768,766],{},[60,770,771],{},"Named in Google's FAQ. Prices not checked here, so see each vendor's pricing page.",[14,773,774],{},"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,776,778],{"id":777},"how-to-choose","How to choose",[191,780,781,787,793,799,805],{},[152,782,783,786],{},[17,784,785],{},"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.",[152,788,789,792],{},[17,790,791],{},"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.",[152,794,795,798],{},[17,796,797],{},"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.",[152,800,801,804],{},[17,802,803],{},"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.",[152,806,807,810,811,813],{},[17,808,809],{},"You have old Dynamic Links printed or published."," Links on a ",[200,812,592],{}," 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,815,816],{},"If you are unsure, start with option 1. You can add a provider later without changing your domain files.",[31,818,820],{"id":819},"set-up-universal-links-and-app-links-on-your-own-domain","Set up Universal Links and App Links on your own domain",[14,822,823],{},"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.",[191,825,826,840],{},[152,827,828,831,832,835,836,839],{},[17,829,830],{},"Pick the links."," Decide which paths open the app, for example ",[200,833,834],{},"\u002Finvite\u002F*"," or ",[200,837,838],{},"\u002Finvoice\u002F*",". Keep the list short.",[152,841,842,845,846,849,850,853],{},[17,843,844],{},"Create the iOS file."," Host it at ",[200,847,848],{},"https:\u002F\u002Fyourdomain.com\u002F.well-known\u002Fapple-app-site-association",". The name has no ",[200,851,852],{},".json"," extension. Apple's docs say to host it over HTTPS with a valid certificate and no redirects. A minimal version looks like this:",[855,856,860],"pre",{"className":857,"code":858,"language":859,"meta":430,"style":430},"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",[200,861,862,871,880,888,894,910,931,937,943,949],{"__ignoreMap":430},[863,864,867],"span",{"class":865,"line":866},"line",1,[863,868,870],{"class":869},"sVt8B","{\n",[863,872,873,877],{"class":865,"line":431},[863,874,876],{"class":875},"sj4cs","  \"applinks\"",[863,878,879],{"class":869},": {\n",[863,881,882,885],{"class":865,"line":436},[863,883,884],{"class":875},"    \"details\"",[863,886,887],{"class":869},": [\n",[863,889,891],{"class":865,"line":890},4,[863,892,893],{"class":869},"      {\n",[863,895,897,900,903,907],{"class":865,"line":896},5,[863,898,899],{"class":875},"        \"appIDs\"",[863,901,902],{"class":869},": [",[863,904,906],{"class":905},"sZZnC","\"TEAMID.com.example.app\"",[863,908,909],{"class":869},"],\n",[863,911,913,916,919,922,925,928],{"class":865,"line":912},6,[863,914,915],{"class":875},"        \"components\"",[863,917,918],{"class":869},": [{ ",[863,920,921],{"class":875},"\"\u002F\"",[863,923,924],{"class":869},": ",[863,926,927],{"class":905},"\"\u002Finvite\u002F*\"",[863,929,930],{"class":869}," }]\n",[863,932,934],{"class":865,"line":933},7,[863,935,936],{"class":869},"      }\n",[863,938,940],{"class":865,"line":939},8,[863,941,942],{"class":869},"    ]\n",[863,944,946],{"class":865,"line":945},9,[863,947,948],{"class":869},"  }\n",[863,950,952],{"class":865,"line":951},10,[863,953,954],{"class":869},"}\n",[191,956,957,967],{"start":436},[152,958,959,962,963,966],{},[17,960,961],{},"Add the iOS entitlement."," In Xcode, open Signing & Capabilities, add Associated Domains, and add ",[200,964,965],{},"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).",[152,968,969,845,972,975,976,979],{},[17,970,971],{},"Create the Android file.",[200,973,974],{},"https:\u002F\u002Fyourdomain.com\u002F.well-known\u002Fassetlinks.json",", with content type ",[200,977,978],{},"application\u002Fjson"," and no redirects:",[855,981,983],{"className":857,"code":982,"language":859,"meta":430,"style":430},"[{\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",[200,984,985,990,1002,1009,1022,1034,1047,1051],{"__ignoreMap":430},[863,986,987],{"class":865,"line":866},[863,988,989],{"class":869},"[{\n",[863,991,992,995,997,1000],{"class":865,"line":431},[863,993,994],{"class":875},"  \"relation\"",[863,996,902],{"class":869},[863,998,999],{"class":905},"\"delegate_permission\u002Fcommon.handle_all_urls\"",[863,1001,909],{"class":869},[863,1003,1004,1007],{"class":865,"line":436},[863,1005,1006],{"class":875},"  \"target\"",[863,1008,879],{"class":869},[863,1010,1011,1014,1016,1019],{"class":865,"line":890},[863,1012,1013],{"class":875},"    \"namespace\"",[863,1015,924],{"class":869},[863,1017,1018],{"class":905},"\"android_app\"",[863,1020,1021],{"class":869},",\n",[863,1023,1024,1027,1029,1032],{"class":865,"line":896},[863,1025,1026],{"class":875},"    \"package_name\"",[863,1028,924],{"class":869},[863,1030,1031],{"class":905},"\"com.example.app\"",[863,1033,1021],{"class":869},[863,1035,1036,1039,1041,1044],{"class":865,"line":912},[863,1037,1038],{"class":875},"    \"sha256_cert_fingerprints\"",[863,1040,902],{"class":869},[863,1042,1043],{"class":905},"\"AA:BB:...:FF\"",[863,1045,1046],{"class":869},"]\n",[863,1048,1049],{"class":865,"line":933},[863,1050,948],{"class":869},[863,1052,1053],{"class":865,"line":939},[863,1054,1055],{"class":869},"}]\n",[14,1057,1058],{},"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.",[191,1060,1061,1093,1099],{"start":896},[152,1062,1063,1066,1067,1070,1071,1074,1075,1078,1079,1082,1083,265,1086,1089,1090,165],{},[17,1064,1065],{},"Add the Android intent filter."," In ",[200,1068,1069],{},"AndroidManifest.xml",", add an ",[200,1072,1073],{},"intent-filter"," with ",[200,1076,1077],{},"android:autoVerify=\"true\"",", the ",[200,1080,1081],{},"VIEW"," action, the ",[200,1084,1085],{},"DEFAULT",[200,1087,1088],{},"BROWSABLE"," categories, and ",[200,1091,1092],{},"\u003Cdata android:scheme=\"https\" android:host=\"yourdomain.com\" \u002F>",[152,1094,1095,1098],{},[17,1096,1097],{},"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.",[152,1100,1101,1104],{},[17,1102,1103],{},"Test before you ship.",[149,1105,1106,1113,1130],{},[152,1107,1108,1109,1112],{},"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 ",[200,1110,1111],{},"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.",[152,1114,1115,1116,1119,1120,1123,1124,1129],{},"Android: run ",[200,1117,1118],{},"adb shell pm get-app-links com.example.app"," and look for ",[200,1121,1122],{},"verified"," next to your domain. Google's ",[69,1125,1128],{"href":1126,"rel":1127},"https:\u002F\u002Fdevelopers.google.com\u002Fdigital-asset-links\u002Ftools\u002Fgenerator",[73],"Statement List Generator and Tester"," can generate and check the file.",[152,1131,1132],{},"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,1134,1135],{},"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,1137,1139],{"id":1138},"flutter-specifics","Flutter specifics",[14,1141,1142,1143,265,1148,1153,1154,1157,1158,265,1161,1164,1165,165],{},"Flutter has official guides for both platforms: ",[69,1144,1147],{"href":1145,"rel":1146},"https:\u002F\u002Fdocs.flutter.dev\u002Fcookbook\u002Fnavigation\u002Fset-up-universal-links",[73],"set up universal links",[69,1149,1152],{"href":1150,"rel":1151},"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 ",[200,1155,1156],{},"FlutterDeepLinkingEnabled"," set to YES in ",[200,1159,1160],{},"Info.plist",[200,1162,1163],{},"flutter_deeplinking_enabled"," set to true in ",[200,1166,1069],{},[14,1168,1169],{},"Two ways to receive links:",[149,1171,1172,1182],{},[152,1173,1174,1177,1178,1181],{},[17,1175,1176],{},"go_router."," Flutter's built-in handling feeds incoming links to the ",[200,1179,1180],{},"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.",[152,1183,1184,1187,1188,1191,1192,1195],{},[17,1185,1186],{},"app_links."," The ",[200,1189,1190],{},"app_links"," package (7.2.2 today) handles Android App Links, iOS Universal Links and custom schemes. You subscribe to ",[200,1193,1194],{},"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,1197,1198],{},"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,1200,411],{"id":410},[14,1202,1203],{},"App369 is Simon Dziak's studio. He builds and runs Workspace369, a production Flutter and Firebase SaaS.",[149,1205,1206,1212],{},[152,1207,1208,1209,1211],{},"Links open the wrong screen, or the app never opens from a link: the ",[69,1210,418],{"href":417}," diagnoses one issue for $450 and fixes it for $1,200, both fixed price (the $450 is credited toward the fix).",[152,1213,1214,1215,1219],{},"Connecting a link provider, your backend and a CRM or Stripe: ",[69,1216,1218],{"href":1217},"\u002Ffixed-price\u002Fapi-integration\u002F","API Integration",", $1,200 fixed price, 5 business days.",[1221,1222,1223],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":430,"searchDepth":431,"depth":431,"links":1225},[1226,1227,1230,1231,1232,1233,1234],{"id":527,"depth":431,"text":528},{"id":574,"depth":431,"text":575,"children":1228},[1229],{"id":625,"depth":436,"text":626},{"id":641,"depth":431,"text":642},{"id":777,"depth":431,"text":778},{"id":819,"depth":431,"text":820},{"id":1138,"depth":431,"text":1139},{"id":410,"depth":431,"text":411},"2026-10-17T12:00:00.000Z","What deep linking is, what happened when Firebase Invites (2020) and Firebase Dynamic Links (August 25, 2025) shut down, and how to set up Universal Links and Android App Links in 2026. Vendor options and prices checked October 17, 2026.",[1238,1241,1244,1247,1250,1253],{"question":1239,"answer":1240},"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":1242,"answer":1243},"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":1245,"answer":1246},"What happened to Firebase Invites?","Firebase deprecated Invites and discontinued support on January 24, 2020. Its replacement was Dynamic Links with your own sharing screen, and Dynamic Links shut down on August 25, 2025. Today the firebase.google.com\u002Fdocs\u002Finvites pages return a 404. For app invites now, share a Universal Link or App Link through the system share sheet, or use a deep linking provider if you need install attribution.",{"question":1248,"answer":1249},"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":1251,"answer":1252},"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":1254,"answer":1255},"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":1257},{"text":1258,"minutes":1259,"time":1260,"words":1261},"9 min read",8.36,501600,1672,"\u002Fblog\u002Ffirebase-dynamic-links-shutdown-deep-linking-2026","firebase dynamic links alternative","9 min",[1266,478,477],"mobile-app-analytics-tracking-guide-2026",{"title":511,"description":1236},[1269,1272,1274,1277,1280,1283,1286,1289,1291,1294,1296,1298,1301,1304,1307,1309,1311,1313],{"title":1270,"url":1271},"Firebase Dynamic Links FAQ (shutdown date, link behaviour, recommended alternatives)","https:\u002F\u002Ffirebase.google.com\u002Fsupport\u002Fdynamic-links-faq",{"title":1273,"url":632},"Firebase, \"Migrating from Firebase Invites to Dynamic Links with Custom Sharing\" (Wayback Machine copy, December 2020)",{"title":1275,"url":1276},"Firebase, Migrate to App Links and Universal Links (official migration guide)","https:\u002F\u002Ffirebase.google.com\u002Fsupport\u002Fguides\u002Fapp-links-universal-links",{"title":1278,"url":1279},"Apple, Supporting associated domains","https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxcode\u002Fsupporting-associated-domains",{"title":1281,"url":1282},"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":1284,"url":1285},"Android, Verify Android App Links","https:\u002F\u002Fdeveloper.android.com\u002Ftraining\u002Fapp-links\u002Fverify-android-applinks",{"title":1287,"url":1288},"Android, Configure website associations (assetlinks.json requirements)","https:\u002F\u002Fdeveloper.android.com\u002Ftraining\u002Fapp-links\u002Fconfigure-assetlinks",{"title":1290,"url":1126},"Google, Statement List Generator and Tester",{"title":1292,"url":1293},"Flutter, Deep linking","https:\u002F\u002Fdocs.flutter.dev\u002Fui\u002Fnavigation\u002Fdeep-linking",{"title":1295,"url":1145},"Flutter, Set up universal links (iOS)",{"title":1297,"url":1150},"Flutter, Set up app links (Android)",{"title":1299,"url":1300},"Flutter DevTools, Validate deep links","https:\u002F\u002Fdocs.flutter.dev\u002Ftools\u002Fdevtools\u002Fdeep-links",{"title":1302,"url":1303},"app_links package on pub.dev","https:\u002F\u002Fpub.dev\u002Fpackages\u002Fapp_links",{"title":1305,"url":1306},"go_router package on pub.dev","https:\u002F\u002Fpub.dev\u002Fpackages\u002Fgo_router",{"title":1308,"url":695},"Branch pricing",{"title":1310,"url":715},"AppsFlyer deep linking pricing",{"title":1312,"url":734},"Adjust pricing",{"title":1314,"url":754},"Airbridge pricing","blog\u002Ffirebase-dynamic-links-shutdown-deep-linking-2026","firebase dynamic links alternative, firebase invites alternative, firebase invites deprecated, 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",[1318,1319,1320,1321,1322,1323],"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.","Firebase Invites, which shared Dynamic Links by SMS or email, was discontinued first: Firebase stopped supporting it on January 24, 2020 and pointed developers to Dynamic Links, which have now shut down too.","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.","2026-10-18T03:00:00.000Z","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-dynamic-links-shutdown-deep-linking-2026\u002F","JEEsVmFihriiUlUKa5kE2kyn59Dh8wynAZFQueah2UU",{"id":1328,"title":1329,"author":1330,"body":1332,"category":440,"createdAt":1723,"description":1724,"disclosure":443,"extension":444,"faq":443,"howTo":443,"location":464,"meta":1725,"navigation":471,"ogImage":443,"path":1731,"primaryKeyword":443,"readingTime":1264,"related":443,"seo":1732,"sources":443,"stem":1733,"tags":1734,"takeaways":443,"updatedAt":1723,"url":1735,"__hash__":1736},"blog\u002Fblog\u002Fhow-to-debug-a-flutter-firebase-bug-2026.md","How to Debug a Flutter and Firebase Bug: Find the Root Cause (2026)",{"name":7,"social":1331},{"twitter":9},{"type":11,"value":1333,"toc":1702},[1334,1340,1348,1352,1355,1369,1372,1376,1379,1464,1473,1477,1485,1489,1493,1530,1534,1537,1552,1556,1582,1586,1610,1614,1621,1625,1633,1637,1649,1653,1657,1665,1669,1675,1679,1688,1692,1695,1699],[14,1335,1336,1339],{},[17,1337,1338],{},"Most Flutter and Firebase bugs that take days to fix take days to find, not to fix."," Once you can reproduce the bug on demand and you know which layer it lives in (the app, Firebase's servers, or the build tooling), the fix is usually small. This guide is the order to work in, then the five problems that come up most.",[14,1341,1342,1343,1347],{},"It is the method behind App369's ",[69,1344,1346],{"href":1345},"\u002Ffixed-price\u002F#fix-ticket","fixed-price Flutter & Firebase Fix Ticket",", and the one Simon uses on his own production Flutter and Firebase SaaS, Workspace369.",[31,1349,1351],{"id":1350},"step-1-write-down-exact-reproduction-steps","Step 1: Write down exact reproduction steps",[14,1353,1354],{},"Before reading any code, write the bug as steps someone else could follow:",[191,1356,1357,1360,1363,1366],{},[152,1358,1359],{},"Which build: the app version, build number, and whether it is debug, profile or release.",[152,1361,1362],{},"Which device and OS version, and whether it happens on web, iOS, Android or all three.",[152,1364,1365],{},"Which account or data: a new user, an existing user, a specific record.",[152,1367,1368],{},"The exact taps, and what you expected versus what happened.",[14,1370,1371],{},"If it only happens \"sometimes\", note what the failing cases have in common: a slow network, a large account, the app coming back from the background. A bug you can't reproduce can't be verified as fixed.",[31,1373,1375],{"id":1374},"step-2-find-out-which-layer-is-failing","Step 2: Find out which layer is failing",[14,1377,1378],{},"Each layer has its own log, and the error is usually already in one of them:",[36,1380,1381,1391],{},[39,1382,1383],{},[42,1384,1385,1388],{},[45,1386,1387],{},"Layer",[45,1389,1390],{},"Where to look",[55,1392,1393,1411,1424,1435,1449],{},[42,1394,1395,1398],{},[60,1396,1397],{},"Flutter app",[60,1399,1400,1401,1404,1405,1410],{},"The ",[200,1402,1403],{},"flutter run"," console, and ",[69,1406,1409],{"href":1407,"rel":1408},"https:\u002F\u002Fdocs.flutter.dev\u002Ftools\u002Fdevtools",[73],"Flutter DevTools"," for widgets, network and memory",[42,1412,1413,1416],{},[60,1414,1415],{},"Release builds in users' hands",[60,1417,1418,1423],{},[69,1419,1422],{"href":1420,"rel":1421},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fcrashlytics",[73],"Firebase Crashlytics"," for crashes and non-fatal errors you record",[42,1425,1426,1429],{},[60,1427,1428],{},"Cloud Functions",[60,1430,1431,1432],{},"Logs Explorer in the Google Cloud console, or ",[200,1433,1434],{},"firebase functions:log",[42,1436,1437,1440],{},[60,1438,1439],{},"Firestore and Storage",[60,1441,1442,1443,835,1446],{},"The exact error code your app receives, such as ",[200,1444,1445],{},"permission-denied",[200,1447,1448],{},"failed-precondition",[42,1450,1451,1454],{},[60,1452,1453],{},"Build tooling",[60,1455,1456,1457,1074,1460,1463],{},"The full output of ",[200,1458,1459],{},"flutter build",[200,1461,1462],{},"-v",", plus Xcode or Gradle's own logs",[14,1465,1466,1467,1469,1470,1472],{},"A Firestore ",[200,1468,1445],{}," error means Security Rules refused the request; it is not an app crash. A ",[200,1471,1448],{}," error on a query often includes a link to create the missing index. Read the full message before changing code.",[31,1474,1476],{"id":1475},"step-3-isolate-it-with-the-emulator-suite","Step 3: Isolate it with the Emulator Suite",[14,1478,1400,1479,1484],{},[69,1480,1483],{"href":1481,"rel":1482},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Femulator-suite",[73],"Firebase Local Emulator Suite"," runs Auth, Firestore, Functions and Storage on your machine. Point a debug build at the emulators, load the data that triggers the bug, and you can repeat the failing steps as often as you need without touching production data. Security Rules problems in particular are easier to see in the emulator UI, which shows which rule allowed or denied each request.",[31,1486,1488],{"id":1487},"the-five-issues-that-come-up-most","The five issues that come up most",[141,1490,1492],{"id":1491},"build-failures","Build failures",[149,1494,1495,1502,1520,1527],{},[152,1496,1497,1498,1501],{},"Run ",[200,1499,1500],{},"flutter doctor -v"," first. It catches missing toolchains, licenses and Xcode or Android SDK problems.",[152,1503,1504,1505,1508,1509,265,1512,1515,1516,1519],{},"For iOS, check the minimum iOS version in the ",[200,1506,1507],{},"Podfile"," against what your Firebase plugins require, then run ",[200,1510,1511],{},"pod repo update",[200,1513,1514],{},"pod install"," in the ",[200,1517,1518],{},"ios"," folder.",[152,1521,1522,1523,1526],{},"For Android, check that the Android Gradle Plugin, Gradle and Kotlin versions are compatible with each other, and that ",[200,1524,1525],{},"minSdkVersion"," meets your plugins' minimum.",[152,1528,1529],{},"After upgrading Flutter or the FlutterFire plugins, upgrade all FlutterFire plugins together; mixed versions are a common cause of build errors.",[141,1531,1533],{"id":1532},"login-loops","Login loops",[14,1535,1536],{},"A user signs in and lands back on the sign-in screen, or the app flickers between screens. The usual causes:",[149,1538,1539,1546,1549],{},[152,1540,1541,1542,1545],{},"The router redirects before Firebase Auth has restored the session. On a cold start, ",[200,1543,1544],{},"authStateChanges()"," emits once the saved user is loaded; decide the first screen after that first event, not before.",[152,1547,1548],{},"Redirect logic that reads two different sources of truth, such as the auth state and a user profile document that hasn't loaded yet.",[152,1550,1551],{},"On web, the redirect sign-in flow returning to a page that immediately redirects again.",[141,1553,1555],{"id":1554},"firestore-writes-that-hang","Firestore writes that hang",[14,1557,1558,1559,835,1562,1565,1566,1569,1570,1572,1573,1575,1576,1578,1579,1581],{},"A ",[200,1560,1561],{},"set()",[200,1563,1564],{},"update()"," call whose ",[200,1567,1568],{},"Future"," never completes is often not stuck at all. With offline persistence, which is on by default on Android and iOS, the write is applied to the local cache immediately, and the ",[200,1571,1568],{}," completes only when the server confirms it. On a poor connection or with no connection, that can take a long time. Don't block the UI on the ",[200,1574,1568],{},": let the snapshot listener show the change. If the write is rejected, the ",[200,1577,1568],{}," completes with an error such as ",[200,1580,1445],{},", so make sure something catches and reports it.",[141,1583,1585],{"id":1584},"cloud-function-errors","Cloud Function errors",[149,1587,1588,1594,1601,1607],{},[152,1589,1590,1591,165],{},"Read the function's own logs before the client error; the client often only receives ",[200,1592,1593],{},"internal",[152,1595,1596,1597,1600],{},"In callable functions, throw an ",[200,1598,1599],{},"HttpsError"," with a clear code and message, so the app can show a useful error instead of a generic one.",[152,1602,1603,1604,165],{},"Check the function's region in the app matches where it is deployed; a callable in the wrong region returns ",[200,1605,1606],{},"not-found",[152,1608,1609],{},"For timeouts, compare the configured timeout and memory with what the function actually does.",[141,1611,1613],{"id":1612},"a-broken-screen","A broken screen",[14,1615,1616,1617,1620],{},"A red error screen in debug, or a grey one in release, means an exception was thrown during build. The console shows the first exception; fix that one first, because later errors are often caused by it. Common causes are a ",[200,1618,1619],{},"null"," value from a document field that doesn't exist on older records, and a list or column without bounded height inside a scrolling parent.",[31,1622,1624],{"id":1623},"fix-it-with-a-test","Fix it with a test",[14,1626,1627,1628,1632],{},"When you've found the root cause, write a test that fails because of it, then make it pass. Widget tests cover screens and state; the ",[69,1629,1631],{"href":1481,"rel":1630},[73],"Firebase emulators"," let integration tests and Security Rules tests run against real Firebase behavior. The test keeps the bug from coming back in the next release.",[31,1634,1636],{"id":1635},"when-to-get-help","When to get help",[14,1638,1639,1640,1643,1644,1648],{},"If a bug has eaten more than a day, App369 offers a ",[69,1641,1642],{"href":1345},"fixed-price Fix Ticket",". The $450 Diagnosis delivers the root cause with evidence and a fixed repair quote within 3 business days, and the $450 is credited toward the fix. The $1,200 Fix delivers one scoped defect fixed as a pull request, with a regression test and handover notes, within 5 business days. If the problem turns out to be your Security Rules, the ",[69,1645,1647],{"href":1646},"\u002Fblog\u002Ffirebase-security-guide-2026\u002F","Firebase Security Rules guide"," is a good next read.",[31,1650,1652],{"id":1651},"frequently-asked-questions","Frequently Asked Questions",[141,1654,1656],{"id":1655},"why-does-my-firestore-write-never-finish","Why does my Firestore write never finish?",[14,1658,1659,1660,1662,1663,165],{},"With offline persistence, a write is saved to the local cache at once, but its ",[200,1661,1568],{}," completes only when the server confirms it. On a slow or missing connection that can take a long time. Show the change from your snapshot listener instead of waiting on the write, and catch errors such as ",[200,1664,1445],{},[141,1666,1668],{"id":1667},"why-does-my-flutter-app-keep-returning-to-the-login-screen","Why does my Flutter app keep returning to the login screen?",[14,1670,1671,1672,1674],{},"Usually because the router decides where to go before Firebase Auth has restored the saved session, or because redirect logic depends on data that hasn't loaded yet. Wait for the first ",[200,1673,1544],{}," event before choosing the first screen.",[141,1676,1678],{"id":1677},"how-do-i-see-cloud-functions-errors","How do I see Cloud Functions errors?",[14,1680,1681,1682,1684,1685,1687],{},"In Logs Explorer in the Google Cloud console, or with ",[200,1683,1434],{},". The app often receives only a generic ",[200,1686,1593],{}," error, so the function's own logs are where the real message is.",[141,1689,1691],{"id":1690},"can-i-debug-firebase-without-touching-production-data","Can I debug Firebase without touching production data?",[14,1693,1694],{},"Yes. The Firebase Local Emulator Suite runs Auth, Firestore, Functions and Storage locally. Load test data that reproduces the bug and point a debug build at the emulators.",[141,1696,1698],{"id":1697},"what-should-i-send-a-developer-to-fix-a-flutter-bug","What should I send a developer to fix a Flutter bug?",[14,1700,1701],{},"Exact reproduction steps, the app version and build number, the device and OS, the error message or a screen recording, and access to the repository. With those, a developer can usually reproduce the bug on the first try.",{"title":430,"searchDepth":431,"depth":431,"links":1703},[1704,1705,1706,1707,1714,1715,1716],{"id":1350,"depth":431,"text":1351},{"id":1374,"depth":431,"text":1375},{"id":1475,"depth":431,"text":1476},{"id":1487,"depth":431,"text":1488,"children":1708},[1709,1710,1711,1712,1713],{"id":1491,"depth":436,"text":1492},{"id":1532,"depth":436,"text":1533},{"id":1554,"depth":436,"text":1555},{"id":1584,"depth":436,"text":1585},{"id":1612,"depth":436,"text":1613},{"id":1623,"depth":431,"text":1624},{"id":1635,"depth":431,"text":1636},{"id":1651,"depth":431,"text":1652,"children":1717},[1718,1719,1720,1721,1722],{"id":1655,"depth":436,"text":1656},{"id":1667,"depth":436,"text":1668},{"id":1677,"depth":436,"text":1678},{"id":1690,"depth":436,"text":1691},{"id":1697,"depth":436,"text":1698},"2026-10-12T15:00:00.000Z","Find the root cause of a Flutter and Firebase bug: reproduce it, read the right logs, isolate it with the Emulator Suite, and fix the five most common issues.",{"readingTimeStats":1726},{"text":1727,"minutes":1728,"time":1729,"words":1730},"6 min read",5.665,339900,1133,"\u002Fblog\u002Fhow-to-debug-a-flutter-firebase-bug-2026",{"title":1329,"description":1724},"blog\u002Fhow-to-debug-a-flutter-firebase-bug-2026","flutter debugging, firebase debugging, flutter build failure, firebase auth, firestore, cloud functions errors","https:\u002F\u002Fapp369.com\u002Fblog\u002Fhow-to-debug-a-flutter-firebase-bug-2026\u002F","Og5eeAe_Qd9f0SuWjNTINlCluQrpEjwYZ_XH4tsTFRU",{"id":1738,"title":1739,"author":1740,"body":1742,"category":440,"createdAt":3099,"description":3100,"disclosure":443,"extension":444,"faq":443,"howTo":443,"location":464,"meta":3101,"navigation":471,"ogImage":443,"path":3107,"primaryKeyword":443,"readingTime":3108,"related":3109,"seo":3113,"sources":443,"stem":3114,"tags":3115,"takeaways":443,"updatedAt":3116,"url":3117,"__hash__":3118},"blog\u002Fblog\u002Ffirebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":1741},{"twitter":9},{"type":11,"value":1743,"toc":3083},[1744,1756,1764,1768,1771,1780,1783,1803,1806,1810,1826,1829,1874,1877,1883,1921,1924,1937,1941,1950,1953,2088,2091,2153,2156,2160,2169,2172,2225,2228,2280,2289,2292,2316,2320,2323,2326,2429,2432,2508,2511,2515,2524,2532,2537,2559,2568,2680,2683,2726,2730,2733,2736,2742,2831,2837,2887,2893,2958,2961,2965,2971,3015,3026,3030,3034,3042,3046,3049,3053,3068,3072,3080],[14,1745,1746,1749,1750,1755],{},[17,1747,1748],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[69,1751,1754],{"href":1752,"rel":1753},"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,1757,1758,1759,1763],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[69,1760,1762],{"href":1761},"\u002Fservice\u002Fmobile-apps\u002F","App369"," uses to secure Firebase backends across production applications.",[31,1765,1767],{"id":1766},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,1769,1770],{},"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,1772,1773,1774,1779],{},"According to ",[69,1775,1778],{"href":1776,"rel":1777},"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,1781,1782],{},"Rules apply to three Firebase services independently:",[149,1784,1785,1791,1797],{},[152,1786,1787,1790],{},[17,1788,1789],{},"Cloud Firestore",": document-level read\u002Fwrite control with path-based matching",[152,1792,1793,1796],{},[17,1794,1795],{},"Realtime Database",": node-level access with cascading permissions",[152,1798,1799,1802],{},[17,1800,1801],{},"Cloud Storage",": file-level access based on path, metadata, and authentication state",[14,1804,1805],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[31,1807,1809],{"id":1808},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,1811,1812,1813,265,1816,1819,1820,1825],{},"Firebase's setup wizard offers two starting configurations: ",[17,1814,1815],{},"test mode",[17,1817,1818],{},"locked mode",". Test mode grants public read and write access to the entire database for 30 days. According to ",[69,1821,1824],{"href":1822,"rel":1823},"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,1827,1828],{},"Test mode rules look like this:",[855,1830,1834],{"className":1831,"code":1832,"language":1833,"meta":430,"style":430},"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",[200,1835,1836,1841,1846,1851,1856,1861,1866,1870],{"__ignoreMap":430},[863,1837,1838],{"class":865,"line":866},[863,1839,1840],{},"rules_version = '2';\n",[863,1842,1843],{"class":865,"line":431},[863,1844,1845],{},"service cloud.firestore {\n",[863,1847,1848],{"class":865,"line":436},[863,1849,1850],{},"  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n",[863,1852,1853],{"class":865,"line":890},[863,1854,1855],{},"    match \u002F{document=**} {\n",[863,1857,1858],{"class":865,"line":896},[863,1859,1860],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[863,1862,1863],{"class":865,"line":912},[863,1864,1865],{},"    }\n",[863,1867,1868],{"class":865,"line":933},[863,1869,948],{},[863,1871,1872],{"class":865,"line":939},[863,1873,954],{},[14,1875,1876],{},"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,1878,1879,1880,1882],{},"The correct starting point is ",[17,1881,1818],{},", where all access is denied by default:",[855,1884,1886],{"className":1831,"code":1885,"language":1833,"meta":430,"style":430},"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",[200,1887,1888,1892,1896,1900,1904,1909,1913,1917],{"__ignoreMap":430},[863,1889,1890],{"class":865,"line":866},[863,1891,1840],{},[863,1893,1894],{"class":865,"line":431},[863,1895,1845],{},[863,1897,1898],{"class":865,"line":436},[863,1899,1850],{},[863,1901,1902],{"class":865,"line":890},[863,1903,1855],{},[863,1905,1906],{"class":865,"line":896},[863,1907,1908],{},"      allow read, write: if false;\n",[863,1910,1911],{"class":865,"line":912},[863,1912,1865],{},[863,1914,1915],{"class":865,"line":933},[863,1916,948],{},[863,1918,1919],{"class":865,"line":939},[863,1920,954],{},[14,1922,1923],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,1925,1926,1927,1931,1932,1936],{},"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,1928,1930],{"href":1929},"\u002Ffixed-price\u002Fai-app-rescue\u002F","AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[69,1933,1935],{"href":1934},"\u002Ftools\u002Fsupabase-oct-30-checker\u002F","Supabase October 30 grants checker"," lists the tables that will need explicit grants after that date.",[31,1938,1940],{"id":1939},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,1942,1943,1944,1949],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[69,1945,1948],{"href":1946,"rel":1947},"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,1951,1952],{},"Here is a production-ready pattern for a user-owned data collection:",[855,1954,1956],{"className":1831,"code":1955,"language":1833,"meta":430,"style":430},"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",[200,1957,1958,1962,1966,1970,1975,1980,1985,1990,1994,1998,2003,2009,2015,2021,2027,2033,2039,2044,2049,2055,2061,2067,2073,2078,2083],{"__ignoreMap":430},[863,1959,1960],{"class":865,"line":866},[863,1961,1840],{},[863,1963,1964],{"class":865,"line":431},[863,1965,1845],{},[863,1967,1968],{"class":865,"line":436},[863,1969,1850],{},[863,1971,1972],{"class":865,"line":890},[863,1973,1974],{"emptyLinePlaceholder":471},"\n",[863,1976,1977],{"class":865,"line":896},[863,1978,1979],{},"    \u002F\u002F Users can only read and write their own profile\n",[863,1981,1982],{"class":865,"line":912},[863,1983,1984],{},"    match \u002Fusers\u002F{userId} {\n",[863,1986,1987],{"class":865,"line":933},[863,1988,1989],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[863,1991,1992],{"class":865,"line":939},[863,1993,1865],{},[863,1995,1996],{"class":865,"line":945},[863,1997,1974],{"emptyLinePlaceholder":471},[863,1999,2000],{"class":865,"line":951},[863,2001,2002],{},"    \u002F\u002F Posts are publicly readable, but only the author can write\n",[863,2004,2006],{"class":865,"line":2005},11,[863,2007,2008],{},"    match \u002Fposts\u002F{postId} {\n",[863,2010,2012],{"class":865,"line":2011},12,[863,2013,2014],{},"      allow read: if true;\n",[863,2016,2018],{"class":865,"line":2017},13,[863,2019,2020],{},"      allow create: if request.auth != null\n",[863,2022,2024],{"class":865,"line":2023},14,[863,2025,2026],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[863,2028,2030],{"class":865,"line":2029},15,[863,2031,2032],{},"      allow update, delete: if request.auth != null\n",[863,2034,2036],{"class":865,"line":2035},16,[863,2037,2038],{},"                            && resource.data.authorId == request.auth.uid;\n",[863,2040,2042],{"class":865,"line":2041},17,[863,2043,1865],{},[863,2045,2047],{"class":865,"line":2046},18,[863,2048,1974],{"emptyLinePlaceholder":471},[863,2050,2052],{"class":865,"line":2051},19,[863,2053,2054],{},"    \u002F\u002F Admin-only collection\n",[863,2056,2058],{"class":865,"line":2057},20,[863,2059,2060],{},"    match \u002FadminConfig\u002F{docId} {\n",[863,2062,2064],{"class":865,"line":2063},21,[863,2065,2066],{},"      allow read, write: if request.auth != null\n",[863,2068,2070],{"class":865,"line":2069},22,[863,2071,2072],{},"                         && request.auth.token.admin == true;\n",[863,2074,2076],{"class":865,"line":2075},23,[863,2077,1865],{},[863,2079,2081],{"class":865,"line":2080},24,[863,2082,948],{},[863,2084,2086],{"class":865,"line":2085},25,[863,2087,954],{},[14,2089,2090],{},"Key principles for effective rules:",[149,2092,2093,2117,2126,2139],{},[152,2094,2095,2098,2099,265,2102,2105,2106,261,2109,2112,2113,2116],{},[17,2096,2097],{},"Separate read and write permissions."," Use ",[200,2100,2101],{},"allow read",[200,2103,2104],{},"allow write"," independently. Break write into ",[200,2107,2108],{},"create",[200,2110,2111],{},"update",", and ",[200,2114,2115],{},"delete"," when different logic applies.",[152,2118,2119,2125],{},[17,2120,2121,2122,165],{},"Always check ",[200,2123,2124],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[152,2127,2128,265,2135,2138],{},[17,2129,2130,2131,2134],{},"Use ",[200,2132,2133],{},"resource.data"," for existing document fields",[200,2136,2137],{},"request.resource.data"," for incoming data in create\u002Fupdate operations.",[152,2140,2141,2144,2145,2148,2149,2152],{},[17,2142,2143],{},"Avoid wildcards at the root level."," The pattern ",[200,2146,2147],{},"match \u002F{document=**}"," applies to every document in the database. Use it only with ",[200,2150,2151],{},"if false"," as a catch-all deny rule.",[14,2154,2155],{},"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,2157,2159],{"id":2158},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,2161,2162,2163,2168],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[69,2164,2167],{"href":2165,"rel":2166},"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,2170,2171],{},"Set custom claims from a Cloud Function or admin server:",[855,2173,2175],{"className":1831,"code":2174,"language":1833,"meta":430,"style":430},"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",[200,2176,2177,2182,2186,2191,2196,2200,2205,2210,2215,2220],{"__ignoreMap":430},[863,2178,2179],{"class":865,"line":866},[863,2180,2181],{},"const admin = require('firebase-admin');\n",[863,2183,2184],{"class":865,"line":431},[863,2185,1974],{"emptyLinePlaceholder":471},[863,2187,2188],{"class":865,"line":436},[863,2189,2190],{},"\u002F\u002F Assign admin role\n",[863,2192,2193],{"class":865,"line":890},[863,2194,2195],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[863,2197,2198],{"class":865,"line":896},[863,2199,1974],{"emptyLinePlaceholder":471},[863,2201,2202],{"class":865,"line":912},[863,2203,2204],{},"\u002F\u002F Assign editor role with department\n",[863,2206,2207],{"class":865,"line":933},[863,2208,2209],{},"await admin.auth().setCustomUserClaims(userId, {\n",[863,2211,2212],{"class":865,"line":939},[863,2213,2214],{},"  role: 'editor',\n",[863,2216,2217],{"class":865,"line":945},[863,2218,2219],{},"  department: 'marketing'\n",[863,2221,2222],{"class":865,"line":951},[863,2223,2224],{},"});\n",[14,2226,2227],{},"Reference claims in Security Rules:",[855,2229,2231],{"className":1831,"code":2230,"language":1833,"meta":430,"style":430},"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",[200,2232,2233,2238,2243,2248,2252,2257,2262,2266,2271,2276],{"__ignoreMap":430},[863,2234,2235],{"class":865,"line":866},[863,2236,2237],{},"match \u002Fcontent\u002F{docId} {\n",[863,2239,2240],{"class":865,"line":431},[863,2241,2242],{},"  \u002F\u002F Admins have full access\n",[863,2244,2245],{"class":865,"line":436},[863,2246,2247],{},"  allow read, write: if request.auth.token.admin == true;\n",[863,2249,2250],{"class":865,"line":890},[863,2251,1974],{"emptyLinePlaceholder":471},[863,2253,2254],{"class":865,"line":896},[863,2255,2256],{},"  \u002F\u002F Editors can read and update, but not delete\n",[863,2258,2259],{"class":865,"line":912},[863,2260,2261],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[863,2263,2264],{"class":865,"line":933},[863,2265,1974],{"emptyLinePlaceholder":471},[863,2267,2268],{"class":865,"line":939},[863,2269,2270],{},"  \u002F\u002F All authenticated users can read\n",[863,2272,2273],{"class":865,"line":945},[863,2274,2275],{},"  allow read: if request.auth != null;\n",[863,2277,2278],{"class":865,"line":951},[863,2279,954],{},[14,2281,2282,2283,2288],{},"As noted in ",[69,2284,2287],{"href":2285,"rel":2286},"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,2290,2291],{},"Implementation guidelines:",[149,2293,2294,2300,2310],{},[152,2295,2296,2299],{},[17,2297,2298],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[152,2301,2302,2305,2306,2309],{},[17,2303,2304],{},"Force token refresh after claim changes."," Call ",[200,2307,2308],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[152,2311,2312,2315],{},[17,2313,2314],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[31,2317,2319],{"id":2318},"data-validation-in-security-rules","Data Validation in Security Rules",[14,2321,2322],{},"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,2324,2325],{},"Validation patterns for common requirements:",[855,2327,2329],{"className":1831,"code":2328,"language":1833,"meta":430,"style":430},"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",[200,2330,2331,2336,2341,2346,2351,2356,2361,2366,2371,2376,2381,2386,2391,2396,2401,2406,2410,2415,2420,2425],{"__ignoreMap":430},[863,2332,2333],{"class":865,"line":866},[863,2334,2335],{},"match \u002Fusers\u002F{userId} {\n",[863,2337,2338],{"class":865,"line":431},[863,2339,2340],{},"  allow create: if request.auth.uid == userId\n",[863,2342,2343],{"class":865,"line":436},[863,2344,2345],{},"    \u002F\u002F Required fields must exist\n",[863,2347,2348],{"class":865,"line":890},[863,2349,2350],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[863,2352,2353],{"class":865,"line":896},[863,2354,2355],{},"    \u002F\u002F Name must be a string between 2 and 100 characters\n",[863,2357,2358],{"class":865,"line":912},[863,2359,2360],{},"    && request.resource.data.name is string\n",[863,2362,2363],{"class":865,"line":933},[863,2364,2365],{},"    && request.resource.data.name.size() >= 2\n",[863,2367,2368],{"class":865,"line":939},[863,2369,2370],{},"    && request.resource.data.name.size() \u003C= 100\n",[863,2372,2373],{"class":865,"line":945},[863,2374,2375],{},"    \u002F\u002F Email must be a string and match the auth email\n",[863,2377,2378],{"class":865,"line":951},[863,2379,2380],{},"    && request.resource.data.email is string\n",[863,2382,2383],{"class":865,"line":2005},[863,2384,2385],{},"    && request.resource.data.email == request.auth.token.email\n",[863,2387,2388],{"class":865,"line":2011},[863,2389,2390],{},"    \u002F\u002F createdAt must be a timestamp\n",[863,2392,2393],{"class":865,"line":2017},[863,2394,2395],{},"    && request.resource.data.createdAt is timestamp\n",[863,2397,2398],{"class":865,"line":2023},[863,2399,2400],{},"    \u002F\u002F No unexpected fields allowed\n",[863,2402,2403],{"class":865,"line":2029},[863,2404,2405],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[863,2407,2408],{"class":865,"line":2035},[863,2409,1974],{"emptyLinePlaceholder":471},[863,2411,2412],{"class":865,"line":2041},[863,2413,2414],{},"  allow update: if request.auth.uid == userId\n",[863,2416,2417],{"class":865,"line":2046},[863,2418,2419],{},"    \u002F\u002F Prevent changing the email field\n",[863,2421,2422],{"class":865,"line":2051},[863,2423,2424],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[863,2426,2427],{"class":865,"line":2057},[863,2428,954],{},[14,2430,2431],{},"Validation capabilities in Firestore rules include:",[149,2433,2434,2461,2471,2484,2493,2499],{},[152,2435,2436,2439,2440,261,2443,261,2446,261,2449,261,2452,261,2455,424,2458],{},[17,2437,2438],{},"Type checking",": verify that fields are ",[200,2441,2442],{},"string",[200,2444,2445],{},"int",[200,2447,2448],{},"float",[200,2450,2451],{},"bool",[200,2453,2454],{},"timestamp",[200,2456,2457],{},"list",[200,2459,2460],{},"map",[152,2462,2463,2466,2467,2470],{},[17,2464,2465],{},"String length",": use ",[200,2468,2469],{},".size()"," to enforce minimum and maximum lengths",[152,2472,2473,924,2476,2479,2480,2483],{},[17,2474,2475],{},"Field existence",[200,2477,2478],{},"hasAll()"," for required fields, ",[200,2481,2482],{},"hasOnly()"," to prevent extra fields",[152,2485,2486,2466,2489,2492],{},[17,2487,2488],{},"Field immutability",[200,2490,2491],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[152,2494,2495,2498],{},[17,2496,2497],{},"Numeric ranges",": enforce minimum and maximum values for numbers",[152,2500,2501,2504,2505,2507],{},[17,2502,2503],{},"List constraints",": check ",[200,2506,2469],{}," on lists to limit array lengths",[14,2509,2510],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[31,2512,2514],{"id":2513},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,2516,2517,2518,2523],{},"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,2519,2522],{"href":2520,"rel":2521},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002Funit-tests",[73],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,2525,2526,2527,2531],{},"For a quick first pass, paste your rules into App369's free ",[69,2528,2530],{"href":2529},"\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,2533,2534],{},[17,2535,2536],{},"Use the Firebase Emulator Suite for local testing:",[855,2538,2542],{"className":2539,"code":2540,"language":2541,"meta":430,"style":430},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[200,2543,2544],{"__ignoreMap":430},[863,2545,2546,2550,2553,2556],{"class":865,"line":866},[863,2547,2549],{"class":2548},"sScJk","firebase",[863,2551,2552],{"class":905}," emulators:start",[863,2554,2555],{"class":875}," --only",[863,2557,2558],{"class":905}," firestore\n",[14,2560,2561],{},[17,2562,2563,2564,2567],{},"Write unit tests with the ",[200,2565,2566],{},"@firebase\u002Frules-unit-testing"," package:",[855,2569,2571],{"className":1831,"code":2570,"language":1833,"meta":430,"style":430},"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",[200,2572,2573,2578,2582,2587,2592,2597,2601,2605,2610,2615,2620,2625,2630,2634,2639,2644,2649,2653,2657,2662,2667,2671,2676],{"__ignoreMap":430},[863,2574,2575],{"class":865,"line":866},[863,2576,2577],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n",[863,2579,2580],{"class":865,"line":431},[863,2581,1974],{"emptyLinePlaceholder":471},[863,2583,2584],{"class":865,"line":436},[863,2585,2586],{},"const testEnv = await initializeTestEnvironment({\n",[863,2588,2589],{"class":865,"line":890},[863,2590,2591],{},"  projectId: 'your-project-id',\n",[863,2593,2594],{"class":865,"line":896},[863,2595,2596],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[863,2598,2599],{"class":865,"line":912},[863,2600,2224],{},[863,2602,2603],{"class":865,"line":933},[863,2604,1974],{"emptyLinePlaceholder":471},[863,2606,2607],{"class":865,"line":939},[863,2608,2609],{},"\u002F\u002F Test: authenticated user can read their own document\n",[863,2611,2612],{"class":865,"line":945},[863,2613,2614],{},"const userContext = testEnv.authenticatedContext('user123');\n",[863,2616,2617],{"class":865,"line":951},[863,2618,2619],{},"await assertSucceeds(\n",[863,2621,2622],{"class":865,"line":2005},[863,2623,2624],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[863,2626,2627],{"class":865,"line":2011},[863,2628,2629],{},");\n",[863,2631,2632],{"class":865,"line":2017},[863,2633,1974],{"emptyLinePlaceholder":471},[863,2635,2636],{"class":865,"line":2023},[863,2637,2638],{},"\u002F\u002F Test: user cannot read another user's document\n",[863,2640,2641],{"class":865,"line":2029},[863,2642,2643],{},"await assertFails(\n",[863,2645,2646],{"class":865,"line":2035},[863,2647,2648],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[863,2650,2651],{"class":865,"line":2041},[863,2652,2629],{},[863,2654,2655],{"class":865,"line":2046},[863,2656,1974],{"emptyLinePlaceholder":471},[863,2658,2659],{"class":865,"line":2051},[863,2660,2661],{},"\u002F\u002F Test: unauthenticated user cannot write\n",[863,2663,2664],{"class":865,"line":2057},[863,2665,2666],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[863,2668,2669],{"class":865,"line":2063},[863,2670,2643],{},[863,2672,2673],{"class":865,"line":2069},[863,2674,2675],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[863,2677,2678],{"class":865,"line":2075},[863,2679,2629],{},[14,2681,2682],{},"Deployment workflow:",[191,2684,2685,2700,2706,2714,2720],{},[152,2686,2687,2690,2691,261,2694,2112,2697],{},[17,2688,2689],{},"Write rules locally"," in ",[200,2692,2693],{},"firestore.rules",[200,2695,2696],{},"database.rules.json",[200,2698,2699],{},"storage.rules",[152,2701,2702,2705],{},[17,2703,2704],{},"Test with the emulator",": run the full test suite against the local emulator before every deployment",[152,2707,2708,2713],{},[17,2709,2130,2710],{},[200,2711,2712],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[152,2715,2716,2719],{},[17,2717,2718],{},"Version control your rules files",": commit them to Git and review changes in pull requests",[152,2721,2722,2725],{},[17,2723,2724],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[31,2727,2729],{"id":2728},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,2731,2732],{},"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,2734,2735],{},"Use Cloud Functions as a second validation layer for:",[14,2737,2738,2741],{},[17,2739,2740],{},"Financial transactions",": verify amounts, check account balances, and enforce rate limits server-side:",[855,2743,2745],{"className":1831,"code":2744,"language":1833,"meta":430,"style":430},"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",[200,2746,2747,2752,2757,2762,2766,2771,2776,2781,2785,2789,2794,2799,2804,2809,2813,2817,2822,2827],{"__ignoreMap":430},[863,2748,2749],{"class":865,"line":866},[863,2750,2751],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[863,2753,2754],{"class":865,"line":431},[863,2755,2756],{},"  \u002F\u002F Verify authentication\n",[863,2758,2759],{"class":865,"line":436},[863,2760,2761],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[863,2763,2764],{"class":865,"line":890},[863,2765,1974],{"emptyLinePlaceholder":471},[863,2767,2768],{"class":865,"line":896},[863,2769,2770],{},"  \u002F\u002F Server-side amount validation\n",[863,2772,2773],{"class":865,"line":912},[863,2774,2775],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[863,2777,2778],{"class":865,"line":933},[863,2779,2780],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[863,2782,2783],{"class":865,"line":939},[863,2784,948],{},[863,2786,2787],{"class":865,"line":945},[863,2788,1974],{"emptyLinePlaceholder":471},[863,2790,2791],{"class":865,"line":951},[863,2792,2793],{},"  \u002F\u002F Check user's balance from a trusted source\n",[863,2795,2796],{"class":865,"line":2005},[863,2797,2798],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[863,2800,2801],{"class":865,"line":2011},[863,2802,2803],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[863,2805,2806],{"class":865,"line":2017},[863,2807,2808],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[863,2810,2811],{"class":865,"line":2023},[863,2812,948],{},[863,2814,2815],{"class":865,"line":2029},[863,2816,1974],{"emptyLinePlaceholder":471},[863,2818,2819],{"class":865,"line":2035},[863,2820,2821],{},"  \u002F\u002F Process the transaction with admin privileges\n",[863,2823,2824],{"class":865,"line":2041},[863,2825,2826],{},"  \u002F\u002F ...\n",[863,2828,2829],{"class":865,"line":2046},[863,2830,2224],{},[14,2832,2833,2836],{},[17,2834,2835],{},"Data aggregation and denormalization",": use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[855,2838,2840],{"className":1831,"code":2839,"language":1833,"meta":430,"style":430},"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",[200,2841,2842,2847,2852,2857,2862,2867,2872,2877,2882],{"__ignoreMap":430},[863,2843,2844],{"class":865,"line":866},[863,2845,2846],{},"exports.onPostCreated = functions.firestore\n",[863,2848,2849],{"class":865,"line":431},[863,2850,2851],{},"  .document('posts\u002F{postId}')\n",[863,2853,2854],{"class":865,"line":436},[863,2855,2856],{},"  .onCreate(async (snap, context) => {\n",[863,2858,2859],{"class":865,"line":890},[863,2860,2861],{},"    \u002F\u002F Increment the author's post count atomically\n",[863,2863,2864],{"class":865,"line":896},[863,2865,2866],{},"    const authorId = snap.data().authorId;\n",[863,2868,2869],{"class":865,"line":912},[863,2870,2871],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[863,2873,2874],{"class":865,"line":933},[863,2875,2876],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[863,2878,2879],{"class":865,"line":939},[863,2880,2881],{},"    });\n",[863,2883,2884],{"class":865,"line":945},[863,2885,2886],{},"  });\n",[14,2888,2889,2892],{},[17,2890,2891],{},"Rate limiting",": prevent abuse by tracking request frequency per user:",[855,2894,2896],{"className":1831,"code":2895,"language":1833,"meta":430,"style":430},"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",[200,2897,2898,2903,2908,2913,2917,2922,2927,2932,2937,2941,2945,2949,2954],{"__ignoreMap":430},[863,2899,2900],{"class":865,"line":866},[863,2901,2902],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[863,2904,2905],{"class":865,"line":431},[863,2906,2907],{},"  const userId = context.auth.uid;\n",[863,2909,2910],{"class":865,"line":436},[863,2911,2912],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[863,2914,2915],{"class":865,"line":890},[863,2916,1974],{"emptyLinePlaceholder":471},[863,2918,2919],{"class":865,"line":896},[863,2920,2921],{},"  if (lastSubmission.exists) {\n",[863,2923,2924],{"class":865,"line":912},[863,2925,2926],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[863,2928,2929],{"class":865,"line":933},[863,2930,2931],{},"    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n",[863,2933,2934],{"class":865,"line":939},[863,2935,2936],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[863,2938,2939],{"class":865,"line":945},[863,2940,1865],{},[863,2942,2943],{"class":865,"line":951},[863,2944,948],{},[863,2946,2947],{"class":865,"line":2005},[863,2948,1974],{"emptyLinePlaceholder":471},[863,2950,2951],{"class":865,"line":2011},[863,2952,2953],{},"  \u002F\u002F Process the submission...\n",[863,2955,2956],{"class":865,"line":2017},[863,2957,2224],{},[14,2959,2960],{},"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,2962,2964],{"id":2963},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,2966,2967,2970],{},[69,2968,1762],{"href":2969},"\u002Fservice\u002Fflutter-development\u002F"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[149,2972,2973,2979,2985,2991,2997,3003],{},[152,2974,2975,2978],{},[17,2976,2977],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[152,2980,2981,2984],{},[17,2982,2983],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[152,2986,2987,2990],{},[17,2988,2989],{},"Comprehensive data validation"," in rules for every writable collection: type checking, field constraints, and immutability guards",[152,2992,2993,2996],{},[17,2994,2995],{},"Automated rules testing"," in CI\u002FCD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[152,2998,2999,3002],{},[17,3000,3001],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[152,3004,3005,3008,3009,3014],{},[17,3006,3007],{},"Security audits"," against the ",[69,3010,3013],{"href":3011,"rel":3012},"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,3016,3017,3018,3021,3022,165],{},"App369 builds ",[69,3019,3020],{"href":2969},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[69,3023,3025],{"href":3024},"\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","Flutter security best practices",[31,3027,3029],{"id":3028},"faq","FAQ",[141,3031,3033],{"id":3032},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,3035,3036,3037,3041],{},"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,3038,3040],{"href":1752,"rel":3039},[73],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[141,3043,3045],{"id":3044},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,3047,3048],{},"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.",[141,3050,3052],{"id":3051},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,3054,3055,3056,3059,3060,3063,3064,3067],{},"Use Firebase Authentication custom claims. Set ",[200,3057,3058],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[200,3061,3062],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[200,3065,3066],{},"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.",[141,3069,3071],{"id":3070},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,3073,3074,3075,3079],{},"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,3076,3078],{"href":1776,"rel":3077},[73],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[1221,3081,3082],{},"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":430,"searchDepth":431,"depth":431,"links":3084},[3085,3086,3087,3088,3089,3090,3091,3092,3093],{"id":1766,"depth":431,"text":1767},{"id":1808,"depth":431,"text":1809},{"id":1939,"depth":431,"text":1940},{"id":2158,"depth":431,"text":2159},{"id":2318,"depth":431,"text":2319},{"id":2513,"depth":431,"text":2514},{"id":2728,"depth":431,"text":2729},{"id":2963,"depth":431,"text":2964},{"id":3028,"depth":431,"text":3029,"children":3094},[3095,3096,3097,3098],{"id":3032,"depth":436,"text":3033},{"id":3044,"depth":436,"text":3045},{"id":3051,"depth":436,"text":3052},{"id":3070,"depth":436,"text":3071},"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":3102},{"text":3103,"minutes":3104,"time":3105,"words":3106},"11 min read",10.305,618300,2061,"\u002Fblog\u002Ffirebase-security-guide-2026","11 min",[3110,3111,3112],"supabase-permission-denied-new-tables-october-30-2026","ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":1739,"description":3100},"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",[3120,3122,3125],{"keyword":5,"url":506,"title":5,"relevance":3121},72,{"keyword":3123,"url":3124,"title":3123,"relevance":2035},"Firebase Deprecations: Every Shutdown Date, 2024 to 2027","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-deprecations-shutdown-dates-2026\u002F",{"keyword":3126,"url":3127,"title":3126,"relevance":912},"JCenter Shutdown: Fixing Old Android and Flutter Builds","https:\u002F\u002Fapp369.com\u002Fblog\u002Fjcenter-bintray-shut-down-fix-android-flutter-builds-2026\u002F",1792307201120]