[{"data":1,"prerenderedAt":3169},["ShallowReactive",2],{"/blog/parse-kinvey-backend-as-a-service-alternatives-2026/":3,"/blog/parse-kinvey-backend-as-a-service-alternatives-2026/-related":533,"/blog/parse-kinvey-backend-as-a-service-alternatives-2026/-resources":3156},{"id":4,"title":5,"author":6,"body":10,"category":458,"createdAt":459,"description":460,"disclosure":461,"extension":462,"faq":463,"howTo":461,"location":476,"meta":477,"navigation":483,"ogImage":461,"path":484,"primaryKeyword":485,"readingTime":486,"related":487,"seo":491,"sources":492,"stem":523,"tags":524,"takeaways":525,"updatedAt":459,"url":531,"__hash__":532},"blog/blog/parse-kinvey-backend-as-a-service-alternatives-2026.md","Parse, Kinvey and Realm Sync Are Gone: Where to Run Your App Backend in 2026",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":449},"minimark",[13,21,24,29,182,185,189,329,332,336,370,374,377,415,419,422],[14,15,16,20],"p",{},[17,18,19],"strong",{},"If your app was built on Parse, Kinvey, Telerik Platform or Realm Sync, that backend is gone, and links to the old docs now lead nowhere."," In 2026, the realistic homes for a mobile app backend are Firebase, Supabase, Parse Server (self-hosted or hosted on Back4App), Appwrite and PocketBase. The safest choice is the one you could leave, because every service on the list below was once someone's safe choice.",[14,22,23],{},"This guide replaces the migration posts and vendor pages that used to answer \"where do I go after Parse?\" Every date below comes from the vendor's own announcement, and every price was checked on the vendor's pricing page on October 17, 2026.",[25,26,28],"h2",{"id":27},"what-shut-down-and-when","What shut down, and when",[30,31,32,51],"table",{},[33,34,35],"thead",{},[36,37,38,42,45,48],"tr",{},[39,40,41],"th",{},"Service",[39,43,44],{},"What happened",[39,46,47],{},"Date",[39,49,50],{},"Source",[52,53,54,78,99,119,140,161],"tbody",{},[36,55,56,63,66,69],{},[57,58,59,62],"td",{},[17,60,61],{},"Parse"," (Facebook)",[57,64,65],{},"Hosted service retired after a one-year wind-down. Parse Server and a database migration tool were released as open source.",[57,67,68],{},"Announced January 28, 2016. Retired January 28, 2017",[57,70,71],{},[72,73,77],"a",{"href":74,"rel":75},"https://web.archive.org/web/20160128222249/http://blog.parse.com/announcements/moving-on/",[76],"nofollow","Parse blog, \"Moving On\"",[36,79,80,86,89,92],{},[57,81,82,85],{},[17,83,84],{},"Kinvey"," (Progress)",[57,87,88],{},"Bought by Progress. Today kinvey.com has no website address in DNS, and progress.com/kinvey redirects to a community page that returns a 404",[57,90,91],{},"Acquisition announced June 28, 2017. Links checked October 17, 2026",[57,93,94],{},[72,95,98],{"href":96,"rel":97},"https://www.progress.com/blogs/progress-acquires-the-leading-provider-of-backend-as-a-service",[76],"Progress",[36,100,101,106,109,112],{},[57,102,103,85],{},[17,104,105],{},"Telerik Platform and AppBuilder",[57,107,108],{},"Retired, with a notice telling users how to recover their apps and data",[57,110,111],{},"May 10, 2018",[57,113,114],{},[72,115,118],{"href":116,"rel":117},"https://www.npmjs.com/package/appbuilder",[76],"Progress notice on npm",[36,120,121,127,130,133],{},[57,122,123,126],{},[17,124,125],{},"Atlas Device Sync and Realm-based Device SDKs"," (MongoDB)",[57,128,129],{},"Deprecated, then removed. The on-device database continues as open source, without sync",[57,131,132],{},"Deprecated September 2024. Removed September 30, 2025",[57,134,135],{},[72,136,139],{"href":137,"rel":138},"https://www.mongodb.com/docs/atlas/device-sdks/deprecation/",[76],"MongoDB",[36,141,142,148,151,154],{},[57,143,144,147],{},[17,145,146],{},"Visual Studio App Center"," (Microsoft)",[57,149,150],{},"Retired, including the hosted CodePush service. Analytics and Diagnostics are extended",[57,152,153],{},"March 31, 2025. Analytics and Diagnostics run until the end of March 2027",[57,155,156],{},[72,157,160],{"href":158,"rel":159},"https://learn.microsoft.com/en-us/appcenter/retirement",[76],"Microsoft",[36,162,163,169,172,175],{},[57,164,165,168],{},[17,166,167],{},"Firebase Dynamic Links"," (Google)",[57,170,171],{},"Shut down. Clicked links now return a 404",[57,173,174],{},"August 25, 2025",[57,176,177],{},[72,178,181],{"href":179,"rel":180},"https://firebase.google.com/support/dynamic-links-faq",[76],"Firebase FAQ",[14,183,184],{},"The pattern is the same each time. A hosted service that is not the vendor's main business gets folded into something else or switched off, often with a year or more of notice. Apps that kept their data in standard formats, and their logic in their own code, moved in weeks. Apps that leaned on vendor-only features had to be rebuilt.",[25,186,188],{"id":187},"the-2026-options-compared","The 2026 options compared",[30,190,191,213],{},[33,192,193],{},[36,194,195,198,201,204,207,210],{},[39,196,197],{},"Backend",[39,199,200],{},"Database",[39,202,203],{},"Open source",[39,205,206],{},"Free tier (checked October 17, 2026)",[39,208,209],{},"First paid tier",[39,211,212],{},"Best for",[52,214,215,237,259,280,307],{},[36,216,217,222,225,228,231,234],{},[57,218,219],{},[17,220,221],{},"Firebase",[57,223,224],{},"Firestore (NoSQL)",[57,226,227],{},"No",[57,229,230],{},"Spark plan, no payment method needed. Firestore: 1 GiB stored, 50K reads, 20K writes and 20K deletes a day",[57,232,233],{},"Blaze, pay as you go (Spark's free usage is included)",[57,235,236],{},"Google-managed apps, push via FCM, offline-first mobile",[36,238,239,244,247,250,253,256],{},[57,240,241],{},[17,242,243],{},"Supabase",[57,245,246],{},"PostgreSQL",[57,248,249],{},"Yes (Apache-2.0)",[57,251,252],{},"500 MB database, 50,000 monthly active users, 2 active projects, paused after 1 week of inactivity",[57,254,255],{},"Pro from $25 a month, including $10 of compute credit",[57,257,258],{},"SQL data, row-level security, teams who may self-host later",[36,260,261,266,269,271,274,277],{},[57,262,263],{},[17,264,265],{},"Parse Server",[57,267,268],{},"MongoDB 7–8 or PostgreSQL 16–18",[57,270,249],{},[57,272,273],{},"Free to self-host. Back4App's hosted free plan: 25K requests a month, 250 MB of data, 1 GB of files",[57,275,276],{},"Back4App MVP at $15 per app a month, billed annually",[57,278,279],{},"Old Parse apps, and teams who want the Parse SDKs",[36,281,282,287,290,293,296,304],{},[57,283,284],{},[17,285,286],{},"Appwrite",[57,288,289],{},"Its own database API",[57,291,292],{},"Yes (BSD-3-Clause)",[57,294,295],{},"Free cloud plan, or self-host",[57,297,298,299],{},"See ",[72,300,303],{"href":301,"rel":302},"https://appwrite.io/pricing",[76],"appwrite.io/pricing",[57,305,306],{},"Self-hosted auth, database, storage and functions in one box",[36,308,309,314,317,320,323,326],{},[57,310,311],{},[17,312,313],{},"PocketBase",[57,315,316],{},"SQLite, in one Go binary",[57,318,319],{},"Yes (MIT)",[57,321,322],{},"Self-host only",[57,324,325],{},"Your own server",[57,327,328],{},"Small apps, internal tools, prototypes",[14,330,331],{},"Two notes on that table. Parse Server is very much alive: the Parse community shipped version 9.10.5 on October 16, 2026, and it runs on Node.js 22 and 24 (Node 20 reached end of life in April 2026). PocketBase is still pre-1.0 (v0.40.5, released October 8, 2026), so expect breaking changes between versions.",[25,333,335],{"id":334},"how-to-choose","How to choose",[337,338,339,346,352,358,364],"ol",{},[340,341,342,345],"li",{},[17,343,344],{},"You have an old Parse app."," Run Parse Server. Your client SDK calls keep working with few code changes, which was the whole point of the open-source release. Self-host it if you have someone to run MongoDB or PostgreSQL. Otherwise use a hosted Parse provider.",[340,347,348,351],{},[17,349,350],{},"You are starting a new mobile app and want the least operations work."," Firebase or Supabase. Choose Firebase for NoSQL documents, Firebase Cloud Messaging and offline persistence. Choose Supabase if your data is relational, if you want SQL, or if you might self-host later.",[340,353,354,357],{},[17,355,356],{},"You are leaving Realm Device Sync."," Keep the open-source on-device database if you only needed local storage. If you needed sync, move to a backend that syncs, then test offline edits, conflict handling and reconnects on real devices before you switch users over.",[340,359,360,363],{},[17,361,362],{},"You must keep data on your own servers."," Appwrite or Parse Server for a full backend, PocketBase for a small one.",[340,365,366,369],{},[17,367,368],{},"You were burned by a shutdown before."," Prefer open source, standard databases (PostgreSQL, MongoDB) and your own domain for every URL your app depends on. Firebase Dynamic Links showed that links on a vendor's domain die with the vendor's product.",[25,371,373],{"id":372},"migrating-off-a-dead-or-dying-backend","Migrating off a dead or dying backend",[14,375,376],{},"A backend migration is usually smaller than it looks, if you do it in this order:",[337,378,379,385,391,397,403,409],{},[340,380,381,384],{},[17,382,383],{},"List what the old backend actually did."," Sign-in methods, database tables or collections, file storage, push notifications, scheduled jobs, server-side code (Parse called it Cloud Code) and any webhooks. This list is your scope.",[340,386,387,390],{},[17,388,389],{},"Export the data first."," Move the records into a database you control, then point the old API at it if you still can. Parse's own migration tool worked this way, with no downtime.",[340,392,393,396],{},[17,394,395],{},"Move files separately."," File storage is often not part of a database export. Copy the files and rewrite their URLs.",[340,398,399,402],{},[17,400,401],{},"Plan sign-in carefully."," Find out whether password hashes can be exported and imported. If they can't, users will have to reset their passwords, so tell them in advance.",[340,404,405,408],{},[17,406,407],{},"Rebuild server-side code as functions."," Cloud Functions on Firebase, Edge Functions on Supabase, Cloud Code on Parse Server.",[340,410,411,414],{},[17,412,413],{},"Ship an app update that talks to the new backend, then switch off the old one."," Keep the old endpoints read-only for one release cycle, because some users update late.",[25,416,418],{"id":417},"need-a-hand","Need a hand?",[14,420,421],{},"App369 is Simon Dziak's studio. He builds and runs Workspace369, a production Flutter and Firebase SaaS, so this advice comes from running a live backend, not from a vendor brochure.",[423,424,425,433,441],"ul",{},[340,426,427,428,432],{},"An app that broke when its backend changed: the ",[72,429,431],{"href":430},"/fixed-price/flutter-firebase-bug-fix/","App Fix Ticket"," diagnoses one issue for $450 and fixes it for $1,200, both fixed price.",[340,434,435,436,440],{},"Moving data or connecting a new backend to Stripe, a CRM or another system: ",[72,437,439],{"href":438},"/fixed-price/api-integration/","API Integration",", $1,200 fixed price, 5 business days.",[340,442,443,444,448],{},"Already on Firebase and the bill is climbing: the ",[72,445,447],{"href":446},"/fixed-price/firebase-cost-audit/","Firebase Cost-Cut Audit",", $750 fixed price, 5 business days.",{"title":450,"searchDepth":451,"depth":451,"links":452},"",2,[453,454,455,456,457],{"id":27,"depth":451,"text":28},{"id":187,"depth":451,"text":188},{"id":334,"depth":451,"text":335},{"id":372,"depth":451,"text":373},{"id":417,"depth":451,"text":418},"AI & Technology","2026-10-17T12:00:00.000Z","Parse, Kinvey, Telerik Platform, Realm Sync and App Center are gone. A dated record of what shut down, plus the 2026 options for your app backend (Firebase, Supabase, Parse Server, Appwrite, PocketBase) with prices checked October 17, 2026.",null,"md",[464,467,470,473],{"question":465,"answer":466},"What replaced Parse after it shut down?","Facebook open-sourced Parse Server when it announced the shutdown on January 28, 2016. It is still maintained by the Parse community (version 9.10.5 shipped October 16, 2026) and runs on Node.js with MongoDB or PostgreSQL. You can self-host it or use a hosted Parse provider such as Back4App.",{"question":468,"answer":469},"Is Kinvey still available?","Not at its old address. Progress announced it had acquired Kinvey on June 28, 2017. On October 17, 2026, kinvey.com had no website address in DNS, and progress.com/kinvey redirected to a community page that returned a 404. If an app still depends on Kinvey, plan a migration now.",{"question":471,"answer":472},"What should I use instead of Realm Device Sync?","MongoDB ended Atlas Device Sync on September 30, 2025. The Realm on-device database continues as open source without sync. For a synced backend, Firebase (Firestore with offline persistence) and Supabase are the common replacements. Test offline behaviour in your own app before you commit.",{"question":474,"answer":475},"Firebase or Supabase for a new mobile app?","Firebase if you want Google-managed NoSQL, push notifications and a free Spark plan with no payment method. Supabase if you want PostgreSQL, SQL queries and an open-source stack you can self-host. Supabase Pro starts at $25 a month, and Firebase Blaze is pay as you go.","Boynton Beach, FL",{"readingTimeStats":478},{"text":479,"minutes":480,"time":481,"words":482},"5 min read",4.915,294900,983,true,"/blog/parse-kinvey-backend-as-a-service-alternatives-2026","parse alternatives","9 min",[488,489,490],"how-to-cut-your-firebase-bill-2026","firebase-security-guide-2026","supabase-permission-denied-new-tables-october-30-2026",{"title":5,"description":460},[493,495,498,500,502,504,506,508,511,514,517,520],{"title":494,"url":74},"Parse blog, \"Moving On\" (January 28, 2016), Wayback Machine copy",{"title":496,"url":497},"Parse Server on GitHub (releases, supported databases)","https://github.com/parse-community/parse-server",{"title":499,"url":96},"Progress, \"Progress Acquires the Leading Provider of Backend-as-a-Service\" (June 28, 2017)",{"title":501,"url":116},"Progress, Telerik Platform retirement notice on the appbuilder npm package",{"title":503,"url":137},"MongoDB, Atlas Device SDKs deprecation",{"title":505,"url":179},"Firebase Dynamic Links FAQ (shutdown date)",{"title":507,"url":158},"Microsoft, Visual Studio App Center retirement",{"title":509,"url":510},"Firebase pricing","https://firebase.google.com/pricing",{"title":512,"url":513},"Supabase pricing","https://supabase.com/pricing",{"title":515,"url":516},"Back4App backend pricing","https://www.back4app.com/pricing/backend-as-a-service",{"title":518,"url":519},"Appwrite on GitHub","https://github.com/appwrite/appwrite",{"title":521,"url":522},"PocketBase on GitHub","https://github.com/pocketbase/pocketbase","blog/parse-kinvey-backend-as-a-service-alternatives-2026","parse alternatives, parse.com, kinvey alternatives, telerik platform, backend as a service, baas, parse server, firebase vs supabase, realm device sync end of life, mbaas, mobile backend",[526,527,528,529,530],"Parse was retired on January 28, 2017. Its open-source successor, Parse Server, is still maintained: version 9.10.5 shipped on October 16, 2026.","Progress bought Kinvey in 2017 and retired Telerik Platform on May 10, 2018. As of October 17, 2026, kinvey.com has no website and progress.com/kinvey ends in a 404.","MongoDB removed Atlas Device Sync and the Realm-based Device SDKs on September 30, 2025. The on-device database lives on as open source, without sync.","For a new app in 2026, the realistic choices are Firebase, Supabase, Parse Server (self-hosted or on Back4App), Appwrite and PocketBase.","Pick the one you could leave. An open-source backend you can self-host protects you from the next shutdown.","https://app369.com/blog/parse-kinvey-backend-as-a-service-alternatives-2026/","pScspKFFx6bIN00EzeLsry6YCZpULCvf8ImgZyrkMV4",[534,1037,2426],{"id":535,"title":536,"author":537,"body":539,"category":458,"createdAt":1021,"description":1022,"disclosure":461,"extension":462,"faq":461,"howTo":461,"location":476,"meta":1023,"navigation":483,"ogImage":461,"path":1029,"primaryKeyword":461,"readingTime":1030,"related":461,"seo":1031,"sources":461,"stem":1032,"tags":1033,"takeaways":461,"updatedAt":1034,"url":1035,"__hash__":1036},"blog/blog/how-to-cut-your-firebase-bill-2026.md","How to Cut Your Firebase Bill: A Line-by-Line Cost Audit (2026)",{"name":7,"social":538},{"twitter":9},{"type":11,"value":540,"toc":1002},[541,547,555,559,567,593,597,600,637,640,644,651,713,719,727,731,755,759,762,803,807,810,821,824,828,831,851,855,858,862,865,939,942,946,958,962,967,970,974,977,981,984,988,991,995,998],[14,542,543,546],{},[17,544,545],{},"Most Firebase bills that keep creeping up have a few specific causes, and you can find them in an afternoon."," Measure what each service actually does, price it at Google's list prices, then rank every possible saving by dollars a month and by the risk your users would notice. Guessing (\"let's turn off min instances everywhere\") usually trades a small saving for slower screens.",[14,548,549,550,554],{},"This is the method App369 uses for its ",[72,551,553],{"href":552},"/fixed-price/#firebase-cost-audit","fixed-price Firebase Cost-Cut Audit",". It is also how Simon audits the production Firebase project behind his own SaaS, Workspace369, which runs about 980 Cloud Run services. On that project, the audit found that roughly 42% of steady-state spend was recoverable. It was a small bill, and not every finding was applied, but the pattern is common.",[25,556,558],{"id":557},"why-firebase-bills-grow-quietly","Why Firebase bills grow quietly",[14,560,561,562,566],{},"Firebase's pay-as-you-go ",[72,563,565],{"href":510,"rel":564},[76],"Blaze plan"," bills each product separately, so there is rarely one obvious line item. The usual sources are:",[423,568,569,575,581,587],{},[340,570,571,574],{},[17,572,573],{},"2nd gen Cloud Functions run on Cloud Run."," Every function is its own Cloud Run service with its own memory, CPU, concurrency and minimum-instance settings. A project with 100 functions has 100 sets of settings that nobody reviews after launch.",[340,576,577,580],{},[17,578,579],{},"Minimum instances bill while idle."," Keeping an instance warm removes cold starts, but you pay for it around the clock, whether or not anyone calls the function.",[340,582,583,586],{},[17,584,585],{},"Firestore bills per document read."," A screen that listens to a whole collection, or a scheduled job that scans one every few minutes, multiplies reads with no visible symptom.",[340,588,589,592],{},[17,590,591],{},"Background work has no user to complain."," Retry loops, triggers that fire other triggers, and verbose logging run at full speed until the invoice arrives.",[25,594,596],{"id":595},"step-1-measure-before-you-cut","Step 1: Measure before you cut",[14,598,599],{},"Start from data, not settings.",[337,601,602,614,625],{},[340,603,604,607,608,613],{},[17,605,606],{},"Billing export."," If ",[72,609,612],{"href":610,"rel":611},"https://cloud.google.com/billing/docs/how-to/export-data-bigquery",[76],"Cloud Billing export to BigQuery"," is on, you have actual spend by service and SKU. If it isn't, turn it on now; it only collects data from the day you enable it.",[340,615,616,619,620,624],{},[17,617,618],{},"Usage from Cloud Monitoring."," For each Cloud Run service, pull 7 and 30 days of request count and billable instance time (",[621,622,623],"code",{},"run.googleapis.com/container/billable_instance_time","). For Firestore, pull document reads, writes and deletes.",[340,626,627,630,631,636],{},[17,628,629],{},"List prices."," The ",[72,632,635],{"href":633,"rel":634},"https://cloud.google.com/billing/docs/how-to/get-pricing-information-api",[76],"Cloud Billing Catalog API"," returns the current price of every SKU in your region, so each estimate is priced from Google's own numbers rather than a blog post.",[14,638,639],{},"Multiply usage by price, per service, and you have an estimated monthly bill by service. Sort it. In most projects a handful of services account for most of the money.",[25,641,643],{"id":642},"step-2-always-on-minimum-instances","Step 2: Always-on minimum instances",[14,645,646,647,650],{},"List every service with ",[621,648,649],{},"minInstances"," above zero:",[652,653,657],"pre",{"className":654,"code":655,"language":656,"meta":450,"style":450},"language-bash shiki shiki-themes github-light github-dark","gcloud run services list --format=json \\\n| jq -r '.[] | [.metadata.name, (.spec.template.metadata.annotations[\"autoscaling.knative.dev/minScale\"] // \"0\")] | @tsv' \\\n| awk '$2 > 0'\n","bash",[621,658,659,685,702],{"__ignoreMap":450},[660,661,664,668,672,675,678,682],"span",{"class":662,"line":663},"line",1,[660,665,667],{"class":666},"sScJk","gcloud",[660,669,671],{"class":670},"sZZnC"," run",[660,673,674],{"class":670}," services",[660,676,677],{"class":670}," list",[660,679,681],{"class":680},"sj4cs"," --format=json",[660,683,684],{"class":680}," \\\n",[660,686,687,691,694,697,700],{"class":662,"line":451},[660,688,690],{"class":689},"szBVR","|",[660,692,693],{"class":666}," jq",[660,695,696],{"class":680}," -r",[660,698,699],{"class":670}," '.[] | [.metadata.name, (.spec.template.metadata.annotations[\"autoscaling.knative.dev/minScale\"] // \"0\")] | @tsv'",[660,701,684],{"class":680},[660,703,705,707,710],{"class":662,"line":704},3,[660,706,690],{"class":689},[660,708,709],{"class":666}," awk",[660,711,712],{"class":670}," '$2 > 0'\n",[14,714,715,716,718],{},"For each one, ask whether a user waits on it. A login or checkout function may deserve a warm instance. A nightly report or a webhook that Stripe retries anyway usually doesn't. In code, the setting lives on the function (",[621,717,649],{}," in the Firebase Functions v2 options), so change it there, not only in the console, or the next deploy puts it back.",[14,720,721,722,726],{},"To see what your current settings cost while idle, use the free ",[72,723,725],{"href":724},"/tools/min-instances-cost-calculator/","min instances cost calculator",".",[25,728,730],{"id":729},"step-3-memory-cpu-and-concurrency","Step 3: Memory, CPU and concurrency",[423,732,733,739,749],{},[340,734,735,738],{},[17,736,737],{},"Memory."," Functions are often deployed at 1 GiB or more \"to be safe\". Compare the configured memory with the memory utilization metric and size down where the peak is far below the limit.",[340,740,741,744,745,748],{},[17,742,743],{},"Concurrency."," 2nd gen functions can serve many requests per instance (80 by default with a full CPU); 1st gen functions serve one. A function forced to ",[621,746,747],{},"concurrency: 1"," scales out an instance per request.",[340,750,751,754],{},[17,752,753],{},"Timeouts."," A long timeout on a function that hangs on a slow upstream keeps a billed instance busy until it gives up.",[25,756,758],{"id":757},"step-4-firestore-reads","Step 4: Firestore reads",[14,760,761],{},"Read costs come from patterns, not single queries:",[423,763,764,770,793],{},[340,765,766,769],{},[17,767,768],{},"Collection listeners on busy screens."," A dashboard that listens to every document in a collection pays for every change, for every open client. Paginate, filter, or listen to a summary document instead.",[340,771,772,775,776,781,782,785,786,785,789,792],{},[17,773,774],{},"Counting by reading."," Use ",[72,777,780],{"href":778,"rel":779},"https://firebase.google.com/docs/firestore/query-data/aggregation-queries",[76],"aggregation queries"," (",[621,783,784],{},"count()",", ",[621,787,788],{},"sum()",[621,790,791],{},"average()",") instead of downloading documents to count them.",[340,794,795,798,799,802],{},[17,796,797],{},"Scheduled scans."," A job that reads a whole collection every five minutes to find a few changed documents should query on an indexed ",[621,800,801],{},"updatedAt"," field instead.",[25,804,806],{"id":805},"step-5-trigger-fan-out-and-retry-storms","Step 5: Trigger fan-out and retry storms",[14,808,809],{},"Event-triggered functions can multiply quietly:",[423,811,812,815,818],{},[340,813,814],{},"A function that writes to the document that triggered it can re-trigger itself.",[340,816,817],{},"One write can fan out to several triggers, each of which writes again.",[340,819,820],{},"A function with retries enabled that fails on the same bad data keeps retrying, for hours or days, and every attempt is billed.",[14,822,823],{},"Look for invocation counts that are far higher than the user actions behind them, and for error rates that never drop.",[25,825,827],{"id":826},"step-6-secrets-logs-and-images","Step 6: Secrets, logs and images",[14,829,830],{},"These are smaller lines, but they add up across many services:",[423,832,833,839,845],{},[340,834,835,838],{},[17,836,837],{},"Secret Manager"," bills for active secret versions and for access operations. Disable or destroy versions you no longer use, and read a secret once per instance rather than once per request.",[340,840,841,844],{},[17,842,843],{},"Cloud Logging"," charges for ingestion beyond the free monthly allotment. Logging full request payloads is expensive, and it can also write customer data into your logs.",[340,846,847,850],{},[17,848,849],{},"Artifact Registry"," stores a container image for every function deploy. A cleanup policy keeps only the recent ones.",[25,852,854],{"id":853},"step-7-ai-and-vertex-ai-usage","Step 7: AI and Vertex AI usage",[14,856,857],{},"If your app calls Gemini or another model, look at tokens per user action, not just total spend. Common fixes are caching repeated prompts, trimming the context you send, and putting a per-user or per-workspace limit on expensive features. If you resell AI features, check that your price covers your cost per call; that is a revenue leak, not a cloud cost, but it shows up in the same audit.",[25,859,861],{"id":860},"rank-the-savings-then-decide","Rank the savings, then decide",[14,863,864],{},"Write every finding in one table and sort it by dollars a month:",[30,866,867,887],{},[33,868,869],{},[36,870,871,874,878,881,884],{},[39,872,873],{},"Finding",[39,875,877],{"align":876},"right","$/month",[39,879,880],{},"User-visible risk",[39,882,883],{},"Effort",[39,885,886],{},"How to roll back",[52,888,889,909,925],{},[36,890,891,894,897,900,903],{},[57,892,893],{},"Example: min instances on a nightly job",[57,895,896],{"align":876},"measured",[57,898,899],{},"None",[57,901,902],{},"1 line",[57,904,905,906,908],{},"Set ",[621,907,649],{}," back",[36,910,911,914,916,919,922],{},[57,912,913],{},"Example: collection listener on the dashboard",[57,915,896],{"align":876},[57,917,918],{},"Low",[57,920,921],{},"Half a day",[57,923,924],{},"Revert the query",[36,926,927,930,932,934,936],{},[57,928,929],{},"Example: 1 GiB on a 120 MiB function",[57,931,896],{"align":876},[57,933,918],{},[57,935,902],{},[57,937,938],{},"Raise memory again",[14,940,941],{},"Apply the low-risk, high-dollar rows first. Leave anything on a latency-sensitive path until you've watched it in staging.",[25,943,945],{"id":944},"when-to-get-help","When to get help",[14,947,948,949,952,953,957],{},"If the bill matters but nobody on the team has a free week, App369 runs this audit for a ",[72,950,951],{"href":552},"fixed $750",", delivered in 5 business days with read-only, temporary access. You get the bill by service, the ranked savings table, a CSV of every service, and the exact commands to apply each change. Nothing in your project is changed. If you are also worried about who can read what, the ",[72,954,956],{"href":955},"/blog/firebase-security-guide-2026/","Firebase Security Rules guide"," covers the access side.",[25,959,961],{"id":960},"frequently-asked-questions","Frequently Asked Questions",[963,964,966],"h3",{"id":965},"why-is-my-firebase-bill-so-high-when-traffic-is-low","Why is my Firebase bill so high when traffic is low?",[14,968,969],{},"Usually because something runs without traffic: minimum instances kept warm around the clock, scheduled functions that scan collections, or triggers that re-fire. Low traffic with a high bill points to background work or idle capacity, not to users.",[963,971,973],{"id":972},"do-minimum-instances-cost-money-when-nobody-calls-the-function","Do minimum instances cost money when nobody calls the function?",[14,975,976],{},"Yes. A minimum instance is kept running so it can answer without a cold start, and it is billed while idle. Keep minimum instances only on functions where a user is waiting for the response.",[963,978,980],{"id":979},"how-do-i-see-the-cost-of-each-cloud-function","How do I see the cost of each Cloud Function?",[14,982,983],{},"Turn on Cloud Billing export to BigQuery for actual spend by SKU, and use Cloud Monitoring's request count and billable instance time per Cloud Run service. 2nd gen functions appear as Cloud Run services, so each function's usage can be priced separately.",[963,985,987],{"id":986},"will-cutting-costs-make-my-app-slower","Will cutting costs make my app slower?",[14,989,990],{},"Only if you cut on the wrong path. Rank each saving by user-visible risk: background jobs, webhooks and reports can usually scale to zero, while login, checkout and screen-loading functions may need warm instances.",[963,992,994],{"id":993},"can-i-lower-my-firebase-bill-without-changing-code","Can I lower my Firebase bill without changing code?",[14,996,997],{},"Partly. Minimum instances, memory, concurrency and timeouts can be changed in configuration. Firestore read patterns, trigger loops and logging volume usually need code changes, and settings changed only in the console are overwritten by the next deploy if the code says otherwise.",[999,1000,1001],"style",{},"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}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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":450,"searchDepth":451,"depth":451,"links":1003},[1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014],{"id":557,"depth":451,"text":558},{"id":595,"depth":451,"text":596},{"id":642,"depth":451,"text":643},{"id":729,"depth":451,"text":730},{"id":757,"depth":451,"text":758},{"id":805,"depth":451,"text":806},{"id":826,"depth":451,"text":827},{"id":853,"depth":451,"text":854},{"id":860,"depth":451,"text":861},{"id":944,"depth":451,"text":945},{"id":960,"depth":451,"text":961,"children":1015},[1016,1017,1018,1019,1020],{"id":965,"depth":704,"text":966},{"id":972,"depth":704,"text":973},{"id":979,"depth":704,"text":980},{"id":986,"depth":704,"text":987},{"id":993,"depth":704,"text":994},"2026-10-12T09:00:00.000Z","A line-by-line method for finding where a Firebase or Google Cloud bill goes: measure usage, price every Cloud Function and Cloud Run service, then rank savings by dollars a month and risk.",{"readingTimeStats":1024},{"text":1025,"minutes":1026,"time":1027,"words":1028},"7 min read",6.435,386100,1287,"/blog/how-to-cut-your-firebase-bill-2026","10 min",{"title":536,"description":1022},"blog/how-to-cut-your-firebase-bill-2026","firebase cost, firebase pricing, cloud functions cost, cloud run cost, google cloud cost optimization, firestore reads","2026-10-12T10:00:00.000Z","https://app369.com/blog/how-to-cut-your-firebase-bill-2026/","v_wPlLxHTHTOyc-RE0EnxEsPoKnKGx8og3UJ9ksyPOs",{"id":1038,"title":1039,"author":1040,"body":1042,"category":458,"createdAt":2407,"description":2408,"disclosure":461,"extension":462,"faq":461,"howTo":461,"location":476,"meta":2409,"navigation":483,"ogImage":461,"path":2415,"primaryKeyword":461,"readingTime":2416,"related":2417,"seo":2420,"sources":461,"stem":2421,"tags":2422,"takeaways":461,"updatedAt":2423,"url":2424,"__hash__":2425},"blog/blog/firebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":1041},{"twitter":9},{"type":11,"value":1043,"toc":2391},[1044,1056,1064,1068,1071,1080,1083,1103,1106,1110,1127,1130,1182,1185,1191,1229,1232,1245,1249,1258,1261,1398,1401,1462,1465,1469,1478,1481,1534,1537,1589,1598,1601,1625,1629,1632,1635,1738,1741,1819,1822,1826,1835,1843,1848,1867,1876,1988,1991,2034,2038,2041,2044,2050,2139,2145,2195,2201,2266,2269,2273,2279,2323,2334,2338,2342,2350,2354,2357,2361,2376,2380,2388],[14,1045,1046,1049,1050,1055],{},[17,1047,1048],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[72,1051,1054],{"href":1052,"rel":1053},"https://www.audityour.app/guides/firebase-security-rules-guide",[76],"AuditYourApp's Firebase security analysis",", roughly 30% of Firebase applications in production have overly permissive rules on at least one collection — meaning nearly one in three apps allows unauthorized reads, writes, or both. Firebase does not enforce any access restrictions by default beyond the initial test/production mode selection, so every permission must be explicitly defined by the developer.",[14,1057,1058,1059,1063],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[72,1060,1062],{"href":1061},"/service/mobile-apps/","App369"," uses to secure Firebase backends across production applications.",[25,1065,1067],{"id":1066},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,1069,1070],{},"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,1072,1073,1074,1079],{},"According to ",[72,1075,1078],{"href":1076,"rel":1077},"https://firebase.google.com/docs/rules/",[76],"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,1081,1082],{},"Rules apply to three Firebase services independently:",[423,1084,1085,1091,1097],{},[340,1086,1087,1090],{},[17,1088,1089],{},"Cloud Firestore"," — document-level read/write control with path-based matching",[340,1092,1093,1096],{},[17,1094,1095],{},"Realtime Database"," — node-level access with cascading permissions",[340,1098,1099,1102],{},[17,1100,1101],{},"Cloud Storage"," — file-level access based on path, metadata, and authentication state",[14,1104,1105],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[25,1107,1109],{"id":1108},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,1111,1112,1113,1116,1117,1120,1121,1126],{},"Firebase's setup wizard offers two starting configurations: ",[17,1114,1115],{},"test mode"," and ",[17,1118,1119],{},"locked mode",". Test mode grants public read and write access to the entire database for 30 days. According to ",[72,1122,1125],{"href":1123,"rel":1124},"https://vibeappscanner.com/is-firebase-safe",[76],"VibeAppScanner's Firebase safety analysis",", deploying with test mode rules is the single most common Firebase security failure in production.",[14,1128,1129],{},"Test mode rules look like this:",[652,1131,1135],{"className":1132,"code":1133,"language":1134,"meta":450,"style":450},"language-javascript shiki shiki-themes github-light github-dark","rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n    }\n  }\n}\n","javascript",[621,1136,1137,1142,1147,1152,1158,1164,1170,1176],{"__ignoreMap":450},[660,1138,1139],{"class":662,"line":663},[660,1140,1141],{},"rules_version = '2';\n",[660,1143,1144],{"class":662,"line":451},[660,1145,1146],{},"service cloud.firestore {\n",[660,1148,1149],{"class":662,"line":704},[660,1150,1151],{},"  match /databases/{database}/documents {\n",[660,1153,1155],{"class":662,"line":1154},4,[660,1156,1157],{},"    match /{document=**} {\n",[660,1159,1161],{"class":662,"line":1160},5,[660,1162,1163],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[660,1165,1167],{"class":662,"line":1166},6,[660,1168,1169],{},"    }\n",[660,1171,1173],{"class":662,"line":1172},7,[660,1174,1175],{},"  }\n",[660,1177,1179],{"class":662,"line":1178},8,[660,1180,1181],{},"}\n",[14,1183,1184],{},"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,1186,1187,1188,1190],{},"The correct starting point is ",[17,1189,1119],{},", where all access is denied by default:",[652,1192,1194],{"className":1132,"code":1193,"language":1134,"meta":450,"style":450},"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if false;\n    }\n  }\n}\n",[621,1195,1196,1200,1204,1208,1212,1217,1221,1225],{"__ignoreMap":450},[660,1197,1198],{"class":662,"line":663},[660,1199,1141],{},[660,1201,1202],{"class":662,"line":451},[660,1203,1146],{},[660,1205,1206],{"class":662,"line":704},[660,1207,1151],{},[660,1209,1210],{"class":662,"line":1154},[660,1211,1157],{},[660,1213,1214],{"class":662,"line":1160},[660,1215,1216],{},"      allow read, write: if false;\n",[660,1218,1219],{"class":662,"line":1166},[660,1220,1169],{},[660,1222,1223],{"class":662,"line":1172},[660,1224,1175],{},[660,1226,1227],{"class":662,"line":1178},[660,1228,1181],{},[14,1230,1231],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,1233,1234,1235,1239,1240,1244],{},"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 ",[72,1236,1238],{"href":1237},"/fixed-price/ai-app-rescue/","AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[72,1241,1243],{"href":1242},"/tools/supabase-oct-30-checker/","Supabase October 30 grants checker"," lists the tables that will need explicit grants after that date.",[25,1246,1248],{"id":1247},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,1250,1251,1252,1257],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[72,1253,1256],{"href":1254,"rel":1255},"https://firebase.google.com/docs/firestore/security/get-started",[76],"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,1259,1260],{},"Here is a production-ready pattern for a user-owned data collection:",[652,1262,1264],{"className":1132,"code":1263,"language":1134,"meta":450,"style":450},"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n\n    // Users can only read and write their own profile\n    match /users/{userId} {\n      allow read, write: if request.auth != null && request.auth.uid == userId;\n    }\n\n    // Posts are publicly readable, but only the author can write\n    match /posts/{postId} {\n      allow read: if true;\n      allow create: if request.auth != null\n                    && request.resource.data.authorId == request.auth.uid;\n      allow update, delete: if request.auth != null\n                            && resource.data.authorId == request.auth.uid;\n    }\n\n    // Admin-only collection\n    match /adminConfig/{docId} {\n      allow read, write: if request.auth != null\n                         && request.auth.token.admin == true;\n    }\n  }\n}\n",[621,1265,1266,1270,1274,1278,1283,1288,1293,1298,1302,1307,1313,1319,1325,1331,1337,1343,1349,1354,1359,1365,1371,1377,1383,1388,1393],{"__ignoreMap":450},[660,1267,1268],{"class":662,"line":663},[660,1269,1141],{},[660,1271,1272],{"class":662,"line":451},[660,1273,1146],{},[660,1275,1276],{"class":662,"line":704},[660,1277,1151],{},[660,1279,1280],{"class":662,"line":1154},[660,1281,1282],{"emptyLinePlaceholder":483},"\n",[660,1284,1285],{"class":662,"line":1160},[660,1286,1287],{},"    // Users can only read and write their own profile\n",[660,1289,1290],{"class":662,"line":1166},[660,1291,1292],{},"    match /users/{userId} {\n",[660,1294,1295],{"class":662,"line":1172},[660,1296,1297],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[660,1299,1300],{"class":662,"line":1178},[660,1301,1169],{},[660,1303,1305],{"class":662,"line":1304},9,[660,1306,1282],{"emptyLinePlaceholder":483},[660,1308,1310],{"class":662,"line":1309},10,[660,1311,1312],{},"    // Posts are publicly readable, but only the author can write\n",[660,1314,1316],{"class":662,"line":1315},11,[660,1317,1318],{},"    match /posts/{postId} {\n",[660,1320,1322],{"class":662,"line":1321},12,[660,1323,1324],{},"      allow read: if true;\n",[660,1326,1328],{"class":662,"line":1327},13,[660,1329,1330],{},"      allow create: if request.auth != null\n",[660,1332,1334],{"class":662,"line":1333},14,[660,1335,1336],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[660,1338,1340],{"class":662,"line":1339},15,[660,1341,1342],{},"      allow update, delete: if request.auth != null\n",[660,1344,1346],{"class":662,"line":1345},16,[660,1347,1348],{},"                            && resource.data.authorId == request.auth.uid;\n",[660,1350,1352],{"class":662,"line":1351},17,[660,1353,1169],{},[660,1355,1357],{"class":662,"line":1356},18,[660,1358,1282],{"emptyLinePlaceholder":483},[660,1360,1362],{"class":662,"line":1361},19,[660,1363,1364],{},"    // Admin-only collection\n",[660,1366,1368],{"class":662,"line":1367},20,[660,1369,1370],{},"    match /adminConfig/{docId} {\n",[660,1372,1374],{"class":662,"line":1373},21,[660,1375,1376],{},"      allow read, write: if request.auth != null\n",[660,1378,1380],{"class":662,"line":1379},22,[660,1381,1382],{},"                         && request.auth.token.admin == true;\n",[660,1384,1386],{"class":662,"line":1385},23,[660,1387,1169],{},[660,1389,1391],{"class":662,"line":1390},24,[660,1392,1175],{},[660,1394,1396],{"class":662,"line":1395},25,[660,1397,1181],{},[14,1399,1400],{},"Key principles for effective rules:",[423,1402,1403,1426,1435,1448],{},[340,1404,1405,775,1408,1116,1411,1414,1415,785,1418,1421,1422,1425],{},[17,1406,1407],{},"Separate read and write permissions.",[621,1409,1410],{},"allow read",[621,1412,1413],{},"allow write"," independently. Break write into ",[621,1416,1417],{},"create",[621,1419,1420],{},"update",", and ",[621,1423,1424],{},"delete"," when different logic applies.",[340,1427,1428,1434],{},[17,1429,1430,1431,726],{},"Always check ",[621,1432,1433],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[340,1436,1437,1116,1444,1447],{},[17,1438,1439,1440,1443],{},"Use ",[621,1441,1442],{},"resource.data"," for existing document fields",[621,1445,1446],{},"request.resource.data"," for incoming data in create/update operations.",[340,1449,1450,1453,1454,1457,1458,1461],{},[17,1451,1452],{},"Avoid wildcards at the root level."," The pattern ",[621,1455,1456],{},"match /{document=**}"," applies to every document in the database. Use it only with ",[621,1459,1460],{},"if false"," as a catch-all deny rule.",[14,1463,1464],{},"Structure your database with security in mind. Organize collections by permission boundary — keep admin-only data in separate collections rather than mixing it with user-readable documents.",[25,1466,1468],{"id":1467},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,1470,1471,1472,1477],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[72,1473,1476],{"href":1474,"rel":1475},"https://firebase.google.com/docs/auth/admin/custom-claims",[76],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,1479,1480],{},"Set custom claims from a Cloud Function or admin server:",[652,1482,1484],{"className":1132,"code":1483,"language":1134,"meta":450,"style":450},"const admin = require('firebase-admin');\n\n// Assign admin role\nawait admin.auth().setCustomUserClaims(userId, { admin: true });\n\n// Assign editor role with department\nawait admin.auth().setCustomUserClaims(userId, {\n  role: 'editor',\n  department: 'marketing'\n});\n",[621,1485,1486,1491,1495,1500,1505,1509,1514,1519,1524,1529],{"__ignoreMap":450},[660,1487,1488],{"class":662,"line":663},[660,1489,1490],{},"const admin = require('firebase-admin');\n",[660,1492,1493],{"class":662,"line":451},[660,1494,1282],{"emptyLinePlaceholder":483},[660,1496,1497],{"class":662,"line":704},[660,1498,1499],{},"// Assign admin role\n",[660,1501,1502],{"class":662,"line":1154},[660,1503,1504],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[660,1506,1507],{"class":662,"line":1160},[660,1508,1282],{"emptyLinePlaceholder":483},[660,1510,1511],{"class":662,"line":1166},[660,1512,1513],{},"// Assign editor role with department\n",[660,1515,1516],{"class":662,"line":1172},[660,1517,1518],{},"await admin.auth().setCustomUserClaims(userId, {\n",[660,1520,1521],{"class":662,"line":1178},[660,1522,1523],{},"  role: 'editor',\n",[660,1525,1526],{"class":662,"line":1304},[660,1527,1528],{},"  department: 'marketing'\n",[660,1530,1531],{"class":662,"line":1309},[660,1532,1533],{},"});\n",[14,1535,1536],{},"Reference claims in Security Rules:",[652,1538,1540],{"className":1132,"code":1539,"language":1134,"meta":450,"style":450},"match /content/{docId} {\n  // Admins have full access\n  allow read, write: if request.auth.token.admin == true;\n\n  // Editors can read and update, but not delete\n  allow read, update: if request.auth.token.role == 'editor';\n\n  // All authenticated users can read\n  allow read: if request.auth != null;\n}\n",[621,1541,1542,1547,1552,1557,1561,1566,1571,1575,1580,1585],{"__ignoreMap":450},[660,1543,1544],{"class":662,"line":663},[660,1545,1546],{},"match /content/{docId} {\n",[660,1548,1549],{"class":662,"line":451},[660,1550,1551],{},"  // Admins have full access\n",[660,1553,1554],{"class":662,"line":704},[660,1555,1556],{},"  allow read, write: if request.auth.token.admin == true;\n",[660,1558,1559],{"class":662,"line":1154},[660,1560,1282],{"emptyLinePlaceholder":483},[660,1562,1563],{"class":662,"line":1160},[660,1564,1565],{},"  // Editors can read and update, but not delete\n",[660,1567,1568],{"class":662,"line":1166},[660,1569,1570],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[660,1572,1573],{"class":662,"line":1172},[660,1574,1282],{"emptyLinePlaceholder":483},[660,1576,1577],{"class":662,"line":1178},[660,1578,1579],{},"  // All authenticated users can read\n",[660,1581,1582],{"class":662,"line":1304},[660,1583,1584],{},"  allow read: if request.auth != null;\n",[660,1586,1587],{"class":662,"line":1309},[660,1588,1181],{},[14,1590,1591,1592,1597],{},"As noted in ",[72,1593,1596],{"href":1594,"rel":1595},"https://medium.com/@NadunKulatunge/advanced-use-cases-and-best-practices-of-firebase-security-rules-cb5d3339d8a7",[76],"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,1599,1600],{},"Implementation guidelines:",[423,1602,1603,1609,1619],{},[340,1604,1605,1608],{},[17,1606,1607],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[340,1610,1611,1614,1615,1618],{},[17,1612,1613],{},"Force token refresh after claim changes."," Call ",[621,1616,1617],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[340,1620,1621,1624],{},[17,1622,1623],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[25,1626,1628],{"id":1627},"data-validation-in-security-rules","Data Validation in Security Rules",[14,1630,1631],{},"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,1633,1634],{},"Validation patterns for common requirements:",[652,1636,1638],{"className":1132,"code":1637,"language":1134,"meta":450,"style":450},"match /users/{userId} {\n  allow create: if request.auth.uid == userId\n    // Required fields must exist\n    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n    // Name must be a string between 2 and 100 characters\n    && request.resource.data.name is string\n    && request.resource.data.name.size() >= 2\n    && request.resource.data.name.size() \u003C= 100\n    // Email must be a string and match the auth email\n    && request.resource.data.email is string\n    && request.resource.data.email == request.auth.token.email\n    // createdAt must be a timestamp\n    && request.resource.data.createdAt is timestamp\n    // No unexpected fields allowed\n    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n\n  allow update: if request.auth.uid == userId\n    // Prevent changing the email field\n    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n}\n",[621,1639,1640,1645,1650,1655,1660,1665,1670,1675,1680,1685,1690,1695,1700,1705,1710,1715,1719,1724,1729,1734],{"__ignoreMap":450},[660,1641,1642],{"class":662,"line":663},[660,1643,1644],{},"match /users/{userId} {\n",[660,1646,1647],{"class":662,"line":451},[660,1648,1649],{},"  allow create: if request.auth.uid == userId\n",[660,1651,1652],{"class":662,"line":704},[660,1653,1654],{},"    // Required fields must exist\n",[660,1656,1657],{"class":662,"line":1154},[660,1658,1659],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[660,1661,1662],{"class":662,"line":1160},[660,1663,1664],{},"    // Name must be a string between 2 and 100 characters\n",[660,1666,1667],{"class":662,"line":1166},[660,1668,1669],{},"    && request.resource.data.name is string\n",[660,1671,1672],{"class":662,"line":1172},[660,1673,1674],{},"    && request.resource.data.name.size() >= 2\n",[660,1676,1677],{"class":662,"line":1178},[660,1678,1679],{},"    && request.resource.data.name.size() \u003C= 100\n",[660,1681,1682],{"class":662,"line":1304},[660,1683,1684],{},"    // Email must be a string and match the auth email\n",[660,1686,1687],{"class":662,"line":1309},[660,1688,1689],{},"    && request.resource.data.email is string\n",[660,1691,1692],{"class":662,"line":1315},[660,1693,1694],{},"    && request.resource.data.email == request.auth.token.email\n",[660,1696,1697],{"class":662,"line":1321},[660,1698,1699],{},"    // createdAt must be a timestamp\n",[660,1701,1702],{"class":662,"line":1327},[660,1703,1704],{},"    && request.resource.data.createdAt is timestamp\n",[660,1706,1707],{"class":662,"line":1333},[660,1708,1709],{},"    // No unexpected fields allowed\n",[660,1711,1712],{"class":662,"line":1339},[660,1713,1714],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[660,1716,1717],{"class":662,"line":1345},[660,1718,1282],{"emptyLinePlaceholder":483},[660,1720,1721],{"class":662,"line":1351},[660,1722,1723],{},"  allow update: if request.auth.uid == userId\n",[660,1725,1726],{"class":662,"line":1356},[660,1727,1728],{},"    // Prevent changing the email field\n",[660,1730,1731],{"class":662,"line":1361},[660,1732,1733],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[660,1735,1736],{"class":662,"line":1367},[660,1737,1181],{},[14,1739,1740],{},"Validation capabilities in Firestore rules include:",[423,1742,1743,1771,1781,1795,1804,1810],{},[340,1744,1745,1748,1749,785,1752,785,1755,785,1758,785,1761,785,1764,1767,1768],{},[17,1746,1747],{},"Type checking"," — verify that fields are ",[621,1750,1751],{},"string",[621,1753,1754],{},"int",[621,1756,1757],{},"float",[621,1759,1760],{},"bool",[621,1762,1763],{},"timestamp",[621,1765,1766],{},"list",", or ",[621,1769,1770],{},"map",[340,1772,1773,1776,1777,1780],{},[17,1774,1775],{},"String length"," — use ",[621,1778,1779],{},".size()"," to enforce minimum and maximum lengths",[340,1782,1783,1786,1787,1790,1791,1794],{},[17,1784,1785],{},"Field existence"," — ",[621,1788,1789],{},"hasAll()"," for required fields, ",[621,1792,1793],{},"hasOnly()"," to prevent extra fields",[340,1796,1797,1776,1800,1803],{},[17,1798,1799],{},"Field immutability",[621,1801,1802],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[340,1805,1806,1809],{},[17,1807,1808],{},"Numeric ranges"," — enforce minimum and maximum values for numbers",[340,1811,1812,1815,1816,1818],{},[17,1813,1814],{},"List constraints"," — check ",[621,1817,1779],{}," on lists to limit array lengths",[14,1820,1821],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[25,1823,1825],{"id":1824},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,1827,1828,1829,1834],{},"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; ",[72,1830,1833],{"href":1831,"rel":1832},"https://firebase.google.com/docs/rules/unit-tests",[76],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,1836,1837,1838,1842],{},"For a quick first pass, paste your rules into App369's free ",[72,1839,1841],{"href":1840},"/tools/firebase-rules-checker/","Firebase Security Rules checker",". It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.",[14,1844,1845],{},[17,1846,1847],{},"Use the Firebase Emulator Suite for local testing:",[652,1849,1851],{"className":654,"code":1850,"language":656,"meta":450,"style":450},"firebase emulators:start --only firestore\n",[621,1852,1853],{"__ignoreMap":450},[660,1854,1855,1858,1861,1864],{"class":662,"line":663},[660,1856,1857],{"class":666},"firebase",[660,1859,1860],{"class":670}," emulators:start",[660,1862,1863],{"class":680}," --only",[660,1865,1866],{"class":670}," firestore\n",[14,1868,1869],{},[17,1870,1871,1872,1875],{},"Write unit tests with the ",[621,1873,1874],{},"@firebase/rules-unit-testing"," package:",[652,1877,1879],{"className":1132,"code":1878,"language":1134,"meta":450,"style":450},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase/rules-unit-testing');\n\nconst testEnv = await initializeTestEnvironment({\n  projectId: 'your-project-id',\n  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n});\n\n// Test: authenticated user can read their own document\nconst userContext = testEnv.authenticatedContext('user123');\nawait assertSucceeds(\n  userContext.firestore().collection('users').doc('user123').get()\n);\n\n// Test: user cannot read another user's document\nawait assertFails(\n  userContext.firestore().collection('users').doc('other-user').get()\n);\n\n// Test: unauthenticated user cannot write\nconst unauthedContext = testEnv.unauthenticatedContext();\nawait assertFails(\n  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n);\n",[621,1880,1881,1886,1890,1895,1900,1905,1909,1913,1918,1923,1928,1933,1938,1942,1947,1952,1957,1961,1965,1970,1975,1979,1984],{"__ignoreMap":450},[660,1882,1883],{"class":662,"line":663},[660,1884,1885],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase/rules-unit-testing');\n",[660,1887,1888],{"class":662,"line":451},[660,1889,1282],{"emptyLinePlaceholder":483},[660,1891,1892],{"class":662,"line":704},[660,1893,1894],{},"const testEnv = await initializeTestEnvironment({\n",[660,1896,1897],{"class":662,"line":1154},[660,1898,1899],{},"  projectId: 'your-project-id',\n",[660,1901,1902],{"class":662,"line":1160},[660,1903,1904],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[660,1906,1907],{"class":662,"line":1166},[660,1908,1533],{},[660,1910,1911],{"class":662,"line":1172},[660,1912,1282],{"emptyLinePlaceholder":483},[660,1914,1915],{"class":662,"line":1178},[660,1916,1917],{},"// Test: authenticated user can read their own document\n",[660,1919,1920],{"class":662,"line":1304},[660,1921,1922],{},"const userContext = testEnv.authenticatedContext('user123');\n",[660,1924,1925],{"class":662,"line":1309},[660,1926,1927],{},"await assertSucceeds(\n",[660,1929,1930],{"class":662,"line":1315},[660,1931,1932],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[660,1934,1935],{"class":662,"line":1321},[660,1936,1937],{},");\n",[660,1939,1940],{"class":662,"line":1327},[660,1941,1282],{"emptyLinePlaceholder":483},[660,1943,1944],{"class":662,"line":1333},[660,1945,1946],{},"// Test: user cannot read another user's document\n",[660,1948,1949],{"class":662,"line":1339},[660,1950,1951],{},"await assertFails(\n",[660,1953,1954],{"class":662,"line":1345},[660,1955,1956],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[660,1958,1959],{"class":662,"line":1351},[660,1960,1937],{},[660,1962,1963],{"class":662,"line":1356},[660,1964,1282],{"emptyLinePlaceholder":483},[660,1966,1967],{"class":662,"line":1361},[660,1968,1969],{},"// Test: unauthenticated user cannot write\n",[660,1971,1972],{"class":662,"line":1367},[660,1973,1974],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[660,1976,1977],{"class":662,"line":1373},[660,1978,1951],{},[660,1980,1981],{"class":662,"line":1379},[660,1982,1983],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[660,1985,1986],{"class":662,"line":1385},[660,1987,1937],{},[14,1989,1990],{},"Deployment workflow:",[337,1992,1993,2008,2014,2022,2028],{},[340,1994,1995,1998,1999,785,2002,1421,2005],{},[17,1996,1997],{},"Write rules locally"," in ",[621,2000,2001],{},"firestore.rules",[621,2003,2004],{},"database.rules.json",[621,2006,2007],{},"storage.rules",[340,2009,2010,2013],{},[17,2011,2012],{},"Test with the emulator"," — run the full test suite against the local emulator before every deployment",[340,2015,2016,2021],{},[17,2017,1439,2018],{},[621,2019,2020],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[340,2023,2024,2027],{},[17,2025,2026],{},"Version control your rules files"," — commit them to Git and review changes in pull requests",[340,2029,2030,2033],{},[17,2031,2032],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[25,2035,2037],{"id":2036},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,2039,2040],{},"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,2042,2043],{},"Use Cloud Functions as a second validation layer for:",[14,2045,2046,2049],{},[17,2047,2048],{},"Financial transactions"," — verify amounts, check account balances, and enforce rate limits server-side:",[652,2051,2053],{"className":1132,"code":2052,"language":1134,"meta":450,"style":450},"exports.processPayment = functions.https.onCall(async (data, context) => {\n  // Verify authentication\n  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n\n  // Server-side amount validation\n  if (data.amount \u003C= 0 || data.amount > 10000) {\n    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n  }\n\n  // Check user's balance from a trusted source\n  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n  if (userDoc.data().balance \u003C data.amount) {\n    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n  }\n\n  // Process the transaction with admin privileges\n  // ...\n});\n",[621,2054,2055,2060,2065,2070,2074,2079,2084,2089,2093,2097,2102,2107,2112,2117,2121,2125,2130,2135],{"__ignoreMap":450},[660,2056,2057],{"class":662,"line":663},[660,2058,2059],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[660,2061,2062],{"class":662,"line":451},[660,2063,2064],{},"  // Verify authentication\n",[660,2066,2067],{"class":662,"line":704},[660,2068,2069],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[660,2071,2072],{"class":662,"line":1154},[660,2073,1282],{"emptyLinePlaceholder":483},[660,2075,2076],{"class":662,"line":1160},[660,2077,2078],{},"  // Server-side amount validation\n",[660,2080,2081],{"class":662,"line":1166},[660,2082,2083],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[660,2085,2086],{"class":662,"line":1172},[660,2087,2088],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[660,2090,2091],{"class":662,"line":1178},[660,2092,1175],{},[660,2094,2095],{"class":662,"line":1304},[660,2096,1282],{"emptyLinePlaceholder":483},[660,2098,2099],{"class":662,"line":1309},[660,2100,2101],{},"  // Check user's balance from a trusted source\n",[660,2103,2104],{"class":662,"line":1315},[660,2105,2106],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[660,2108,2109],{"class":662,"line":1321},[660,2110,2111],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[660,2113,2114],{"class":662,"line":1327},[660,2115,2116],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[660,2118,2119],{"class":662,"line":1333},[660,2120,1175],{},[660,2122,2123],{"class":662,"line":1339},[660,2124,1282],{"emptyLinePlaceholder":483},[660,2126,2127],{"class":662,"line":1345},[660,2128,2129],{},"  // Process the transaction with admin privileges\n",[660,2131,2132],{"class":662,"line":1351},[660,2133,2134],{},"  // ...\n",[660,2136,2137],{"class":662,"line":1356},[660,2138,1533],{},[14,2140,2141,2144],{},[17,2142,2143],{},"Data aggregation and denormalization"," — use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[652,2146,2148],{"className":1132,"code":2147,"language":1134,"meta":450,"style":450},"exports.onPostCreated = functions.firestore\n  .document('posts/{postId}')\n  .onCreate(async (snap, context) => {\n    // Increment the author's post count atomically\n    const authorId = snap.data().authorId;\n    await admin.firestore().collection('users').doc(authorId).update({\n      postCount: admin.firestore.FieldValue.increment(1)\n    });\n  });\n",[621,2149,2150,2155,2160,2165,2170,2175,2180,2185,2190],{"__ignoreMap":450},[660,2151,2152],{"class":662,"line":663},[660,2153,2154],{},"exports.onPostCreated = functions.firestore\n",[660,2156,2157],{"class":662,"line":451},[660,2158,2159],{},"  .document('posts/{postId}')\n",[660,2161,2162],{"class":662,"line":704},[660,2163,2164],{},"  .onCreate(async (snap, context) => {\n",[660,2166,2167],{"class":662,"line":1154},[660,2168,2169],{},"    // Increment the author's post count atomically\n",[660,2171,2172],{"class":662,"line":1160},[660,2173,2174],{},"    const authorId = snap.data().authorId;\n",[660,2176,2177],{"class":662,"line":1166},[660,2178,2179],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[660,2181,2182],{"class":662,"line":1172},[660,2183,2184],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[660,2186,2187],{"class":662,"line":1178},[660,2188,2189],{},"    });\n",[660,2191,2192],{"class":662,"line":1304},[660,2193,2194],{},"  });\n",[14,2196,2197,2200],{},[17,2198,2199],{},"Rate limiting"," — prevent abuse by tracking request frequency per user:",[652,2202,2204],{"className":1132,"code":2203,"language":1134,"meta":450,"style":450},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n  const userId = context.auth.uid;\n  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n\n  if (lastSubmission.exists) {\n    const lastTime = lastSubmission.data().timestamp.toMillis();\n    if (Date.now() - lastTime \u003C 60000) { // 1 minute cooldown\n      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n    }\n  }\n\n  // Process the submission...\n});\n",[621,2205,2206,2211,2216,2221,2225,2230,2235,2240,2245,2249,2253,2257,2262],{"__ignoreMap":450},[660,2207,2208],{"class":662,"line":663},[660,2209,2210],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[660,2212,2213],{"class":662,"line":451},[660,2214,2215],{},"  const userId = context.auth.uid;\n",[660,2217,2218],{"class":662,"line":704},[660,2219,2220],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[660,2222,2223],{"class":662,"line":1154},[660,2224,1282],{"emptyLinePlaceholder":483},[660,2226,2227],{"class":662,"line":1160},[660,2228,2229],{},"  if (lastSubmission.exists) {\n",[660,2231,2232],{"class":662,"line":1166},[660,2233,2234],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[660,2236,2237],{"class":662,"line":1172},[660,2238,2239],{},"    if (Date.now() - lastTime \u003C 60000) { // 1 minute cooldown\n",[660,2241,2242],{"class":662,"line":1178},[660,2243,2244],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[660,2246,2247],{"class":662,"line":1304},[660,2248,1169],{},[660,2250,2251],{"class":662,"line":1309},[660,2252,1175],{},[660,2254,2255],{"class":662,"line":1315},[660,2256,1282],{"emptyLinePlaceholder":483},[660,2258,2259],{"class":662,"line":1321},[660,2260,2261],{},"  // Process the submission...\n",[660,2263,2264],{"class":662,"line":1327},[660,2265,1533],{},[14,2267,2268],{},"Cloud Functions complement Security Rules — they do not replace them. Rules should still enforce basic authentication and authorization checks. Functions handle the logic that rules cannot express.",[25,2270,2272],{"id":2271},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,2274,2275,2278],{},[72,2276,1062],{"href":2277},"/service/flutter-development/"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[423,2280,2281,2287,2293,2299,2305,2311],{},[340,2282,2283,2286],{},[17,2284,2285],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[340,2288,2289,2292],{},[17,2290,2291],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[340,2294,2295,2298],{},[17,2296,2297],{},"Comprehensive data validation"," in rules for every writable collection — type checking, field constraints, and immutability guards",[340,2300,2301,2304],{},[17,2302,2303],{},"Automated rules testing"," in CI/CD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[340,2306,2307,2310],{},[17,2308,2309],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[340,2312,2313,2316,2317,2322],{},[17,2314,2315],{},"Security audits"," against the ",[72,2318,2321],{"href":2319,"rel":2320},"https://owasp.org/www-project-mobile-top-10/",[76],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,2324,2325,2326,2329,2330,726],{},"App369 builds ",[72,2327,2328],{"href":2277},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[72,2331,2333],{"href":2332},"/blog/flutter-security-best-practices-2026/","Flutter security best practices",[25,2335,2337],{"id":2336},"faq","FAQ",[963,2339,2341],{"id":2340},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,2343,2344,2345,2349],{},"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 ",[72,2346,2348],{"href":1052,"rel":2347},[76],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[963,2351,2353],{"id":2352},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,2355,2356],{},"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.",[963,2358,2360],{"id":2359},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,2362,2363,2364,2367,2368,2371,2372,2375],{},"Use Firebase Authentication custom claims. Set ",[621,2365,2366],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[621,2369,2370],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[621,2373,2374],{},"users/{userId}.isAdmin"," for authorization — clients can modify their own documents if write rules are misconfigured. Custom claims are set server-side and cannot be modified by clients.",[963,2377,2379],{"id":2378},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,2381,2382,2383,2387],{},"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 ",[72,2384,2386],{"href":1076,"rel":2385},[76],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[999,2389,2390],{},"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":450,"searchDepth":451,"depth":451,"links":2392},[2393,2394,2395,2396,2397,2398,2399,2400,2401],{"id":1066,"depth":451,"text":1067},{"id":1108,"depth":451,"text":1109},{"id":1247,"depth":451,"text":1248},{"id":1467,"depth":451,"text":1468},{"id":1627,"depth":451,"text":1628},{"id":1824,"depth":451,"text":1825},{"id":2036,"depth":451,"text":2037},{"id":2271,"depth":451,"text":2272},{"id":2336,"depth":451,"text":2337,"children":2402},[2403,2404,2405,2406],{"id":2340,"depth":704,"text":2341},{"id":2352,"depth":704,"text":2353},{"id":2359,"depth":704,"text":2360},{"id":2378,"depth":704,"text":2379},"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":2410},{"text":2411,"minutes":2412,"time":2413,"words":2414},"11 min read",10.415,624900,2083,"/blog/firebase-security-guide-2026","11 min",[490,2418,2419],"ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":1039,"description":2408},"blog/firebase-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://app369.com/blog/firebase-security-guide-2026/","03zzXvqm83bKHIwJCQ0nwQhHoP5iUWwZrIqQ7AgyfgY",{"id":2427,"title":2428,"author":2429,"body":2431,"category":458,"createdAt":3093,"description":3094,"disclosure":461,"extension":462,"faq":3095,"howTo":3111,"location":476,"meta":3125,"navigation":483,"ogImage":461,"path":3131,"primaryKeyword":3132,"readingTime":3133,"related":461,"seo":3134,"sources":3135,"stem":3145,"tags":3146,"takeaways":3147,"updatedAt":3153,"url":3154,"__hash__":3155},"blog/blog/supabase-permission-denied-new-tables-october-30-2026.md","Supabase \"Permission Denied\" on New Tables: the October 30 Fix",{"name":7,"social":2430},{"twitter":9},{"type":11,"value":2432,"toc":3085},[2433,2457,2463,2467,2476,2528,2566,2569,2605,2609,2612,2665,2684,2688,2697,2865,2868,2934,2938,2967,2974,2978,3056,3060,3075,3082],[14,2434,2435,2438,2439,2442,2443,785,2446,1116,2449,2452,2453,2456],{},[17,2436,2437],{},"From October 30, 2026, a table you add to an existing Supabase project is invisible to your app until you grant it."," Supabase used to give every new table in the ",[621,2440,2441],{},"public"," schema to the ",[621,2444,2445],{},"anon",[621,2447,2448],{},"authenticated",[621,2450,2451],{},"service_role"," roles automatically. It now leaves that to you. Tables you already have keep working; a table created after the change returns ",[621,2454,2455],{},"permission denied for table"," until its migration includes the grants.",[14,2458,2459,2460,2462],{},"To see which tables in your migrations are affected, paste them into the free ",[72,2461,1243],{"href":1242},". It runs in your browser and never uploads your SQL.",[25,2464,2466],{"id":2465},"what-changes-and-when","What changes, and when",[14,2468,2469,2470,2475],{},"Supabase announced the change in its ",[72,2471,2474],{"href":2472,"rel":2473},"https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically",[76],"changelog on April 28, 2026",":",[30,2477,2478,2487],{},[33,2479,2480],{},[36,2481,2482,2484],{},[39,2483,47],{},[39,2485,2486],{},"What happens",[52,2488,2489,2497,2508,2516],{},[36,2490,2491,2494],{},[57,2492,2493],{},"April 28, 2026",[57,2495,2496],{},"New projects can opt out of exposing new tables when they are created",[36,2498,2499,2502],{},[57,2500,2501],{},"May 18, 2026",[57,2503,2504,2507],{},[621,2505,2506],{},"pg_graphql"," is no longer enabled by default",[36,2509,2510,2513],{},[57,2511,2512],{},"May 30, 2026",[57,2514,2515],{},"New projects stop exposing new tables by default (rolled out over a few weeks)",[36,2517,2518,2523],{},[57,2519,2520],{},[17,2521,2522],{},"October 30, 2026",[57,2524,2525],{},[17,2526,2527],{},"The same default applies to all existing projects",[14,2529,2530,2531,2535,2536,2538,2539,2542,2543,785,2545,1116,2547,2549,2550,2553,2554,2557,2558,2561,2562,2565],{},"What changes is Postgres's ",[2532,2533,2534],"em",{},"default privileges"," in the ",[621,2537,2441],{}," schema. Before, every new table was granted ",[621,2540,2541],{},"select, insert, update, delete"," to ",[621,2544,2445],{},[621,2546,2448],{},[621,2548,2451],{},", new sequences got ",[621,2551,2552],{},"usage, select",", and new functions got ",[621,2555,2556],{},"execute",". After the change those defaults are gone, so the Data API (REST at ",[621,2559,2560],{},"/rest/v1"," and GraphQL at ",[621,2563,2564],{},"/graphql/v1",") can't see a new table until a role is granted on it.",[14,2567,2568],{},"What doesn't change:",[423,2570,2571,2577,2583,2599],{},[340,2572,2573,2576],{},[17,2574,2575],{},"Existing tables"," keep the grants they already have.",[340,2578,2579,2582],{},[17,2580,2581],{},"Row-level security"," works exactly as before.",[340,2584,2585,2588,2589,785,2592,1116,2595,2598],{},[17,2586,2587],{},"Other schemas",", such as ",[621,2590,2591],{},"auth",[621,2593,2594],{},"storage",[621,2596,2597],{},"realtime",", aren't affected.",[340,2600,2601,2604],{},[17,2602,2603],{},"Direct Postgres connections"," (psql, an ORM or your own server) aren't affected. The change matters if your app talks to Supabase through supabase-js, REST or GraphQL.",[25,2606,2608],{"id":2607},"the-symptom","The symptom",[14,2610,2611],{},"A request to a new table without grants fails with Postgres error 42501:",[652,2613,2617],{"className":2614,"code":2615,"language":2616,"meta":450,"style":450},"language-json shiki shiki-themes github-light github-dark","{\n  \"code\": \"42501\",\n  \"message\": \"permission denied for table projects\",\n  \"hint\": \"Grant the required privileges to the current role with: GRANT SELECT ON public.projects TO anon;\"\n}\n","json",[621,2618,2619,2625,2639,2651,2661],{"__ignoreMap":450},[660,2620,2621],{"class":662,"line":663},[660,2622,2624],{"class":2623},"sVt8B","{\n",[660,2626,2627,2630,2633,2636],{"class":662,"line":451},[660,2628,2629],{"class":680},"  \"code\"",[660,2631,2632],{"class":2623},": ",[660,2634,2635],{"class":670},"\"42501\"",[660,2637,2638],{"class":2623},",\n",[660,2640,2641,2644,2646,2649],{"class":662,"line":704},[660,2642,2643],{"class":680},"  \"message\"",[660,2645,2632],{"class":2623},[660,2647,2648],{"class":670},"\"permission denied for table projects\"",[660,2650,2638],{"class":2623},[660,2652,2653,2656,2658],{"class":662,"line":1154},[660,2654,2655],{"class":680},"  \"hint\"",[660,2657,2632],{"class":2623},[660,2659,2660],{"class":670},"\"Grant the required privileges to the current role with: GRANT SELECT ON public.projects TO anon;\"\n",[660,2662,2663],{"class":662,"line":1160},[660,2664,1181],{"class":2623},[14,2666,2667,2668,2671,2672,2675,2676,2679,2680,2683],{},"In supabase-js, the ",[621,2669,2670],{},"error"," object comes back filled and ",[621,2673,2674],{},"data"," is ",[621,2677,2678],{},"null",". If the app doesn't show errors, users simply see an empty screen or a button that does nothing. The same query works in the SQL editor, which runs as ",[621,2681,2682],{},"postgres",", the role that owns the table and has every privilege, and fails in the app. That is why this one can be confusing.",[25,2685,2687],{"id":2686},"the-fix-grant-in-the-same-migration","The fix: grant in the same migration",[14,2689,2690,2691,2696],{},"Put the grants in the migration that creates the table, so every environment (local, staging, production) ends up the same. This is the pattern from Supabase's ",[72,2692,2695],{"href":2693,"rel":2694},"https://supabase.com/docs/guides/api/securing-your-api",[76],"Securing your API"," guide:",[652,2698,2702],{"className":2699,"code":2700,"language":2701,"meta":450,"style":450},"language-sql shiki shiki-themes github-light github-dark","create table public.projects (\n  id uuid primary key default gen_random_uuid(),\n  user_id uuid not null references auth.users on delete cascade,\n  name text not null\n);\n\n-- 1. Who can reach the table at all\ngrant select, insert, update, delete on table public.projects to authenticated;\ngrant select, insert, update, delete on table public.projects to service_role;\n\n-- 2. Which rows they can see and change\nalter table public.projects enable row level security;\n\ncreate policy \"Owners can read their projects\"\n  on public.projects for select\n  to authenticated\n  using ((select auth.uid()) = user_id);\n\ncreate policy \"Owners can add projects\"\n  on public.projects for insert\n  to authenticated\n  with check ((select auth.uid()) = user_id);\n\ncreate policy \"Owners can update their projects\"\n  on public.projects for update\n  to authenticated\n  using ((select auth.uid()) = user_id)\n  with check ((select auth.uid()) = user_id);\n\ncreate policy \"Owners can delete their projects\"\n  on public.projects for delete\n  to authenticated\n  using ((select auth.uid()) = user_id);\n","sql",[621,2703,2704,2709,2714,2719,2724,2728,2732,2737,2742,2747,2751,2756,2761,2765,2770,2775,2780,2785,2789,2794,2799,2803,2808,2812,2817,2822,2827,2833,2838,2843,2849,2855,2860],{"__ignoreMap":450},[660,2705,2706],{"class":662,"line":663},[660,2707,2708],{},"create table public.projects (\n",[660,2710,2711],{"class":662,"line":451},[660,2712,2713],{},"  id uuid primary key default gen_random_uuid(),\n",[660,2715,2716],{"class":662,"line":704},[660,2717,2718],{},"  user_id uuid not null references auth.users on delete cascade,\n",[660,2720,2721],{"class":662,"line":1154},[660,2722,2723],{},"  name text not null\n",[660,2725,2726],{"class":662,"line":1160},[660,2727,1937],{},[660,2729,2730],{"class":662,"line":1166},[660,2731,1282],{"emptyLinePlaceholder":483},[660,2733,2734],{"class":662,"line":1172},[660,2735,2736],{},"-- 1. Who can reach the table at all\n",[660,2738,2739],{"class":662,"line":1178},[660,2740,2741],{},"grant select, insert, update, delete on table public.projects to authenticated;\n",[660,2743,2744],{"class":662,"line":1304},[660,2745,2746],{},"grant select, insert, update, delete on table public.projects to service_role;\n",[660,2748,2749],{"class":662,"line":1309},[660,2750,1282],{"emptyLinePlaceholder":483},[660,2752,2753],{"class":662,"line":1315},[660,2754,2755],{},"-- 2. Which rows they can see and change\n",[660,2757,2758],{"class":662,"line":1321},[660,2759,2760],{},"alter table public.projects enable row level security;\n",[660,2762,2763],{"class":662,"line":1327},[660,2764,1282],{"emptyLinePlaceholder":483},[660,2766,2767],{"class":662,"line":1333},[660,2768,2769],{},"create policy \"Owners can read their projects\"\n",[660,2771,2772],{"class":662,"line":1339},[660,2773,2774],{},"  on public.projects for select\n",[660,2776,2777],{"class":662,"line":1345},[660,2778,2779],{},"  to authenticated\n",[660,2781,2782],{"class":662,"line":1351},[660,2783,2784],{},"  using ((select auth.uid()) = user_id);\n",[660,2786,2787],{"class":662,"line":1356},[660,2788,1282],{"emptyLinePlaceholder":483},[660,2790,2791],{"class":662,"line":1361},[660,2792,2793],{},"create policy \"Owners can add projects\"\n",[660,2795,2796],{"class":662,"line":1367},[660,2797,2798],{},"  on public.projects for insert\n",[660,2800,2801],{"class":662,"line":1373},[660,2802,2779],{},[660,2804,2805],{"class":662,"line":1379},[660,2806,2807],{},"  with check ((select auth.uid()) = user_id);\n",[660,2809,2810],{"class":662,"line":1385},[660,2811,1282],{"emptyLinePlaceholder":483},[660,2813,2814],{"class":662,"line":1390},[660,2815,2816],{},"create policy \"Owners can update their projects\"\n",[660,2818,2819],{"class":662,"line":1395},[660,2820,2821],{},"  on public.projects for update\n",[660,2823,2825],{"class":662,"line":2824},26,[660,2826,2779],{},[660,2828,2830],{"class":662,"line":2829},27,[660,2831,2832],{},"  using ((select auth.uid()) = user_id)\n",[660,2834,2836],{"class":662,"line":2835},28,[660,2837,2807],{},[660,2839,2841],{"class":662,"line":2840},29,[660,2842,1282],{"emptyLinePlaceholder":483},[660,2844,2846],{"class":662,"line":2845},30,[660,2847,2848],{},"create policy \"Owners can delete their projects\"\n",[660,2850,2852],{"class":662,"line":2851},31,[660,2853,2854],{},"  on public.projects for delete\n",[660,2856,2858],{"class":662,"line":2857},32,[660,2859,2779],{},[660,2861,2863],{"class":662,"line":2862},33,[660,2864,2784],{},[14,2866,2867],{},"A few details are easy to miss:",[423,2869,2870,2883,2902,2925],{},[340,2871,2872,2875,2876,2878,2879,2882],{},[17,2873,2874],{},"Grant only what each role needs."," A table that signed-out visitors should never touch gets no ",[621,2877,2445],{}," grant at all. Public read-only data gets ",[621,2880,2881],{},"grant select ... to anon"," and nothing more.",[340,2884,2885,2891,2892,2894,2895,2898,2899,726],{},[17,2886,2887,2890],{},[621,2888,2889],{},"serial"," columns need a sequence grant."," If the table uses ",[621,2893,2889],{}," or ",[621,2896,2897],{},"bigserial"," rather than an identity column or a UUID, inserts also need ",[621,2900,2901],{},"grant usage, select on sequence public.projects_id_seq to authenticated;",[340,2903,2904,2909,2910,2913,2914,2917,2918,2921,2922,2924],{},[17,2905,2906,2907,726],{},"Functions called over the API need ",[621,2908,2556],{}," For a function your app calls with ",[621,2911,2912],{},"rpc()",", use ",[621,2915,2916],{},"grant execute on function public.your_function() to authenticated;",". A ",[621,2919,2920],{},"security definer"," function runs with its owner's rights and bypasses RLS, so grant ",[621,2923,2556],{}," only to roles that should call it, and review those functions.",[340,2926,2927,2930,2931,2933],{},[17,2928,2929],{},"Grants are not a replacement for RLS."," With a grant and RLS off, any signed-in user, or anyone at all if ",[621,2932,2445],{}," is granted, can read every row. Turn RLS on for every exposed table.",[25,2935,2937],{"id":2936},"local-development-already-works-this-way","Local development already works this way",[14,2939,2940,2941,2946,2947,1116,2950,2953,2954,2956,2957,1998,2960,2963,2964,726],{},"If local tables work but the hosted project doesn't (or the other way round), check your Supabase CLI version. Since ",[72,2942,2945],{"href":2943,"rel":2944},"https://github.com/supabase/cli/releases/tag/v2.106.0",[76],"Supabase CLI v2.106.0"," (June 11, 2026), ",[621,2948,2949],{},"supabase start",[621,2951,2952],{},"supabase db reset"," revoke the default Data API privileges for new tables, sequences and functions in ",[621,2955,2441],{},", because ",[621,2958,2959],{},"[api].auto_expose_new_tables",[621,2961,2962],{},"config.toml"," now defaults to ",[621,2965,2966],{},"false",[14,2968,2969,2970,2973],{},"So a migration without grants fails locally with the same 42501 error. That's useful: it shows the problem before you deploy. Setting ",[621,2971,2972],{},"auto_expose_new_tables = true"," brings the old behaviour back for now, but the CLI release notes say the flag is deprecated and scheduled for removal on October 30, 2026. Add the grants to the migration instead.",[25,2975,2977],{"id":2976},"check-your-project-before-october-30","Check your project before October 30",[337,2979,2980,3000,3009,3017],{},[340,2981,2982,781,2985,2988,2989,2992,2993,2995,2996,2999],{},[17,2983,2984],{},"List your newest migrations",[621,2986,2987],{},"supabase/migrations/"," in the repo, or the SQL your AI tool generated) and look for ",[621,2990,2991],{},"create table"," statements in ",[621,2994,2441],{}," without matching ",[621,2997,2998],{},"grant"," lines.",[340,3001,3002,3008],{},[17,3003,3004,3005,726],{},"Run them through the ",[72,3006,3007],{"href":1242},"grants checker"," It lists each table that would need a grant, each table with RLS off, and each policy that lets anyone in.",[340,3010,3011,3016],{},[17,3012,3013,3014,726],{},"Test as a real user, not as ",[621,3015,2682],{}," Sign in to the app with a normal account and use each feature that touches a new table. The SQL editor bypasses grants, so it won't show the problem.",[340,3018,3019,3022,3023,3027,3028,3052,3055],{},[17,3020,3021],{},"Opt in early if you can."," Supabase lets existing projects switch to the new default before October 30. The ",[72,3024,3026],{"href":2472,"rel":3025},[76],"changelog"," gives the SQL to run in the SQL editor; it changes only tables and sequences created afterwards:",[652,3029,3031],{"className":2699,"code":3030,"language":2701,"meta":450,"style":450},"alter default privileges for role postgres in schema public\n  revoke select, insert, update, delete on tables from anon, authenticated, service_role;\nalter default privileges for role postgres in schema public\n  revoke usage, select on sequences from anon, authenticated, service_role;\n",[621,3032,3033,3038,3043,3047],{"__ignoreMap":450},[660,3034,3035],{"class":662,"line":663},[660,3036,3037],{},"alter default privileges for role postgres in schema public\n",[660,3039,3040],{"class":662,"line":451},[660,3041,3042],{},"  revoke select, insert, update, delete on tables from anon, authenticated, service_role;\n",[660,3044,3045],{"class":662,"line":704},[660,3046,3037],{},[660,3048,3049],{"class":662,"line":1154},[660,3050,3051],{},"  revoke usage, select on sequences from anon, authenticated, service_role;\n",[3053,3054],"br",{},"Doing it on a staging project first shows you any missing grants on your schedule instead of the deadline's.",[25,3057,3059],{"id":3058},"if-you-use-lovable-bolt-or-another-ai-builder","If you use Lovable, Bolt or another AI builder",[14,3061,3062,3063,1998,3065,3067,3068,3070,3071,3074],{},"These tools write SQL migrations for you, and nothing in a generated migration warns you that the defaults changed. Read each new migration before you approve it: every ",[621,3064,2991],{},[621,3066,2441],{}," should be followed by a ",[621,3069,2998],{}," for each role that needs it and by ",[621,3072,3073],{},"enable row level security",". If you can't read the SQL yourself, paste it into the checker; it flags missing grants in plain language.",[14,3076,3077,3078,3081],{},"If your AI-built app has more than a couple of these problems, or users are already seeing empty screens, App369's ",[72,3079,3080],{"href":1237},"AI-Built App Rescue"," starts with a $450 Launch Check. Within 3 business days of read-only access, you get each issue it finds in sign-in, data access, payments and deploys, ranked, with where it is, plus a fixed price to fix the worst of them. The $450 is credited to that fix.",[999,3083,3084],{},"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":450,"searchDepth":451,"depth":451,"links":3086},[3087,3088,3089,3090,3091,3092],{"id":2465,"depth":451,"text":2466},{"id":2607,"depth":451,"text":2608},{"id":2686,"depth":451,"text":2687},{"id":2936,"depth":451,"text":2937},{"id":2976,"depth":451,"text":2977},{"id":3058,"depth":451,"text":3059},"2026-10-15T18:00:00.000Z","From October 30, 2026, new Supabase tables return \"permission denied for table\" (42501) until you grant them. What changes, who is affected, and the exact SQL to fix it.",[3096,3099,3102,3105,3108],{"question":3097,"answer":3098},"Why does Supabase say \"permission denied for table\" on a table I just created?","Since the Data API change (May 30, 2026 for new projects, October 30, 2026 for existing ones), new tables in the public schema have no grants for anon, authenticated or service_role. Add the grants in the migration that creates the table.",{"question":3100,"answer":3101},"Will my existing Supabase tables stop working on October 30?","No. Supabase says existing tables keep their current grants. Only tables created after the change need explicit grants.",{"question":3103,"answer":3104},"Is this the same as a row-level security error?","No. A missing grant fails every request with \"permission denied for table\". RLS blocks differently: reads return no rows, updates and deletes on hidden rows change nothing, and a blocked insert fails with \"new row violates row-level security policy\".",{"question":3106,"answer":3107},"Can I turn the change off for an existing project?","Supabase documents a rollback that restores the old default privileges, but that exposes every future table again. Granting each table on purpose is safer.",{"question":3109,"answer":3110},"Does this affect Lovable, Bolt or other AI-built apps?","It affects any project that reaches new tables through the Data API (supabase-js, REST or GraphQL), whoever wrote the migrations, which covers most AI-built apps. Apps that only use a direct Postgres connection are not affected. Check new migrations for a grant per role before they run.",{"name":3112,"steps":3113},"Check your Supabase project before October 30",[3114,3116,3119,3122],{"name":2984,"text":3115},"List your newest migrations and look for create table statements in public without matching grant lines.",{"name":3117,"text":3118},"Run them through the grants checker","It lists each table that would need a grant, each table with RLS off, and each policy that lets anyone in.",{"name":3120,"text":3121},"Test as a real user, not as postgres","Sign in to the app with a normal account and use each feature that touches a new table. The SQL editor bypasses grants, so it won't show the problem.",{"name":3123,"text":3124},"Opt in early if you can","Supabase lets existing projects switch to the new default before October 30.",{"readingTimeStats":3126},{"text":3127,"minutes":3128,"time":3129,"words":3130},"6 min read",5.415,324900,1083,"/blog/supabase-permission-denied-new-tables-october-30-2026","supabase permission denied for table","7 min",{"title":2428,"description":3094},[3136,3138,3140,3142],{"title":3137,"url":2943},"Supabase CLI v2.106.0 release notes (June 11, 2026)",{"title":3139,"url":2472},"Supabase changelog: tables not exposed to Data and GraphQL API automatically (April 28, 2026)",{"title":3141,"url":2693},"Supabase docs: Securing your API",{"title":3143,"url":3144},"PostgreSQL docs: GRANT","https://www.postgresql.org/docs/current/sql-grant.html","blog/supabase-permission-denied-new-tables-october-30-2026","supabase permission denied, supabase 42501, supabase grants, supabase data api, supabase october 30, supabase new tables, lovable supabase, row level security",[3148,3149,3150,3151,3152],"On October 30, 2026, Supabase stops granting new tables in the public schema to anon, authenticated and service_role in existing projects. New projects have worked this way since May 30.","Tables you already have keep their grants and keep working. Only tables created after the change are affected.","A new table without grants fails with error 42501, \"permission denied for table\", in supabase-js, the REST API and GraphQL.","The fix is one grant per role in the same migration that creates the table, next to row-level security and its policies.","Grants decide whether a role can reach a table at all; RLS policies decide which rows it sees. You need both.","2026-10-17T05:00:00.000Z","https://app369.com/blog/supabase-permission-denied-new-tables-october-30-2026/","fqKhgW6X_wO0p-1NZlGSBVcno8ihleO--3is_Kv7OTA",[3157,3159,3162,3163,3166],{"keyword":5,"url":531,"title":5,"relevance":3158},83,{"keyword":3160,"url":3161,"title":3160,"relevance":1304},"HockeyApp, Fabric and App Center Are Gone: Beta Testing and Crash Reporting in 2026","https://app369.com/blog/mobile-app-beta-testing-crash-reporting-tools-2026/",{"keyword":2428,"url":3154,"title":2428,"relevance":1178},{"keyword":3164,"url":3165,"title":3164,"relevance":1166},"Deep Linking in 2026: What Replaced Firebase Dynamic Links and How to Set It Up","https://app369.com/blog/firebase-dynamic-links-shutdown-deep-linking-2026/",{"keyword":3167,"url":3168,"title":3167,"relevance":1166},"Titanium, PhoneGap and Xamarin Are Gone: Which Cross-Platform Framework in 2026?","https://app369.com/blog/titanium-phonegap-xamarin-cross-platform-frameworks-2026/",1792225358996]