[{"data":1,"prerenderedAt":2140},["ShallowReactive",2],{"\u002Fblog\u002Fcursor-app-rescue-2026\u002F":3,"\u002Fblog\u002Fcursor-app-rescue-2026\u002F-related":411,"\u002Fblog\u002Fcursor-app-rescue-2026\u002F-resources":2123},{"id":4,"title":5,"author":6,"body":10,"category":322,"checker":323,"createdAt":324,"description":325,"disclosure":326,"extension":327,"faq":328,"heroActions":326,"howTo":326,"lastVerified":344,"location":345,"meta":346,"navigation":352,"ogImage":326,"path":353,"primaryKeyword":354,"readingTime":355,"related":356,"seo":359,"sources":360,"stem":401,"tags":402,"takeaways":403,"updatedAt":324,"url":409,"__hash__":410},"blog\u002Fblog\u002Fcursor-app-rescue-2026.md","Cursor App Rescue: 7 Problems to Fix Before Launch",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":309},"minimark",[13,21,24,29,37,52,72,78,82,85,94,99,103,110,113,126,141,145,148,161,166,170,173,178,183,187,196,205,210,214,230,239,244,248,270,274],[14,15,16,20],"p",{},[17,18,19],"strong",{},"Cursor writes code in your own repository, so the problems that show up with real users are ordinary web app problems that are easy to miss without a review: secret keys, unprotected endpoints, open database rules, unchecked packages and settings that differ between your machine and the host."," Unlike Lovable, Bolt or Replit, Cursor doesn't host your app or pick your database. That gives you more control, and it also means no platform check runs before you launch.",[14,22,23],{},"Below are the seven problems to check before launch, how to test each one, and how to fix it. The facts come from Cursor's documentation and forum, the Next.js, Vercel, Supabase and Firebase docs, OWASP, NVD and one research paper, and I re-read each source on October 20, 2026. The examples use Next.js, but the checks apply to any stack. The how-to-check and how-to-fix steps are my own advice.",[25,26,28],"h2",{"id":27},"_1-secret-keys-in-the-browser-or-in-git","1. Secret keys in the browser or in git",[14,30,31,32,36],{},"In Next.js, a variable whose name starts with ",[33,34,35],"code",{},"NEXT_PUBLIC_"," \"will be inlined into any JavaScript sent to the browser.\" If the agent puts a secret there to make a feature work, every visitor can read it. Supabase is blunt about its secret keys: \"A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control.\"",[14,38,39,40,43,44,47,48,51],{},"Cursor can also read your ",[33,41,42],{},".env"," file. A Cursor staff reply on its forum explains that ",[33,45,46],{},".env*"," is in the default ignore list, \"but that list only applies to indexing\", and Cursor's docs say the terminal and MCP tools used by the Agent \"cannot block access to code governed by ",[33,49,50],{},".cursorignore","\".",[14,53,54,57,58,61,62,61,65,68,69,71],{},[17,55,56],{},"How to check:"," search the code and the git history for ",[33,59,60],{},"service_role",", ",[33,63,64],{},"sb_secret_",[33,66,67],{},"sk_live"," and your providers' key prefixes. List every ",[33,70,35],{}," variable and ask whether each one is safe to publish. Open the JavaScript your live site loads and search it for the same prefixes.",[14,73,74,77],{},[17,75,76],{},"How to fix:"," rotate every key that was ever in browser code or git, move secrets to server-only code and the host's environment settings, and keep production secrets out of the project folder.",[25,79,81],{"id":80},"_2-api-routes-and-server-actions-anyone-can-call","2. API routes and server actions anyone can call",[14,83,84],{},"Hiding a button doesn't protect the action behind it. The Next.js data security guide says a Server Action \"is reachable via a direct POST request, not just through your application's UI,\" and that \"A page-level authentication check does not extend to the Server Actions defined within it.\" OWASP calls the related flaw insecure direct object reference (IDOR): attackers \"access or modify objects by manipulating identifiers\", such as changing an ID in a request to read another user's record.",[14,86,87,89,90,93],{},[17,88,56],{}," for every API route and every ",[33,91,92],{},"'use server'"," function, confirm it checks the session and that the record belongs to that user. Call each one while logged out, then as user B with user A's IDs.",[14,95,96,98],{},[17,97,76],{}," check authentication and ownership inside each handler, or in one server-only data layer that every handler uses. Return only the fields the screen needs, and deny by default.",[25,100,102],{"id":101},"_3-database-rules-left-wide-open","3. Database rules left wide open",[14,104,105,106,109],{},"If the browser talks to the database directly, the rules are the only thing standing between a stranger and your data. Supabase says: \"Enable RLS on every table in an exposed schema.\" Firebase's own example of an open rule set carries the warning \"",[17,107,108],{},"NEVER"," use this rule set in production; it allows anyone to overwrite your entire database,\" and says that without authentication and rules, \"anyone who guesses your project ID can steal, modify, or delete the data.\"",[14,111,112],{},"A quick way to make a permission error go away during development is to turn row-level security off or add a rule that lets everyone in. That shortcut is easy to forget before launch.",[14,114,115,117,118,121,122,125],{},[17,116,56],{}," on Supabase, open the Security Advisor and search your policies for ",[33,119,120],{},"using ( true )",". On Firebase, search your rules for ",[33,123,124],{},"if true"," and for test-mode dates. Then read and write as a logged-out visitor and as a second user. If your app uses Supabase from the browser, the free scan at the end of this page does the logged-out read test for you.",[14,127,128,130,131,134,135,140],{},[17,129,76],{}," turn on RLS for every exposed table with policies tied to the signed-in user, write Firebase rules that check ",[33,132,133],{},"request.auth",", and keep secret or admin keys on the server. The ",[136,137,139],"a",{"href":138},"\u002Fblog\u002Ffirebase-security-guide-2026\u002F","Firebase security guide"," has worked examples.",[25,142,144],{"id":143},"_4-packages-that-dont-exist-or-arent-what-they-seem","4. Packages that don't exist, or aren't what they seem",[14,146,147],{},"Code models sometimes suggest packages that were never published. A 2024 study of 16 code models found \"the average percentage of hallucinated packages is at least 5.2% for commercial models and 21.7% for open-source models.\" Those figures cover models in general, not Cursor in particular. The risk is that someone registers an invented name with harmful code, and the next install pulls it in.",[14,149,150,152,153,156,157,160],{},[17,151,56],{}," read every new line in ",[33,154,155],{},"package.json"," or ",[33,158,159],{},"requirements.txt",". Confirm each package exists on its registry, has a real history and the publisher you expect. Run your package manager's audit and commit the lockfile.",[14,162,163,165],{},[17,164,76],{}," remove anything you can't account for, install only after checking the registry, and read what the Agent wants to install before you approve it, and don't rely on Cursor's approval prompts alone.",[25,167,169],{"id":168},"_5-big-edits-that-look-right-and-arent","5. Big edits that look right and aren't",[14,171,172],{},"Cursor's own guide to reviewing agent code says \"AI-generated code can look correct but be subtly wrong,\" and \"Passing tests don't guarantee the code works correctly,\" because the tests may check the wrong behavior. It recommends small commits, each one a single logical change, so a person can review them.",[14,174,175,177],{},[17,176,56],{}," read the full diff before you accept it, then run the main user flows by hand: sign-up, sign-in, payment and anything that writes data. Look at which tests cover sign-in, payments and data changes.",[14,179,180,182],{},[17,181,76],{}," ask for one change at a time, commit small, and add tests for the money, login and data paths. Test with a second account and realistic data, not only the demo user.",[25,184,186],{"id":185},"_6-works-locally-fails-on-the-host","6. Works locally, fails on the host",[14,188,189,190,192,193,195],{},"The ",[33,191,42],{}," file on your machine is not on your host. In Next.js, ",[33,194,35],{}," values are fixed when the app is built: \"After being built, your app will no longer respond to changes to these environment variables.\" Vercel adds that \"Any change you make to environment variables are not applied to previous deployments, they only apply to new deployments.\"",[14,197,198,200,201,204],{},[17,199,56],{}," list every ",[33,202,203],{},"process.env"," value the code reads and confirm each one is set on the host for production and for previews. Check whether any were added after the last deploy.",[14,206,207,209],{},[17,208,76],{}," set every variable on the host, redeploy, read server-only values only on the server, and add a startup check that fails loudly when a required value is missing.",[25,211,213],{"id":212},"_7-an-out-of-date-editor-and-unknown-mcp-servers","7. An out-of-date editor and unknown MCP servers",[14,215,216,217,223,224,229],{},"The editor itself is part of the risk. NVD lists ",[136,218,222],{"href":219,"rel":220},"https:\u002F\u002Fnvd.nist.gov\u002Fvuln\u002Fdetail\u002FCVE-2025-54136",[221],"nofollow","CVE-2025-54136",", where in Cursor \"versions 1.2.4 and below, attackers can achieve remote and persistent code execution\" by swapping an already-approved MCP configuration for a malicious command (fixed in 1.3), and ",[136,225,228],{"href":226,"rel":227},"https:\u002F\u002Fnvd.nist.gov\u002Fvuln\u002Fdetail\u002FCVE-2025-64109",[221],"CVE-2025-64109",", where opening a cloned repository in the Cursor CLI Beta could run a malicious MCP server \"without any warning\". Cursor's security page also says \"Cursor supports workspace trust, but it's disabled by default,\" and calls its run controls \"best-effort guardrails rather than a hard security boundary.\"",[14,231,232,234,235,238],{},[17,233,56],{}," confirm Cursor is on the current version, open ",[33,236,237],{},".cursor\u002Fmcp.json"," in any repository you cloned, and list the MCP servers you have approved.",[14,240,241,243],{},[17,242,76],{}," update Cursor, remove MCP servers you don't recognize, turn on workspace trust, and open repositories you don't trust in a plain text editor first.",[25,245,247],{"id":246},"what-to-do-this-week","What to do this week",[249,250,251,255,258,261,264],"ol",{},[252,253,254],"li",{},"Search the code, git history and live JavaScript for secret keys, and rotate anything you find.",[252,256,257],{},"Call every API route and server action while logged out and as a second user.",[252,259,260],{},"Check your database rules, or run the free leak scan below if your app uses Supabase.",[252,262,263],{},"Check every package added in the last month against its registry.",[252,265,266,267,269],{},"Compare the host's environment variables with your local ",[33,268,42],{},", then redeploy.",[25,271,273],{"id":272},"need-a-hand","Need a hand?",[14,275,276,277,281,282,61,286,61,290,61,294,298,299,303,304,308],{},"If you'd rather have someone else check it, the ",[136,278,280],{"href":279},"\u002Ffixed-price\u002Fai-app-rescue\u002F","AI-Built App Rescue"," starts with a $450 Launch Check. You send read-only access, and within 3 business days you get each problem ranked by severity, with where it is and how it was confirmed, plus a fixed price for the Rescue Sprint. The $450 is credited if you go ahead, and you keep the code either way. Built with an app builder instead? See the ",[136,283,285],{"href":284},"\u002Fblog\u002Flovable-app-rescue-2026\u002F","Lovable",[136,287,289],{"href":288},"\u002Fblog\u002Fbolt-app-rescue-2026\u002F","Bolt",[136,291,293],{"href":292},"\u002Fblog\u002Freplit-app-rescue-2026\u002F","Replit",[136,295,297],{"href":296},"\u002Fblog\u002Fbase44-app-rescue-2026\u002F","Base44"," and ",[136,300,302],{"href":301},"\u002Fblog\u002Fv0-app-rescue-2026\u002F","v0"," checklists. Or ",[136,305,307],{"href":306},"\u002Fcontact\u002F","send me a short description",".",{"title":310,"searchDepth":311,"depth":311,"links":312},"",2,[313,314,315,316,317,318,319,320,321],{"id":27,"depth":311,"text":28},{"id":80,"depth":311,"text":81},{"id":101,"depth":311,"text":102},{"id":143,"depth":311,"text":144},{"id":168,"depth":311,"text":169},{"id":185,"depth":311,"text":186},{"id":212,"depth":311,"text":213},{"id":246,"depth":311,"text":247},{"id":272,"depth":311,"text":273},"AI & Technology","supabase-leak-scan","2026-10-20T12:00:00.000Z","Your app was built with Cursor and works on your machine, but real users are coming. The 7 problems to check first, from secret keys and unprotected API routes to open database rules and invented packages, with how to test and fix each one.",null,"md",[329,332,335,338,341],{"question":330,"answer":331},"Is code written by Cursor safe to launch?","It can be, once a person has reviewed it. Cursor itself says AI-generated code can look correct but be subtly wrong, and that passing tests do not prove it works. Review the diff, test the main flows by hand and test access with two accounts.",{"question":333,"answer":334},"Can Cursor read my .env file?","It can. A Cursor staff reply on its forum says the default ignore list only applies to indexing, not to Agent tool calls, and Cursor's docs say the terminal and MCP tools cannot block access to files in .cursorignore. Keep production secrets out of the project folder.",{"question":336,"answer":337},"Why does my Cursor app work locally but not after deploying?","Usually because an environment variable exists in your local .env file but not on the host, or was set after the last build. In Next.js, NEXT_PUBLIC_ values are fixed at build time, and on Vercel a variable change only applies to new deployments, so redeploy after changing one.",{"question":339,"answer":340},"Does the free leak scan work for apps built with Cursor?","Only if the app uses Supabase from the browser. The scan checks the public Supabase key your live site ships. For Firebase, a custom backend or server-only data access, use the two-account and logged-out tests on this page instead.",{"question":342,"answer":343},"Do I keep my code if I hire someone to fix my Cursor app?","Yes. A Cursor project is ordinary code in your own repository, so a developer works from that repository and you keep everything.","2026-10-20","Boynton Beach, FL",{"readingTimeStats":347},{"text":348,"minutes":349,"time":350,"words":351},"7 min read",6.965,417900,1393,true,"\u002Fblog\u002Fcursor-app-rescue-2026","cursor app rescue","9 min",[357,358],"replit-app-rescue-2026","firebase-security-guide-2026",{"title":5,"description":325},[361,364,367,370,373,376,379,382,385,388,391,394,397,399],{"title":362,"url":363},"Cursor docs, Reviewing agent code","https:\u002F\u002Fcursor.com\u002Fdocs\u002Fagent\u002Freview",{"title":365,"url":366},"Cursor docs, Agent security","https:\u002F\u002Fcursor.com\u002Fdocs\u002Fagent\u002Fsecurity",{"title":368,"url":369},"Cursor docs, Ignore files","https:\u002F\u002Fcursor.com\u002Fdocs\u002Fcontext\u002Fignore-files",{"title":371,"url":372},"Cursor forum, Cursor reads .env files by default","https:\u002F\u002Fforum.cursor.com\u002Ft\u002Fcursor-reads-env-files-by-default\u002F152167",{"title":374,"url":375},"Next.js docs, Data security","https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fapp\u002Fguides\u002Fdata-security",{"title":377,"url":378},"Next.js docs, Environment variables","https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fapp\u002Fguides\u002Fenvironment-variables",{"title":380,"url":381},"Vercel docs, Environment variables","https:\u002F\u002Fvercel.com\u002Fdocs\u002Fenvironment-variables",{"title":383,"url":384},"OWASP, Insecure Direct Object Reference Prevention Cheat Sheet","https:\u002F\u002Fcheatsheetseries.owasp.org\u002Fcheatsheets\u002FInsecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html",{"title":386,"url":387},"Supabase docs, Row Level Security","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Frow-level-security",{"title":389,"url":390},"Supabase docs, API keys","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fapi\u002Fapi-keys",{"title":392,"url":393},"Firebase docs, Avoid insecure rules","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Finsecure-rules",{"title":395,"url":396},"Spracklen et al., We Have a Package for You! (arXiv 2406.10279)","https:\u002F\u002Farxiv.org\u002Fabs\u002F2406.10279",{"title":398,"url":219},"NVD, CVE-2025-54136",{"title":400,"url":226},"NVD, CVE-2025-64109","blog\u002Fcursor-app-rescue-2026","cursor app rescue, fix cursor app, cursor ai code review, cursor security, next.js server actions security, supabase row level security, firebase security rules, ai app rescue, vibe coded app",[404,405,406,407,408],"Cursor's own review guide warns that \"AI-generated code can look correct but be subtly wrong,\" and that \"Passing tests don't guarantee the code works correctly.\"","In Next.js, \"A page-level authentication check does not extend to the Server Actions defined within it,\" so every action and API route has to check the user itself.","Any variable starting with NEXT_PUBLIC_ \"will be inlined into any JavaScript sent to the browser,\" so a secret key there is public.","A 2024 study found at least 5.2% of packages suggested by commercial models, and 21.7% by open-source models, did not exist. Check every new dependency before you install it.","Cursor's workspace trust is \"disabled by default\", and older Cursor versions had MCP flaws that allowed code to run on your machine, so keep Cursor updated.","https:\u002F\u002Fapp369.com\u002Fblog\u002Fcursor-app-rescue-2026\u002F","tm67SK9Xuh8DwiyBxKWstOudaGhnCaKnUixkgo3_F8U",[412,710],{"id":413,"title":414,"author":415,"body":417,"category":322,"checker":326,"createdAt":632,"description":633,"disclosure":326,"extension":327,"faq":634,"heroActions":326,"howTo":326,"lastVerified":344,"location":345,"meta":650,"navigation":352,"ogImage":326,"path":656,"primaryKeyword":657,"readingTime":658,"related":659,"seo":662,"sources":663,"stem":700,"tags":701,"takeaways":702,"updatedAt":632,"url":708,"__hash__":709},"blog\u002Fblog\u002Freplit-app-rescue-2026.md","Replit App Rescue: 7 Problems to Fix Before Launch",{"name":7,"social":416},{"twitter":9},{"type":11,"value":418,"toc":621},[419,425,428,432,435,445,450,459,463,466,478,483,487,490,493,498,503,507,510,513,518,523,527,534,542,547,551,554,559,564,568,571,576,581,583,600,602],[14,420,421,424],{},[17,422,423],{},"Problems with real Replit apps often show up at the step from the workspace to the published app: secrets, files, the production database, and the deployment settings."," Replit's docs describe a published app as \"a running instance of your app on Replit's cloud infrastructure\", separate from the workspace where Replit Agent builds it. It has its own production database and its own settings, so something that works in the preview can still fail live.",[14,426,427],{},"Below are the seven problems to check before launch, how to test each one, and how to fix it. The facts come from Replit's documentation and Replit's own blog posts, and I re-read each source on October 20, 2026. The how-to-check and how-to-fix steps are my own advice.",[25,429,431],{"id":430},"_1-works-in-the-preview-fails-after-publishing","1. Works in the preview, fails after publishing",[14,433,434],{},"A frequent first surprise is configuration. Replit's troubleshooting guide says that \"Secrets you set in the Project Editor do not automatically carry over to your published app\", while its publishing overview says secrets \"sync automatically from your development environment to your published app when you publish or republish\". With the docs disagreeing, check the published app's secrets yourself rather than assuming either way.",[14,436,437,438,156,441,444],{},"The same guide lists a second cause: listening only on ",[33,439,440],{},"localhost",[33,442,443],{},"127.0.0.1"," \"does not work for published apps\".",[14,446,447,449],{},[17,448,56],{}," open the deployment logs, compare the published app's secrets with the workspace's, and open the live URL in a private window.",[14,451,452,454,455,458],{},[17,453,76],{}," set every secret for the published app, make the server listen on ",[33,456,457],{},"0.0.0.0",", and publish again.",[25,460,462],{"id":461},"_2-uploaded-files-that-disappear","2. Uploaded files that disappear",[14,464,465],{},"Replit's troubleshooting guide is direct: \"The file system in published apps is not persistent and resets every time you publish.\" An app that saves profile photos, PDFs, a SQLite file or a JSON \"database\" to its own folder works in the workspace and then loses that data on the next publish.",[14,467,468,470,471,298,474,477],{},[17,469,56],{}," search the code for file writes, ",[33,472,473],{},"sqlite",[33,475,476],{},"uploads"," folders. Upload a test file to the live app, publish again, and see whether it is still there.",[14,479,480,482],{},[17,481,76],{}," keep data in the database and files in Replit's App Storage. Replit's storage guide warns that \"any public route that returns stored files exposes them\", so only serve files to users your app has authorized.",[25,484,486],{"id":485},"_3-development-and-production-data-mixed-up","3. Development and production data mixed up",[14,488,489],{},"In July 2025 Replit wrote that \"Until now, Replit apps used a single database for both development and live customer data.\" Apps now get separate development and production databases, and Replit says the \"First deployment creates a production database with a fresh schema but no data.\" Replit says that when you re-deploy, the Agent checks for development database changes and asks whether to apply them to production, whether or not they are destructive.",[14,491,492],{},"That fixes one risk and creates two others: a first publish that shows an empty app, and a schema change, such as a renamed column, that reaches live data once you approve it.",[14,494,495,497],{},[17,496,56],{}," in the Database tool, switch to the production database and look at what is there. Find out whether your app was built before the split, because then it may still use one database for both.",[14,499,500,502],{},[17,501,76],{}," add new columns as optional before you make them required, avoid renames on tables with live data, load only safe starter data into production, and turn on production backups before launch.",[25,504,506],{"id":505},"_4-rollbacks-that-dont-bring-your-data-back","4. Rollbacks that don't bring your data back",[14,508,509],{},"Replit's checkpoint guide says \"By default, rollbacks do not change your database.\" Its data recovery guide adds: \"Restoring your database doesn't restore your app's code, and rolling back your app doesn't restore your database.\"",[14,511,512],{},"This is what went wrong in the incident Replit wrote about in July 2025, when, in Replit's words, \"the Agent deleted data from the app's database.\" Replit says the user was able to fully restore the database with the Rollback button, and that with separate databases \"the Agent cannot make any change to the production database during development.\"",[14,514,515,517],{},[17,516,56],{}," confirm your app uses separate databases, confirm production backups are on and how far back they go, and try one restore on a copy.",[14,519,520,522],{},[17,521,76],{}," turn on production backups, keep the Agent away from production data, and when something goes wrong, restore the database and roll back the code to the same moment before you publish again.",[25,524,526],{"id":525},"_5-secrets-in-the-code-or-the-repository","5. Secrets in the code or the repository",[14,528,529,530,533],{},"API keys pasted into code, the ",[33,531,532],{},".replit"," file or a pushed repository stay readable long after they are moved. Replit's security scanner is described as \"an early, lightweight check in the Agent workflow rather than a full codebase review\", so it can't be your only check.",[14,535,536,538,539,541],{},[17,537,56],{}," search the code, the ",[33,540,532],{}," file and the git history for key patterns, and list who has access to the project.",[14,543,544,546],{},[17,545,76],{}," rotate every key that was ever in the code, store keys only in Secrets, and remove collaborators who no longer need access.",[25,548,550],{"id":549},"_6-login-without-access-control","6. Login without access control",[14,552,553],{},"Replit's shared responsibility model is clear about who owns what in a published app. Logging in is yours to set up, and so is \"which users have access to which resources or parts of the app based on authorization.\" A working sign-in screen does not mean user A can't reach user B's records through the API.",[14,555,556,558],{},[17,557,56],{}," sign in as one user and request another user's records by changing the ID in the address or the request. Call your API routes with no session.",[14,560,561,563],{},[17,562,76],{}," check ownership on the server for every query, deny by default, set the right publish visibility, and turn on the setting that blocks publishing when the scan finds critical problems.",[25,565,567],{"id":566},"_7-the-wrong-deployment-type","7. The wrong deployment type",[14,569,570],{},"Replit offers several deployment types. Autoscale apps scale with traffic and can scale down when idle, while a Reserved VM is \"a dedicated virtual machine that never sleeps.\" A Discord bot, a background job or a long-running worker on the wrong type stops, crashes or fails its health check.",[14,572,573,575],{},[17,574,56],{}," read the publishing settings and logs, and match the type to the work: web requests, always-on work, scheduled jobs or static files.",[14,577,578,580],{},[17,579,76],{}," move always-on work to a type that stays on, keep the homepage light so health checks pass, and watch the usage page after launch.",[25,582,247],{"id":246},[249,584,585,588,591,594,597],{},[252,586,587],{},"Publish, then test sign-up, sign-in and payments on the live URL, not the preview.",[252,589,590],{},"Check that every secret is set for the published app.",[252,592,593],{},"Move uploads out of local files.",[252,595,596],{},"Turn on production database backups and practise one restore.",[252,598,599],{},"Do the two-account test on every page with private data.",[25,601,273],{"id":272},[14,603,276,604,606,607,61,609,61,611,61,615,298,617,303,619,308],{},[136,605,280],{"href":279}," starts with a $450 Launch Check. You send read-only access, and within 3 business days you get each problem ranked by severity, with where it is and how it was confirmed, plus a fixed price for the Rescue Sprint. The $450 is credited if you go ahead, and you keep the code either way. Built with something else? See the ",[136,608,285],{"href":284},[136,610,289],{"href":288},[136,612,614],{"href":613},"\u002Fblog\u002Fcursor-app-rescue-2026\u002F","Cursor",[136,616,297],{"href":296},[136,618,302],{"href":301},[136,620,307],{"href":306},{"title":310,"searchDepth":311,"depth":311,"links":622},[623,624,625,626,627,628,629,630,631],{"id":430,"depth":311,"text":431},{"id":461,"depth":311,"text":462},{"id":485,"depth":311,"text":486},{"id":505,"depth":311,"text":506},{"id":525,"depth":311,"text":526},{"id":549,"depth":311,"text":550},{"id":566,"depth":311,"text":567},{"id":246,"depth":311,"text":247},{"id":272,"depth":311,"text":273},"2026-10-20T11:00:00.000Z","Your Replit app works in the workspace but breaks after you publish. The 7 problems to check first, from missing production secrets and disappearing uploads to database changes and rollbacks, with how to test and fix each one.",[635,638,641,644,647],{"question":636,"answer":637},"Why does my Replit app work in the workspace but not when published?","The published app runs separately, with its own production database and its own secrets. Check the deployment logs, confirm every secret is set for the published app, and make sure the server listens on 0.0.0.0, not localhost.",{"question":639,"answer":640},"Why do uploaded files disappear from my published Replit app?","Replit says the file system in published apps is not persistent and resets every time you publish. Store uploads in Replit's App Storage or another object store, and data in the database.",{"question":642,"answer":643},"Can Replit Agent delete my production data?","Replit says that with separate development and production databases, the Agent cannot make any change to the production database during development. Apps built before that split used one database for both, so check which setup yours has and turn on production backups.",{"question":645,"answer":646},"Does a Replit rollback restore my data?","Not by default. Replit says rollbacks do not change your database unless you choose to include the development database, and production data is restored separately through backups.",{"question":648,"answer":649},"Is my Replit app secure if the security scan passes?","Not necessarily. Replit describes the scan as an early, lightweight check rather than a full codebase review, and its shared responsibility model puts access control in published apps on you. Test with two accounts and with no login.",{"readingTimeStats":651},{"text":652,"minutes":653,"time":654,"words":655},"6 min read",5.775,346500,1155,"\u002Fblog\u002Freplit-app-rescue-2026","replit app rescue","8 min",[660,661],"lovable-app-rescue-2026","bolt-app-rescue-2026",{"title":414,"description":633},[664,667,670,673,676,679,682,685,688,691,694,697],{"title":665,"url":666},"Replit docs, About deployments","https:\u002F\u002Fdocs.replit.com\u002Fcloud-services\u002Fdeployments\u002Fabout-deployments",{"title":668,"url":669},"Replit docs, Deployment troubleshooting","https:\u002F\u002Fdocs.replit.com\u002Fcloud-services\u002Fdeployments\u002Ftroubleshooting",{"title":671,"url":672},"Replit docs, Publishing overview","https:\u002F\u002Fdocs.replit.com\u002Ffeatures\u002Fpublishing\u002Foverview",{"title":674,"url":675},"Replit docs, Object Storage","https:\u002F\u002Fdocs.replit.com\u002Fcloud-services\u002Fstorage-and-databases\u002Fobject-storage",{"title":677,"url":678},"Replit docs, Development and production databases","https:\u002F\u002Fdocs.replit.com\u002Ffeatures\u002Fdata-and-storage\u002Fdevelopment-and-production",{"title":680,"url":681},"Replit docs, Data recovery","https:\u002F\u002Fdocs.replit.com\u002Ffeatures\u002Fdata-and-storage\u002Fdata-recovery",{"title":683,"url":684},"Replit docs, Checkpoints and rollbacks","https:\u002F\u002Fdocs.replit.com\u002Freplitai\u002Fcheckpoints-and-rollbacks",{"title":686,"url":687},"Replit docs, Security scanner","https:\u002F\u002Fdocs.replit.com\u002Freplit-workspace\u002Fworkspace-features\u002Fsecurity-scanner",{"title":689,"url":690},"Replit docs, Shared responsibility model","https:\u002F\u002Fdocs.replit.com\u002Ffeatures\u002Fsecurity\u002Fshared-responsibility-model",{"title":692,"url":693},"Replit docs, Reserved VM deployments","https:\u002F\u002Fdocs.replit.com\u002Fcloud-services\u002Fdeployments\u002Freserved-vm-deployments",{"title":695,"url":696},"Replit blog, Introducing a safer way to vibe code with Replit Databases (July 21, 2025)","https:\u002F\u002Freplit.com\u002Fblog\u002Fintroducing-a-safer-way-to-vibe-code-with-replit-databases",{"title":698,"url":699},"Replit blog, Doubling down on our commitment to secure vibe coding","https:\u002F\u002Freplit.com\u002Fblog\u002Fdoubling-down-on-our-commitment-to-secure-vibe-coding","blog\u002Freplit-app-rescue-2026","replit app rescue, fix replit app, replit agent, replit deployment, replit production database, replit secrets, ai app rescue, vibe coded app",[703,704,705,706,707],"A published Replit app is a separate running copy of your app, with its own secrets and its own production database, so \"works in the workspace\" doesn't mean \"works when published\".","Replit's docs say the file system in published apps \"is not persistent and resets every time you publish\", so uploads saved to local files disappear.","Replit says rolling back your app \"doesn't restore your database\", and restoring the database doesn't restore your code. You have to bring both back to the same moment.","Replit's security scan is \"an early, lightweight check\", not a full code review. Deciding which users can reach which data is your job.","Background jobs and bots need a deployment type that stays on. Autoscale apps can scale down when idle.","https:\u002F\u002Fapp369.com\u002Fblog\u002Freplit-app-rescue-2026\u002F","XxwnQj_Uc0U9HF8-OJLUeb1arNY9nst59gFd3-d5m6g",{"id":711,"title":712,"author":713,"body":715,"category":322,"checker":326,"createdAt":2103,"description":2104,"disclosure":326,"extension":327,"faq":326,"heroActions":326,"howTo":326,"lastVerified":326,"location":345,"meta":2105,"navigation":352,"ogImage":326,"path":2111,"primaryKeyword":326,"readingTime":2112,"related":2113,"seo":2117,"sources":326,"stem":2118,"tags":2119,"takeaways":326,"updatedAt":2120,"url":2121,"__hash__":2122},"blog\u002Fblog\u002Ffirebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":714},{"twitter":9},{"type":11,"value":716,"toc":2087},[717,729,737,741,744,758,761,782,785,789,805,808,865,868,874,912,915,935,939,948,951,1088,1091,1153,1156,1160,1169,1172,1225,1228,1280,1287,1290,1314,1318,1321,1324,1427,1430,1508,1511,1515,1524,1532,1537,1561,1570,1682,1685,1728,1732,1735,1738,1744,1833,1839,1889,1895,1960,1963,1967,1973,2017,2028,2032,2037,2045,2049,2052,2056,2071,2075,2083],[14,718,719,722,723,728],{},[17,720,721],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[136,724,727],{"href":725,"rel":726},"https:\u002F\u002Fwww.audityour.app\u002Fguides\u002Ffirebase-security-rules-guide",[221],"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. When you create a database, Firebase lets you choose locked mode, which denies all client access, or test mode, which allows anyone to read and overwrite your data until an expiry date, so every permission must be explicitly defined by the developer.",[14,730,731,732,736],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[136,733,735],{"href":734},"\u002Fservice\u002Fmobile-apps\u002F","App369"," uses to secure Firebase backends across production applications.",[25,738,740],{"id":739},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,742,743],{},"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,745,746,747,752,753,757],{},"According to ",[136,748,751],{"href":749,"rel":750},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002F",[221],"Firebase's official documentation",", Security Rules stand between your data and malicious users, and ",[136,754,756],{"href":749,"rel":755},[221],"Firebase's rules overview"," says clients aren't responsible for enforcing security. Unlike client-side validation, which can be disabled by modifying the app, Security Rules are enforced on Firebase's servers.",[14,759,760],{},"Rules apply to three Firebase services independently:",[762,763,764,770,776],"ul",{},[252,765,766,769],{},[17,767,768],{},"Cloud Firestore",": document-level read\u002Fwrite control with path-based matching",[252,771,772,775],{},[17,773,774],{},"Realtime Database",": node-level access with cascading permissions",[252,777,778,781],{},[17,779,780],{},"Cloud Storage",": file-level access based on path, metadata, and authentication state",[14,783,784],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[25,786,788],{"id":787},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,790,791,792,298,795,798,799,804],{},"Firebase's setup wizard offers two starting configurations: ",[17,793,794],{},"test mode",[17,796,797],{},"locked mode",". Test mode grants public read and write access to the entire database until an expiry date. ",[136,800,803],{"href":801,"rel":802},"https:\u002F\u002Fvibeappscanner.com\u002Fis-firebase-safe",[221],"VibeAppScanner's Firebase safety analysis"," calls test mode the number one cause of Firebase data breaches.",[14,806,807],{},"Test mode rules look like this:",[809,810,814],"pre",{"className":811,"code":812,"language":813,"meta":310,"style":310},"language-javascript shiki shiki-themes github-light github-dark","rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n    match \u002F{document=**} {\n      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n    }\n  }\n}\n","javascript",[33,815,816,824,829,835,841,847,853,859],{"__ignoreMap":310},[817,818,821],"span",{"class":819,"line":820},"line",1,[817,822,823],{},"rules_version = '2';\n",[817,825,826],{"class":819,"line":311},[817,827,828],{},"service cloud.firestore {\n",[817,830,832],{"class":819,"line":831},3,[817,833,834],{},"  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n",[817,836,838],{"class":819,"line":837},4,[817,839,840],{},"    match \u002F{document=**} {\n",[817,842,844],{"class":819,"line":843},5,[817,845,846],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[817,848,850],{"class":819,"line":849},6,[817,851,852],{},"    }\n",[817,854,856],{"class":819,"line":855},7,[817,857,858],{},"  }\n",[817,860,862],{"class":819,"line":861},8,[817,863,864],{},"}\n",[14,866,867],{},"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,869,870,871,873],{},"The correct starting point is ",[17,872,797],{},", where all access is denied by default:",[809,875,877],{"className":811,"code":876,"language":813,"meta":310,"style":310},"rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n    match \u002F{document=**} {\n      allow read, write: if false;\n    }\n  }\n}\n",[33,878,879,883,887,891,895,900,904,908],{"__ignoreMap":310},[817,880,881],{"class":819,"line":820},[817,882,823],{},[817,884,885],{"class":819,"line":311},[817,886,828],{},[817,888,889],{"class":819,"line":831},[817,890,834],{},[817,892,893],{"class":819,"line":837},[817,894,840],{},[817,896,897],{"class":819,"line":843},[817,898,899],{},"      allow read, write: if false;\n",[817,901,902],{"class":819,"line":849},[817,903,852],{},[817,905,906],{"class":819,"line":855},[817,907,858],{},[817,909,910],{"class":819,"line":861},[817,911,864],{},[14,913,914],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,916,917,918,921,922,926,927,61,929,61,931,298,933,308],{},"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 ",[136,919,920],{"href":279},"AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[136,923,925],{"href":924},"\u002Ftools\u002Fsupabase-oct-30-checker\u002F","Supabase October 30 grants checker"," lists the tables that will need explicit grants after that date. Builder-specific checklists: ",[136,928,285],{"href":284},[136,930,289],{"href":288},[136,932,293],{"href":292},[136,934,614],{"href":613},[25,936,938],{"id":937},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,940,941,942,947],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[136,943,946],{"href":944,"rel":945},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Frules-structure",[221],"Firebase's rules structure 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,949,950],{},"Here is a production-ready pattern for a user-owned data collection:",[809,952,954],{"className":811,"code":953,"language":813,"meta":310,"style":310},"rules_version = '2';\nservice cloud.firestore {\n  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n\n    \u002F\u002F Users can only read and write their own profile\n    match \u002Fusers\u002F{userId} {\n      allow read, write: if request.auth != null && request.auth.uid == userId;\n    }\n\n    \u002F\u002F Posts are publicly readable, but only the author can write\n    match \u002Fposts\u002F{postId} {\n      allow read: if true;\n      allow create: if request.auth != null\n                    && request.resource.data.authorId == request.auth.uid;\n      allow update, delete: if request.auth != null\n                            && resource.data.authorId == request.auth.uid;\n    }\n\n    \u002F\u002F Admin-only collection\n    match \u002FadminConfig\u002F{docId} {\n      allow read, write: if request.auth != null\n                         && request.auth.token.admin == true;\n    }\n  }\n}\n",[33,955,956,960,964,968,973,978,983,988,992,997,1003,1009,1015,1021,1027,1033,1039,1044,1049,1055,1061,1067,1073,1078,1083],{"__ignoreMap":310},[817,957,958],{"class":819,"line":820},[817,959,823],{},[817,961,962],{"class":819,"line":311},[817,963,828],{},[817,965,966],{"class":819,"line":831},[817,967,834],{},[817,969,970],{"class":819,"line":837},[817,971,972],{"emptyLinePlaceholder":352},"\n",[817,974,975],{"class":819,"line":843},[817,976,977],{},"    \u002F\u002F Users can only read and write their own profile\n",[817,979,980],{"class":819,"line":849},[817,981,982],{},"    match \u002Fusers\u002F{userId} {\n",[817,984,985],{"class":819,"line":855},[817,986,987],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[817,989,990],{"class":819,"line":861},[817,991,852],{},[817,993,995],{"class":819,"line":994},9,[817,996,972],{"emptyLinePlaceholder":352},[817,998,1000],{"class":819,"line":999},10,[817,1001,1002],{},"    \u002F\u002F Posts are publicly readable, but only the author can write\n",[817,1004,1006],{"class":819,"line":1005},11,[817,1007,1008],{},"    match \u002Fposts\u002F{postId} {\n",[817,1010,1012],{"class":819,"line":1011},12,[817,1013,1014],{},"      allow read: if true;\n",[817,1016,1018],{"class":819,"line":1017},13,[817,1019,1020],{},"      allow create: if request.auth != null\n",[817,1022,1024],{"class":819,"line":1023},14,[817,1025,1026],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[817,1028,1030],{"class":819,"line":1029},15,[817,1031,1032],{},"      allow update, delete: if request.auth != null\n",[817,1034,1036],{"class":819,"line":1035},16,[817,1037,1038],{},"                            && resource.data.authorId == request.auth.uid;\n",[817,1040,1042],{"class":819,"line":1041},17,[817,1043,852],{},[817,1045,1047],{"class":819,"line":1046},18,[817,1048,972],{"emptyLinePlaceholder":352},[817,1050,1052],{"class":819,"line":1051},19,[817,1053,1054],{},"    \u002F\u002F Admin-only collection\n",[817,1056,1058],{"class":819,"line":1057},20,[817,1059,1060],{},"    match \u002FadminConfig\u002F{docId} {\n",[817,1062,1064],{"class":819,"line":1063},21,[817,1065,1066],{},"      allow read, write: if request.auth != null\n",[817,1068,1070],{"class":819,"line":1069},22,[817,1071,1072],{},"                         && request.auth.token.admin == true;\n",[817,1074,1076],{"class":819,"line":1075},23,[817,1077,852],{},[817,1079,1081],{"class":819,"line":1080},24,[817,1082,858],{},[817,1084,1086],{"class":819,"line":1085},25,[817,1087,864],{},[14,1089,1090],{},"Key principles for effective rules:",[762,1092,1093,1117,1126,1139],{},[252,1094,1095,1098,1099,298,1102,1105,1106,61,1109,1112,1113,1116],{},[17,1096,1097],{},"Separate read and write permissions."," Use ",[33,1100,1101],{},"allow read",[33,1103,1104],{},"allow write"," independently. Break write into ",[33,1107,1108],{},"create",[33,1110,1111],{},"update",", and ",[33,1114,1115],{},"delete"," when different logic applies.",[252,1118,1119,1125],{},[17,1120,1121,1122,308],{},"Always check ",[33,1123,1124],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[252,1127,1128,298,1135,1138],{},[17,1129,1130,1131,1134],{},"Use ",[33,1132,1133],{},"resource.data"," for existing document fields",[33,1136,1137],{},"request.resource.data"," for incoming data in create\u002Fupdate operations.",[252,1140,1141,1144,1145,1148,1149,1152],{},[17,1142,1143],{},"Avoid wildcards at the root level."," The pattern ",[33,1146,1147],{},"match \u002F{document=**}"," applies to every document in the database. Use it only with ",[33,1150,1151],{},"if false"," as a catch-all deny rule.",[14,1154,1155],{},"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,1157,1159],{"id":1158},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,1161,1162,1163,1168],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[136,1164,1167],{"href":1165,"rel":1166},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fadmin\u002Fcustom-claims",[221],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,1170,1171],{},"Set custom claims from a Cloud Function or admin server:",[809,1173,1175],{"className":811,"code":1174,"language":813,"meta":310,"style":310},"const admin = require('firebase-admin');\n\n\u002F\u002F Assign admin role\nawait admin.auth().setCustomUserClaims(userId, { admin: true });\n\n\u002F\u002F Assign editor role with department\nawait admin.auth().setCustomUserClaims(userId, {\n  role: 'editor',\n  department: 'marketing'\n});\n",[33,1176,1177,1182,1186,1191,1196,1200,1205,1210,1215,1220],{"__ignoreMap":310},[817,1178,1179],{"class":819,"line":820},[817,1180,1181],{},"const admin = require('firebase-admin');\n",[817,1183,1184],{"class":819,"line":311},[817,1185,972],{"emptyLinePlaceholder":352},[817,1187,1188],{"class":819,"line":831},[817,1189,1190],{},"\u002F\u002F Assign admin role\n",[817,1192,1193],{"class":819,"line":837},[817,1194,1195],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[817,1197,1198],{"class":819,"line":843},[817,1199,972],{"emptyLinePlaceholder":352},[817,1201,1202],{"class":819,"line":849},[817,1203,1204],{},"\u002F\u002F Assign editor role with department\n",[817,1206,1207],{"class":819,"line":855},[817,1208,1209],{},"await admin.auth().setCustomUserClaims(userId, {\n",[817,1211,1212],{"class":819,"line":861},[817,1213,1214],{},"  role: 'editor',\n",[817,1216,1217],{"class":819,"line":994},[817,1218,1219],{},"  department: 'marketing'\n",[817,1221,1222],{"class":819,"line":999},[817,1223,1224],{},"});\n",[14,1226,1227],{},"Reference claims in Security Rules:",[809,1229,1231],{"className":811,"code":1230,"language":813,"meta":310,"style":310},"match \u002Fcontent\u002F{docId} {\n  \u002F\u002F Admins have full access\n  allow read, write: if request.auth.token.admin == true;\n\n  \u002F\u002F Editors can read and update, but not delete\n  allow read, update: if request.auth.token.role == 'editor';\n\n  \u002F\u002F All authenticated users can read\n  allow read: if request.auth != null;\n}\n",[33,1232,1233,1238,1243,1248,1252,1257,1262,1266,1271,1276],{"__ignoreMap":310},[817,1234,1235],{"class":819,"line":820},[817,1236,1237],{},"match \u002Fcontent\u002F{docId} {\n",[817,1239,1240],{"class":819,"line":311},[817,1241,1242],{},"  \u002F\u002F Admins have full access\n",[817,1244,1245],{"class":819,"line":831},[817,1246,1247],{},"  allow read, write: if request.auth.token.admin == true;\n",[817,1249,1250],{"class":819,"line":837},[817,1251,972],{"emptyLinePlaceholder":352},[817,1253,1254],{"class":819,"line":843},[817,1255,1256],{},"  \u002F\u002F Editors can read and update, but not delete\n",[817,1258,1259],{"class":819,"line":849},[817,1260,1261],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[817,1263,1264],{"class":819,"line":855},[817,1265,972],{"emptyLinePlaceholder":352},[817,1267,1268],{"class":819,"line":861},[817,1269,1270],{},"  \u002F\u002F All authenticated users can read\n",[817,1272,1273],{"class":819,"line":994},[817,1274,1275],{},"  allow read: if request.auth != null;\n",[817,1277,1278],{"class":819,"line":999},[817,1279,864],{},[14,1281,1282,1283,1286],{},"As ",[136,1284,1167],{"href":1165,"rel":1285},[221]," advises, 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,1288,1289],{},"Implementation guidelines:",[762,1291,1292,1298,1308],{},[252,1293,1294,1297],{},[17,1295,1296],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[252,1299,1300,1303,1304,1307],{},[17,1301,1302],{},"Force token refresh after claim changes."," Call ",[33,1305,1306],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[252,1309,1310,1313],{},[17,1311,1312],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[25,1315,1317],{"id":1316},"data-validation-in-security-rules","Data Validation in Security Rules",[14,1319,1320],{},"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,1322,1323],{},"Validation patterns for common requirements:",[809,1325,1327],{"className":811,"code":1326,"language":813,"meta":310,"style":310},"match \u002Fusers\u002F{userId} {\n  allow create: if request.auth.uid == userId\n    \u002F\u002F Required fields must exist\n    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n    \u002F\u002F Name must be a string between 2 and 100 characters\n    && request.resource.data.name is string\n    && request.resource.data.name.size() >= 2\n    && request.resource.data.name.size() \u003C= 100\n    \u002F\u002F Email must be a string and match the auth email\n    && request.resource.data.email is string\n    && request.resource.data.email == request.auth.token.email\n    \u002F\u002F createdAt must be a timestamp\n    && request.resource.data.createdAt is timestamp\n    \u002F\u002F No unexpected fields allowed\n    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n\n  allow update: if request.auth.uid == userId\n    \u002F\u002F Prevent changing the email field\n    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n}\n",[33,1328,1329,1334,1339,1344,1349,1354,1359,1364,1369,1374,1379,1384,1389,1394,1399,1404,1408,1413,1418,1423],{"__ignoreMap":310},[817,1330,1331],{"class":819,"line":820},[817,1332,1333],{},"match \u002Fusers\u002F{userId} {\n",[817,1335,1336],{"class":819,"line":311},[817,1337,1338],{},"  allow create: if request.auth.uid == userId\n",[817,1340,1341],{"class":819,"line":831},[817,1342,1343],{},"    \u002F\u002F Required fields must exist\n",[817,1345,1346],{"class":819,"line":837},[817,1347,1348],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[817,1350,1351],{"class":819,"line":843},[817,1352,1353],{},"    \u002F\u002F Name must be a string between 2 and 100 characters\n",[817,1355,1356],{"class":819,"line":849},[817,1357,1358],{},"    && request.resource.data.name is string\n",[817,1360,1361],{"class":819,"line":855},[817,1362,1363],{},"    && request.resource.data.name.size() >= 2\n",[817,1365,1366],{"class":819,"line":861},[817,1367,1368],{},"    && request.resource.data.name.size() \u003C= 100\n",[817,1370,1371],{"class":819,"line":994},[817,1372,1373],{},"    \u002F\u002F Email must be a string and match the auth email\n",[817,1375,1376],{"class":819,"line":999},[817,1377,1378],{},"    && request.resource.data.email is string\n",[817,1380,1381],{"class":819,"line":1005},[817,1382,1383],{},"    && request.resource.data.email == request.auth.token.email\n",[817,1385,1386],{"class":819,"line":1011},[817,1387,1388],{},"    \u002F\u002F createdAt must be a timestamp\n",[817,1390,1391],{"class":819,"line":1017},[817,1392,1393],{},"    && request.resource.data.createdAt is timestamp\n",[817,1395,1396],{"class":819,"line":1023},[817,1397,1398],{},"    \u002F\u002F No unexpected fields allowed\n",[817,1400,1401],{"class":819,"line":1029},[817,1402,1403],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[817,1405,1406],{"class":819,"line":1035},[817,1407,972],{"emptyLinePlaceholder":352},[817,1409,1410],{"class":819,"line":1041},[817,1411,1412],{},"  allow update: if request.auth.uid == userId\n",[817,1414,1415],{"class":819,"line":1046},[817,1416,1417],{},"    \u002F\u002F Prevent changing the email field\n",[817,1419,1420],{"class":819,"line":1051},[817,1421,1422],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[817,1424,1425],{"class":819,"line":1057},[817,1426,864],{},[14,1428,1429],{},"Validation capabilities in Firestore rules include:",[762,1431,1432,1460,1470,1484,1493,1499],{},[252,1433,1434,1437,1438,61,1441,61,1444,61,1447,61,1450,61,1453,1456,1457],{},[17,1435,1436],{},"Type checking",": verify that fields are ",[33,1439,1440],{},"string",[33,1442,1443],{},"int",[33,1445,1446],{},"float",[33,1448,1449],{},"bool",[33,1451,1452],{},"timestamp",[33,1454,1455],{},"list",", or ",[33,1458,1459],{},"map",[252,1461,1462,1465,1466,1469],{},[17,1463,1464],{},"String length",": use ",[33,1467,1468],{},".size()"," to enforce minimum and maximum lengths",[252,1471,1472,1475,1476,1479,1480,1483],{},[17,1473,1474],{},"Field existence",": ",[33,1477,1478],{},"hasAll()"," for required fields, ",[33,1481,1482],{},"hasOnly()"," to prevent extra fields",[252,1485,1486,1465,1489,1492],{},[17,1487,1488],{},"Field immutability",[33,1490,1491],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[252,1494,1495,1498],{},[17,1496,1497],{},"Numeric ranges",": enforce minimum and maximum values for numbers",[252,1500,1501,1504,1505,1507],{},[17,1502,1503],{},"List constraints",": check ",[33,1506,1468],{}," on lists to limit array lengths",[14,1509,1510],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[25,1512,1514],{"id":1513},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,1516,1517,1518,1523],{},"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; ",[136,1519,1522],{"href":1520,"rel":1521},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002Funit-tests",[221],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,1525,1526,1527,1531],{},"For a quick first pass, paste your rules into App369's free ",[136,1528,1530],{"href":1529},"\u002Ftools\u002Ffirebase-rules-checker\u002F","Firebase Security Rules checker",". It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.",[14,1533,1534],{},[17,1535,1536],{},"Use the Firebase Emulator Suite for local testing:",[809,1538,1542],{"className":1539,"code":1540,"language":1541,"meta":310,"style":310},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[33,1543,1544],{"__ignoreMap":310},[817,1545,1546,1550,1554,1558],{"class":819,"line":820},[817,1547,1549],{"class":1548},"sScJk","firebase",[817,1551,1553],{"class":1552},"sZZnC"," emulators:start",[817,1555,1557],{"class":1556},"sj4cs"," --only",[817,1559,1560],{"class":1552}," firestore\n",[14,1562,1563],{},[17,1564,1565,1566,1569],{},"Write unit tests with the ",[33,1567,1568],{},"@firebase\u002Frules-unit-testing"," package:",[809,1571,1573],{"className":811,"code":1572,"language":813,"meta":310,"style":310},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n\nconst testEnv = await initializeTestEnvironment({\n  projectId: 'your-project-id',\n  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n});\n\n\u002F\u002F Test: authenticated user can read their own document\nconst userContext = testEnv.authenticatedContext('user123');\nawait assertSucceeds(\n  userContext.firestore().collection('users').doc('user123').get()\n);\n\n\u002F\u002F Test: user cannot read another user's document\nawait assertFails(\n  userContext.firestore().collection('users').doc('other-user').get()\n);\n\n\u002F\u002F Test: unauthenticated user cannot write\nconst unauthedContext = testEnv.unauthenticatedContext();\nawait assertFails(\n  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n);\n",[33,1574,1575,1580,1584,1589,1594,1599,1603,1607,1612,1617,1622,1627,1632,1636,1641,1646,1651,1655,1659,1664,1669,1673,1678],{"__ignoreMap":310},[817,1576,1577],{"class":819,"line":820},[817,1578,1579],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n",[817,1581,1582],{"class":819,"line":311},[817,1583,972],{"emptyLinePlaceholder":352},[817,1585,1586],{"class":819,"line":831},[817,1587,1588],{},"const testEnv = await initializeTestEnvironment({\n",[817,1590,1591],{"class":819,"line":837},[817,1592,1593],{},"  projectId: 'your-project-id',\n",[817,1595,1596],{"class":819,"line":843},[817,1597,1598],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[817,1600,1601],{"class":819,"line":849},[817,1602,1224],{},[817,1604,1605],{"class":819,"line":855},[817,1606,972],{"emptyLinePlaceholder":352},[817,1608,1609],{"class":819,"line":861},[817,1610,1611],{},"\u002F\u002F Test: authenticated user can read their own document\n",[817,1613,1614],{"class":819,"line":994},[817,1615,1616],{},"const userContext = testEnv.authenticatedContext('user123');\n",[817,1618,1619],{"class":819,"line":999},[817,1620,1621],{},"await assertSucceeds(\n",[817,1623,1624],{"class":819,"line":1005},[817,1625,1626],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[817,1628,1629],{"class":819,"line":1011},[817,1630,1631],{},");\n",[817,1633,1634],{"class":819,"line":1017},[817,1635,972],{"emptyLinePlaceholder":352},[817,1637,1638],{"class":819,"line":1023},[817,1639,1640],{},"\u002F\u002F Test: user cannot read another user's document\n",[817,1642,1643],{"class":819,"line":1029},[817,1644,1645],{},"await assertFails(\n",[817,1647,1648],{"class":819,"line":1035},[817,1649,1650],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[817,1652,1653],{"class":819,"line":1041},[817,1654,1631],{},[817,1656,1657],{"class":819,"line":1046},[817,1658,972],{"emptyLinePlaceholder":352},[817,1660,1661],{"class":819,"line":1051},[817,1662,1663],{},"\u002F\u002F Test: unauthenticated user cannot write\n",[817,1665,1666],{"class":819,"line":1057},[817,1667,1668],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[817,1670,1671],{"class":819,"line":1063},[817,1672,1645],{},[817,1674,1675],{"class":819,"line":1069},[817,1676,1677],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[817,1679,1680],{"class":819,"line":1075},[817,1681,1631],{},[14,1683,1684],{},"Deployment workflow:",[249,1686,1687,1702,1708,1716,1722],{},[252,1688,1689,1692,1693,61,1696,1112,1699],{},[17,1690,1691],{},"Write rules locally"," in ",[33,1694,1695],{},"firestore.rules",[33,1697,1698],{},"database.rules.json",[33,1700,1701],{},"storage.rules",[252,1703,1704,1707],{},[17,1705,1706],{},"Test with the emulator",": run the full test suite against the local emulator before every deployment",[252,1709,1710,1715],{},[17,1711,1130,1712],{},[33,1713,1714],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[252,1717,1718,1721],{},[17,1719,1720],{},"Version control your rules files",": commit them to Git and review changes in pull requests",[252,1723,1724,1727],{},[17,1725,1726],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[25,1729,1731],{"id":1730},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,1733,1734],{},"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,1736,1737],{},"Use Cloud Functions as a second validation layer for:",[14,1739,1740,1743],{},[17,1741,1742],{},"Financial transactions",": verify amounts, check account balances, and enforce rate limits server-side:",[809,1745,1747],{"className":811,"code":1746,"language":813,"meta":310,"style":310},"exports.processPayment = functions.https.onCall(async (data, context) => {\n  \u002F\u002F Verify authentication\n  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n\n  \u002F\u002F Server-side amount validation\n  if (data.amount \u003C= 0 || data.amount > 10000) {\n    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n  }\n\n  \u002F\u002F Check user's balance from a trusted source\n  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n  if (userDoc.data().balance \u003C data.amount) {\n    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n  }\n\n  \u002F\u002F Process the transaction with admin privileges\n  \u002F\u002F ...\n});\n",[33,1748,1749,1754,1759,1764,1768,1773,1778,1783,1787,1791,1796,1801,1806,1811,1815,1819,1824,1829],{"__ignoreMap":310},[817,1750,1751],{"class":819,"line":820},[817,1752,1753],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[817,1755,1756],{"class":819,"line":311},[817,1757,1758],{},"  \u002F\u002F Verify authentication\n",[817,1760,1761],{"class":819,"line":831},[817,1762,1763],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[817,1765,1766],{"class":819,"line":837},[817,1767,972],{"emptyLinePlaceholder":352},[817,1769,1770],{"class":819,"line":843},[817,1771,1772],{},"  \u002F\u002F Server-side amount validation\n",[817,1774,1775],{"class":819,"line":849},[817,1776,1777],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[817,1779,1780],{"class":819,"line":855},[817,1781,1782],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[817,1784,1785],{"class":819,"line":861},[817,1786,858],{},[817,1788,1789],{"class":819,"line":994},[817,1790,972],{"emptyLinePlaceholder":352},[817,1792,1793],{"class":819,"line":999},[817,1794,1795],{},"  \u002F\u002F Check user's balance from a trusted source\n",[817,1797,1798],{"class":819,"line":1005},[817,1799,1800],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[817,1802,1803],{"class":819,"line":1011},[817,1804,1805],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[817,1807,1808],{"class":819,"line":1017},[817,1809,1810],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[817,1812,1813],{"class":819,"line":1023},[817,1814,858],{},[817,1816,1817],{"class":819,"line":1029},[817,1818,972],{"emptyLinePlaceholder":352},[817,1820,1821],{"class":819,"line":1035},[817,1822,1823],{},"  \u002F\u002F Process the transaction with admin privileges\n",[817,1825,1826],{"class":819,"line":1041},[817,1827,1828],{},"  \u002F\u002F ...\n",[817,1830,1831],{"class":819,"line":1046},[817,1832,1224],{},[14,1834,1835,1838],{},[17,1836,1837],{},"Data aggregation and denormalization",": use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[809,1840,1842],{"className":811,"code":1841,"language":813,"meta":310,"style":310},"exports.onPostCreated = functions.firestore\n  .document('posts\u002F{postId}')\n  .onCreate(async (snap, context) => {\n    \u002F\u002F Increment the author's post count atomically\n    const authorId = snap.data().authorId;\n    await admin.firestore().collection('users').doc(authorId).update({\n      postCount: admin.firestore.FieldValue.increment(1)\n    });\n  });\n",[33,1843,1844,1849,1854,1859,1864,1869,1874,1879,1884],{"__ignoreMap":310},[817,1845,1846],{"class":819,"line":820},[817,1847,1848],{},"exports.onPostCreated = functions.firestore\n",[817,1850,1851],{"class":819,"line":311},[817,1852,1853],{},"  .document('posts\u002F{postId}')\n",[817,1855,1856],{"class":819,"line":831},[817,1857,1858],{},"  .onCreate(async (snap, context) => {\n",[817,1860,1861],{"class":819,"line":837},[817,1862,1863],{},"    \u002F\u002F Increment the author's post count atomically\n",[817,1865,1866],{"class":819,"line":843},[817,1867,1868],{},"    const authorId = snap.data().authorId;\n",[817,1870,1871],{"class":819,"line":849},[817,1872,1873],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[817,1875,1876],{"class":819,"line":855},[817,1877,1878],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[817,1880,1881],{"class":819,"line":861},[817,1882,1883],{},"    });\n",[817,1885,1886],{"class":819,"line":994},[817,1887,1888],{},"  });\n",[14,1890,1891,1894],{},[17,1892,1893],{},"Rate limiting",": prevent abuse by tracking request frequency per user:",[809,1896,1898],{"className":811,"code":1897,"language":813,"meta":310,"style":310},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n  const userId = context.auth.uid;\n  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n\n  if (lastSubmission.exists) {\n    const lastTime = lastSubmission.data().timestamp.toMillis();\n    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n    }\n  }\n\n  \u002F\u002F Process the submission...\n});\n",[33,1899,1900,1905,1910,1915,1919,1924,1929,1934,1939,1943,1947,1951,1956],{"__ignoreMap":310},[817,1901,1902],{"class":819,"line":820},[817,1903,1904],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[817,1906,1907],{"class":819,"line":311},[817,1908,1909],{},"  const userId = context.auth.uid;\n",[817,1911,1912],{"class":819,"line":831},[817,1913,1914],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[817,1916,1917],{"class":819,"line":837},[817,1918,972],{"emptyLinePlaceholder":352},[817,1920,1921],{"class":819,"line":843},[817,1922,1923],{},"  if (lastSubmission.exists) {\n",[817,1925,1926],{"class":819,"line":849},[817,1927,1928],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[817,1930,1931],{"class":819,"line":855},[817,1932,1933],{},"    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n",[817,1935,1936],{"class":819,"line":861},[817,1937,1938],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[817,1940,1941],{"class":819,"line":994},[817,1942,852],{},[817,1944,1945],{"class":819,"line":999},[817,1946,858],{},[817,1948,1949],{"class":819,"line":1005},[817,1950,972],{"emptyLinePlaceholder":352},[817,1952,1953],{"class":819,"line":1011},[817,1954,1955],{},"  \u002F\u002F Process the submission...\n",[817,1957,1958],{"class":819,"line":1017},[817,1959,1224],{},[14,1961,1962],{},"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,1964,1966],{"id":1965},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,1968,1969,1972],{},[136,1970,735],{"href":1971},"\u002Fservice\u002Fflutter-development\u002F"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[762,1974,1975,1981,1987,1993,1999,2005],{},[252,1976,1977,1980],{},[17,1978,1979],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[252,1982,1983,1986],{},[17,1984,1985],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[252,1988,1989,1992],{},[17,1990,1991],{},"Comprehensive data validation"," in rules for every writable collection: type checking, field constraints, and immutability guards",[252,1994,1995,1998],{},[17,1996,1997],{},"Automated rules testing"," in CI\u002FCD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[252,2000,2001,2004],{},[17,2002,2003],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[252,2006,2007,2010,2011,2016],{},[17,2008,2009],{},"Security audits"," against the ",[136,2012,2015],{"href":2013,"rel":2014},"https:\u002F\u002Fowasp.org\u002Fwww-project-mobile-top-10\u002F",[221],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,2018,2019,2020,2023,2024,308],{},"App369 builds ",[136,2021,2022],{"href":1971},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[136,2025,2027],{"href":2026},"\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","Flutter security best practices",[25,2029,2031],{"id":2030},"faq","FAQ",[2033,2034,2036],"h3",{"id":2035},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,2038,2039,2040,2044],{},"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 ",[136,2041,2043],{"href":725,"rel":2042},[221],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[2033,2046,2048],{"id":2047},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,2050,2051],{},"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.",[2033,2053,2055],{"id":2054},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,2057,2058,2059,2062,2063,2066,2067,2070],{},"Use Firebase Authentication custom claims. Set ",[33,2060,2061],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[33,2064,2065],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[33,2068,2069],{},"users\u002F{userId}.isAdmin"," for authorization: clients can modify their own documents if write rules are misconfigured. Custom claims are set server-side and cannot be modified by clients.",[2033,2072,2074],{"id":2073},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,2076,2077,2078,2082],{},"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 ",[136,2079,2081],{"href":749,"rel":2080},[221],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[2084,2085,2086],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":310,"searchDepth":311,"depth":311,"links":2088},[2089,2090,2091,2092,2093,2094,2095,2096,2097],{"id":739,"depth":311,"text":740},{"id":787,"depth":311,"text":788},{"id":937,"depth":311,"text":938},{"id":1158,"depth":311,"text":1159},{"id":1316,"depth":311,"text":1317},{"id":1513,"depth":311,"text":1514},{"id":1730,"depth":311,"text":1731},{"id":1965,"depth":311,"text":1966},{"id":2030,"depth":311,"text":2031,"children":2098},[2099,2100,2101,2102],{"id":2035,"depth":831,"text":2036},{"id":2047,"depth":831,"text":2048},{"id":2054,"depth":831,"text":2055},{"id":2073,"depth":831,"text":2074},"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":2106},{"text":2107,"minutes":2108,"time":2109,"words":2110},"11 min read",10.4,624000,2080,"\u002Fblog\u002Ffirebase-security-guide-2026","11 min",[2114,2115,2116],"supabase-permission-denied-new-tables-october-30-2026","ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":712,"description":2104},"blog\u002Ffirebase-security-guide-2026","firebase security rules, firestore security, firebase authentication, cloud functions, role-based access control, firebase guide","2026-10-19T23:30:00.000Z","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-security-guide-2026\u002F","qV1XAaRetOZg8UFryn1EEA3iKirbYXctFCziN6E8aro",[2124,2126,2130,2133,2136,2139],{"keyword":5,"url":409,"title":5,"relevance":2125},70,{"keyword":2127,"url":2128,"title":2127,"relevance":2129},"Bolt App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbolt-app-rescue-2026\u002F",31,{"keyword":2131,"url":2132,"title":2131,"relevance":2129},"Lovable App Rescue: 7 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Flovable-app-rescue-2026\u002F",{"keyword":2134,"url":2135,"title":2134,"relevance":1085},"v0 App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fv0-app-rescue-2026\u002F",{"keyword":2137,"url":2138,"title":2137,"relevance":1085},"Base44 App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbase44-app-rescue-2026\u002F",{"keyword":414,"url":708,"title":414,"relevance":1080},1792488988572]