[{"data":1,"prerenderedAt":2586},["ShallowReactive",2],{"\u002Fblog\u002Flovable-app-rescue-2026\u002F":3,"\u002Fblog\u002Flovable-app-rescue-2026\u002F-related":397,"\u002Fblog\u002Flovable-app-rescue-2026\u002F-resources":2568},{"id":4,"title":5,"author":6,"body":10,"category":317,"checker":318,"createdAt":319,"description":320,"disclosure":321,"extension":322,"faq":323,"howTo":321,"lastVerified":339,"location":340,"meta":341,"navigation":347,"ogImage":321,"path":348,"primaryKeyword":349,"readingTime":350,"related":351,"seo":354,"sources":355,"stem":387,"tags":388,"takeaways":389,"updatedAt":319,"url":395,"__hash__":396},"blog\u002Fblog\u002Flovable-app-rescue-2026.md","Lovable App Rescue: 7 Problems to Fix Before Launch",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":304},"minimark",[13,21,24,29,37,54,60,70,74,77,82,87,91,98,113,118,122,129,142,147,151,154,159,176,180,183,186,201,206,210,213,235,238,243,247,265,269],[14,15,16,20],"p",{},[17,18,19],"strong",{},"Problems with real Lovable apps often show up in the same few places: database access rules, keys in the browser, server functions that don't check the caller, and the gap between the preview and the live app."," A Lovable app is a React front end that talks to a Supabase database, either Lovable Cloud (which Lovable says uses \"Supabase's open-source foundation\") or a Supabase project you connect. The browser calls the database directly with a public key, so the database rules are what keep one user's data away from another.",[14,22,23],{},"Below are the seven problems to check before launch, how to test each one in a few minutes, and how to fix it. The facts below come from Lovable's and Supabase's own documentation and from the public security report on Lovable apps, and I re-read each source on October 20, 2026. The how-to-check and how-to-fix steps are my own advice.",[25,26,28],"h2",{"id":27},"_1-tables-anyone-can-read-or-change","1. Tables anyone can read or change",[14,30,31,32,36],{},"Lovable's Supabase guide is blunt: \"Missing RLS policies are the most common way app data gets exposed.\" A table in the public schema with row-level security (RLS) turned off, or with a policy like ",[33,34,35],"code",{},"USING (true)",", can be read, and often changed, by anyone who has your app's public key. That key ships in your site's JavaScript by design.",[14,38,39,40,47,48,53],{},"This is not a hypothetical. ",[41,42,46],"a",{"href":43,"rel":44},"https:\u002F\u002Fnvd.nist.gov\u002Fvuln\u002Fdetail\u002FCVE-2025-48757",[45],"nofollow","CVE-2025-48757"," describes \"an insufficient database Row-Level Security policy in Lovable through 2025-04-15\" that, per the CVE text, allows unauthenticated attackers to read or write tables of generated sites. The entry is marked as disputed by Lovable, because each customer is responsible for protecting their own app's data. The researchers who filed it ",[41,49,52],{"href":50,"rel":51},"https:\u002F\u002Fmattpalmer.io\u002Fposts\u002Fstatement-on-CVE-2025-48757\u002F",[45],"report"," that their own March 2025 scan, which predates Lovable's current Quick and Deep scans, found 303 endpoints across 170 projects, about 10.3% of the 1,645 they analyzed, with inadequate RLS settings.",[14,55,56,59],{},[17,57,58],{},"How to check:"," open Supabase's Security Advisor and look for \"RLS disabled in public\" and \"policy exists, RLS disabled\". Then request each table with only the public key and no login. The free scan at the end of this page does that for you.",[14,61,62,65,66,69],{},[17,63,64],{},"How to fix:"," turn on RLS for every table in the public schema and write policies that tie rows to ",[33,67,68],{},"auth.uid()",". Remove any policy that lets everyone in.",[25,71,73],{"id":72},"_2-a-green-scan-with-the-wrong-policies","2. A green scan with the wrong policies",[14,75,76],{},"Lovable's Quick scan \"runs automatically every time you publish\" and checks things like tables without RLS. A table can have a policy and still let the wrong people in. The CVE researchers said in 2025 that the scanner they tested only checked that a policy existed, \"not its correctness\". Lovable's current Quick scan reviews database access rules, for example tables without per-record access control, but it is still a fixed set of checks. Lovable itself says its tools \"cannot guarantee complete security\".",[14,78,79,81],{},[17,80,58],{}," Lovable's own best-practice guide says to \"sign in as a second user and confirm you cannot see or change the first user's data.\" Do that for every screen that shows private data, and repeat it for each role your app has.",[14,83,84,86],{},[17,85,64],{}," rewrite any policy that fails the two-user test, then test reads, inserts, updates and deletes separately. A policy that blocks reads can still allow writes.",[25,88,90],{"id":89},"_3-secret-keys-in-the-browser","3. Secret keys in the browser",[14,92,93,94,97],{},"Lovable's security docs explain that it \"generates frontend code that runs in the browser, which means API keys and other secrets cannot be stored safely in client-side code.\" Supabase says the same about its secret and ",[33,95,96],{},"service_role"," keys: never put one in a browser, a shipped app or source control. Stripe secret keys, AI provider keys and admin keys belong in the same category.",[14,99,100,102,103,105,106,105,109,112],{},[17,101,58],{}," search your GitHub repository and the built JavaScript for ",[33,104,96],{},", ",[33,107,108],{},"sb_secret_",[33,110,111],{},"sk_live"," and your providers' key prefixes.",[14,114,115,117],{},[17,116,64],{}," rotate any secret that was ever exposed, then move the call into a server function that reads the key from Lovable's Secrets. Only the publishable or anon key belongs in the browser.",[25,119,121],{"id":120},"_4-server-functions-that-dont-check-who-is-calling","4. Server functions that don't check who is calling",[14,123,124,125,128],{},"Payments, emails, AI calls and admin actions usually run in Supabase Edge Functions that Lovable writes for you. A function that uses the service key skips RLS entirely, so it has to check the caller itself. Supabase's default is ",[33,126,127],{},"verify_jwt = true",", so \"the platform validates the JWT before your handler runs\", and it warns against turning that off just to clear an error.",[14,130,131,133,134,137,138,141],{},[17,132,58],{}," call each function with no token, then with a normal user's token for an action only an admin should take. Look for ",[33,135,136],{},"verify_jwt = false"," in ",[33,139,140],{},"supabase\u002Fconfig.toml",". Lovable's Deep scan reviews this kind of access control in your code, but \"the Deep scan does not run automatically\", so run it yourself.",[14,143,144,146],{},[17,145,64],{}," keep JWT verification on for anything a user triggers, check the user's role and ownership inside the function, and verify webhook signatures for calls from Stripe or other services.",[25,148,150],{"id":149},"_5-views-and-storage-that-skip-your-rules","5. Views and storage that skip your rules",[14,152,153],{},"Locking down a table is not enough if a view or a storage bucket exposes the same data. Supabase's advisor warns that a security definer view \"runs with elevated privileges and ignores Row-Level Security\". Files in a public storage bucket can be opened by anyone with the link.",[14,155,156,158],{},[17,157,58],{}," in the Security Advisor, look for \"security definer view\" and \"auth users exposed\". In Storage, list which buckets are public.",[14,160,161,163,164,167,168,171,172,175],{},[17,162,64],{}," recreate views with ",[33,165,166],{},"security_invoker = on",", copy the user fields you need into a ",[33,169,170],{},"profiles"," table instead of exposing ",[33,173,174],{},"auth.users",", and make buckets with private files private, with storage policies.",[25,177,179],{"id":178},"_6-the-supabase-october-30-2026-change","6. The Supabase October 30, 2026 change",[14,181,182],{},"Supabase has made \"no automatic grants\" the default for new projects since May 30, 2026, and applies it to all existing projects on October 30, 2026. Once it applies to your project, a new table in the public schema fails with \"permission denied for table\" until a migration grants it. Supabase also says \"existing tables are not affected\", so a table that is open today stays open.",[14,184,185],{},"If your Lovable app uses a Supabase project you own, the change applies. Lovable's docs don't say whether Lovable Cloud follows the same default or adds grants to the migrations it writes, so after October 30, create a test table and check it from the app.",[14,187,188,190,191,195,196,200],{},[17,189,58],{}," paste your migrations into the free ",[41,192,194],{"href":193},"\u002Ftools\u002Fsupabase-oct-30-checker\u002F","Supabase October 30 grants checker",". The ",[41,197,199],{"href":198},"\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026\u002F","full guide to the change"," has the exact grant lines.",[14,202,203,205],{},[17,204,64],{}," add a grant per role in the same migration that creates each table, next to RLS and its policies.",[25,207,209],{"id":208},"_7-no-safe-path-between-the-preview-and-production","7. No safe path between the preview and production",[14,211,212],{},"Three facts from Lovable's docs catch teams out once real users arrive:",[214,215,216,223,229],"ul",{},[217,218,219,222],"li",{},[17,220,221],{},"Reverting doesn't undo data changes."," Lovable's guide says \"reverting restores your code, not your database data\". A bad migration or a test that wrote to the live database stays in the database.",[217,224,225,228],{},[17,226,227],{},"The live site is a snapshot."," It only changes when you publish again, so the preview and the live app can drift apart.",[217,230,231,234],{},[17,232,233],{},"GitHub sync has limits."," \"Lovable only edits and syncs one branch at a time\", and \"Git sync always creates a new repository\", so you can't attach a project to an existing repository. Two people, or Lovable and a local editor, pushing to the same branch can overwrite each other's work.",[14,236,237],{},"Moving off Lovable Cloud is possible but manual, and removing Cloud \"permanently deletes your Cloud instance and cannot be undone\", so export your database and files first.",[14,239,240,242],{},[17,241,64],{}," export and test a database backup before launch, keep schema changes in migrations in the repository, agree who pushes to the synced branch, and run payments in test mode until the two-user test passes.",[25,244,246],{"id":245},"what-to-do-this-week","What to do this week",[248,249,250,253,256,259,262],"ol",{},[217,251,252],{},"Run the free leak scan below against your live site.",[217,254,255],{},"Do the two-user test on every page with private data.",[217,257,258],{},"Search your repository for secret keys and rotate anything you find.",[217,260,261],{},"Run Lovable's Deep scan and read every finding.",[217,263,264],{},"Check your newest migrations for grants before October 30.",[25,266,268],{"id":267},"need-a-hand","Need a hand?",[14,270,271,272,276,277,105,281,105,285,105,289,293,294,298,299,303],{},"If you'd rather have someone else check it, the ",[41,273,275],{"href":274},"\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 something else? See the ",[41,278,280],{"href":279},"\u002Fblog\u002Fbolt-app-rescue-2026\u002F","Bolt",[41,282,284],{"href":283},"\u002Fblog\u002Freplit-app-rescue-2026\u002F","Replit",[41,286,288],{"href":287},"\u002Fblog\u002Fcursor-app-rescue-2026\u002F","Cursor",[41,290,292],{"href":291},"\u002Fblog\u002Fbase44-app-rescue-2026\u002F","Base44"," and ",[41,295,297],{"href":296},"\u002Fblog\u002Fv0-app-rescue-2026\u002F","v0"," checklists. Or ",[41,300,302],{"href":301},"\u002Fcontact\u002F","send me a short description",".",{"title":305,"searchDepth":306,"depth":306,"links":307},"",2,[308,309,310,311,312,313,314,315,316],{"id":27,"depth":306,"text":28},{"id":72,"depth":306,"text":73},{"id":89,"depth":306,"text":90},{"id":120,"depth":306,"text":121},{"id":149,"depth":306,"text":150},{"id":178,"depth":306,"text":179},{"id":208,"depth":306,"text":209},{"id":245,"depth":306,"text":246},{"id":267,"depth":306,"text":268},"AI & Technology","supabase-leak-scan","2026-10-20T09:00:00.000Z","Your Lovable app works in the preview but real users are coming. The 7 problems to check first, from open tables to edge functions and the Supabase October 30 change, with how to test and fix each one.",null,"md",[324,327,330,333,336],{"question":325,"answer":326},"Is my Lovable app secure if the security scan passes?","Not necessarily. Lovable says its scans \"cannot guarantee complete security\", and the Quick scan that runs on publish does not review your app code. Run the Deep scan and test access yourself with two accounts and with no login.",{"question":328,"answer":329},"Why can other users see each other's data in my Lovable app?","Usually because a table has row-level security turned off, or a policy that lets everyone in, such as USING (true). The browser talks to the database with a public key, so the policies are the only thing deciding who sees which rows.",{"question":331,"answer":332},"Will the Supabase October 30, 2026 change break my Lovable app?","Existing tables keep working. Once the change applies to your project (new projects since May 30, 2026, all existing projects on October 30, 2026), new tables return \"permission denied\" until a migration grants them to the right roles. If your app uses a Supabase project you own, the change applies. Lovable's docs don't say how Lovable Cloud handles it, so test a new table after that date.",{"question":334,"answer":335},"Can I move my Lovable app off Lovable Cloud?","Yes, but it is manual. You export your data and files, connect your own Supabase project and rebuild the schema. Lovable says removing Cloud \"permanently deletes your Cloud instance and cannot be undone\", so export first.",{"question":337,"answer":338},"Do I keep my code if I hire someone to fix my Lovable app?","Yes. Lovable syncs your project to a GitHub repository you own, and a developer can work from that repository. Lovable only syncs one branch at a time, so agree on which branch Lovable edits before anyone else pushes to it.","2026-10-20","Boynton Beach, FL",{"readingTimeStats":342},{"text":343,"minutes":344,"time":345,"words":346},"7 min read",6.9,414000,1380,true,"\u002Fblog\u002Flovable-app-rescue-2026","lovable app rescue","8 min",[352,353],"supabase-permission-denied-new-tables-october-30-2026","firebase-security-guide-2026",{"title":5,"description":320},[356,359,362,365,368,371,373,375,378,381,384],{"title":357,"url":358},"Lovable docs, Security","https:\u002F\u002Fdocs.lovable.dev\u002Ffeatures\u002Fsecurity",{"title":360,"url":361},"Lovable docs, Supabase integration","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fsupabase",{"title":363,"url":364},"Lovable docs, Lovable Cloud","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fcloud",{"title":366,"url":367},"Lovable docs, GitHub integration","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fgithub",{"title":369,"url":370},"Lovable docs, Best practices","https:\u002F\u002Fdocs.lovable.dev\u002Ftips-tricks\u002Fbest-practice",{"title":372,"url":43},"NVD, CVE-2025-48757",{"title":374,"url":50},"Matt Palmer, Statement on CVE-2025-48757",{"title":376,"url":377},"Supabase changelog, tables not exposed to the Data API automatically","https:\u002F\u002Fsupabase.com\u002Fchangelog\u002F45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically",{"title":379,"url":380},"Supabase docs, Database advisors","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fdatabase-advisors",{"title":382,"url":383},"Supabase docs, API keys","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fapi\u002Fapi-keys",{"title":385,"url":386},"Supabase docs, Edge Functions auth","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Ffunctions\u002Fauth","blog\u002Flovable-app-rescue-2026","lovable app rescue, fix lovable app, lovable security, lovable supabase rls, lovable cloud, lovable production, ai app rescue, vibe coded app, supabase row level security",[390,391,392,393,394],"A Lovable app talks to its Supabase database straight from the browser with a public key, so row-level security (RLS) is what stops one user from reading everyone's data.","Lovable's own docs say \"Missing RLS policies are the most common way app data gets exposed,\" and that its scans \"cannot guarantee complete security.\"","The Quick scan runs every time you publish, but the Deep scan, which reviews your app code, does not run automatically.","Reverting in Lovable restores your code, not your database data, so test risky changes before real users depend on them.","Supabase now requires an explicit grant for new tables (in all existing projects from October 30, 2026), and existing tables keep theirs, so old leaks stay open.","https:\u002F\u002Fapp369.com\u002Fblog\u002Flovable-app-rescue-2026\u002F","Qh9ND95m05Zft1xna4NHBqPZ1C0YzR8BJQTWj3Dyxv4",[398,1193],{"id":399,"title":400,"author":401,"body":403,"category":317,"checker":321,"createdAt":1130,"description":1131,"disclosure":321,"extension":322,"faq":1132,"howTo":1148,"lastVerified":321,"location":340,"meta":1162,"navigation":347,"ogImage":321,"path":1168,"primaryKeyword":1169,"readingTime":1170,"related":321,"seo":1171,"sources":1172,"stem":1182,"tags":1183,"takeaways":1184,"updatedAt":1190,"url":1191,"__hash__":1192},"blog\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026.md","Supabase \"Permission Denied\" on New Tables: the October 30 Fix",{"name":7,"social":402},{"twitter":9},{"type":11,"value":404,"toc":1122},[405,428,434,438,446,505,547,550,586,590,601,664,683,687,696,886,889,955,959,988,995,999,1078,1085,1089,1112,1118],[14,406,407,410,411,414,415,105,418,293,421,423,424,427],{},[17,408,409],{},"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 ",[33,412,413],{},"public"," schema to the ",[33,416,417],{},"anon",[33,419,420],{},"authenticated",[33,422,96],{}," roles automatically. It now leaves that to you. Tables you already have keep working; a table created after the change returns ",[33,425,426],{},"permission denied for table"," until its migration includes the grants.",[14,429,430,431,433],{},"To see which tables in your migrations are affected, paste them into the free ",[41,432,194],{"href":193},". It runs in your browser and never uploads your SQL.",[25,435,437],{"id":436},"what-changes-and-when","What changes, and when",[14,439,440,441,445],{},"Supabase announced the change in its ",[41,442,444],{"href":377,"rel":443},[45],"changelog on April 28, 2026",":",[447,448,449,462],"table",{},[450,451,452],"thead",{},[453,454,455,459],"tr",{},[456,457,458],"th",{},"Date",[456,460,461],{},"What happens",[463,464,465,474,485,493],"tbody",{},[453,466,467,471],{},[468,469,470],"td",{},"April 28, 2026",[468,472,473],{},"New projects can opt out of exposing new tables when they are created",[453,475,476,479],{},[468,477,478],{},"May 18, 2026",[468,480,481,484],{},[33,482,483],{},"pg_graphql"," is no longer enabled by default",[453,486,487,490],{},[468,488,489],{},"May 30, 2026",[468,491,492],{},"New projects stop exposing new tables by default (rolled out over a few weeks)",[453,494,495,500],{},[468,496,497],{},[17,498,499],{},"October 30, 2026",[468,501,502],{},[17,503,504],{},"The same default applies to all existing projects",[14,506,507,508,512,513,515,516,519,520,105,522,293,524,526,527,530,531,534,535,538,539,542,543,546],{},"What changes is Postgres's ",[509,510,511],"em",{},"default privileges"," in the ",[33,514,413],{}," schema. Before, every new table was granted ",[33,517,518],{},"select, insert, update, delete"," to ",[33,521,417],{},[33,523,420],{},[33,525,96],{},", and new sequences got ",[33,528,529],{},"usage, select",". The hosted change removes those two defaults, and Supabase's setting is worded as \"Automatically expose new tables and functions\". Supabase's Securing your API guide says that on existing projects new functions receive ",[33,532,533],{},"EXECUTE"," by default, and that the platform change revokes these automatic grants, so plan to grant ",[33,536,537],{},"execute"," on any function you call through the Data API. (The local CLI revokes the defaults for new functions as well; see below.) Without those defaults, the Data API (REST at ",[33,540,541],{},"\u002Frest\u002Fv1"," and GraphQL at ",[33,544,545],{},"\u002Fgraphql\u002Fv1",") can't see a new table until a role is granted on it.",[14,548,549],{},"What doesn't change:",[214,551,552,558,564,580],{},[217,553,554,557],{},[17,555,556],{},"Existing tables"," keep the grants they already have.",[217,559,560,563],{},[17,561,562],{},"Row-level security"," works exactly as before.",[217,565,566,569,570,105,573,293,576,579],{},[17,567,568],{},"Other schemas",", such as ",[33,571,572],{},"auth",[33,574,575],{},"storage",[33,577,578],{},"realtime",", aren't affected.",[217,581,582,585],{},[17,583,584],{},"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,587,589],{"id":588},"the-symptom","The symptom",[14,591,592,593,597,598,445],{},"A request to a new table without grants fails with Postgres error 42501. This is the response ",[41,594,596],{"href":377,"rel":595},[45],"Supabase's changelog"," shows, with its example table renamed to ",[33,599,600],{},"projects",[602,603,607],"pre",{"className":604,"code":605,"language":606,"meta":305,"style":305},"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",[33,608,609,618,634,647,658],{"__ignoreMap":305},[610,611,614],"span",{"class":612,"line":613},"line",1,[610,615,617],{"class":616},"sVt8B","{\n",[610,619,620,624,627,631],{"class":612,"line":306},[610,621,623],{"class":622},"sj4cs","  \"code\"",[610,625,626],{"class":616},": ",[610,628,630],{"class":629},"sZZnC","\"42501\"",[610,632,633],{"class":616},",\n",[610,635,637,640,642,645],{"class":612,"line":636},3,[610,638,639],{"class":622},"  \"message\"",[610,641,626],{"class":616},[610,643,644],{"class":629},"\"permission denied for table projects\"",[610,646,633],{"class":616},[610,648,650,653,655],{"class":612,"line":649},4,[610,651,652],{"class":622},"  \"hint\"",[610,654,626],{"class":616},[610,656,657],{"class":629},"\"Grant the required privileges to the current role with: GRANT SELECT ON public.projects TO anon;\"\n",[610,659,661],{"class":612,"line":660},5,[610,662,663],{"class":616},"}\n",[14,665,666,667,670,671,674,675,678,679,682],{},"In supabase-js, the ",[33,668,669],{},"error"," object comes back filled and ",[33,672,673],{},"data"," is ",[33,676,677],{},"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 ",[33,680,681],{},"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,684,686],{"id":685},"the-fix-grant-in-the-same-migration","The fix: grant in the same migration",[14,688,689,690,695],{},"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 ",[41,691,694],{"href":692,"rel":693},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fapi\u002Fsecuring-your-api",[45],"Securing your API"," guide:",[602,697,701],{"className":698,"code":699,"language":700,"meta":305,"style":305},"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",[33,702,703,708,713,718,723,728,734,740,746,752,757,763,769,774,780,786,792,798,803,809,815,820,826,831,837,843,848,854,859,864,870,876,881],{"__ignoreMap":305},[610,704,705],{"class":612,"line":613},[610,706,707],{},"create table public.projects (\n",[610,709,710],{"class":612,"line":306},[610,711,712],{},"  id uuid primary key default gen_random_uuid(),\n",[610,714,715],{"class":612,"line":636},[610,716,717],{},"  user_id uuid not null references auth.users on delete cascade,\n",[610,719,720],{"class":612,"line":649},[610,721,722],{},"  name text not null\n",[610,724,725],{"class":612,"line":660},[610,726,727],{},");\n",[610,729,731],{"class":612,"line":730},6,[610,732,733],{"emptyLinePlaceholder":347},"\n",[610,735,737],{"class":612,"line":736},7,[610,738,739],{},"-- 1. Who can reach the table at all\n",[610,741,743],{"class":612,"line":742},8,[610,744,745],{},"grant select, insert, update, delete on table public.projects to authenticated;\n",[610,747,749],{"class":612,"line":748},9,[610,750,751],{},"grant select, insert, update, delete on table public.projects to service_role;\n",[610,753,755],{"class":612,"line":754},10,[610,756,733],{"emptyLinePlaceholder":347},[610,758,760],{"class":612,"line":759},11,[610,761,762],{},"-- 2. Which rows they can see and change\n",[610,764,766],{"class":612,"line":765},12,[610,767,768],{},"alter table public.projects enable row level security;\n",[610,770,772],{"class":612,"line":771},13,[610,773,733],{"emptyLinePlaceholder":347},[610,775,777],{"class":612,"line":776},14,[610,778,779],{},"create policy \"Owners can read their projects\"\n",[610,781,783],{"class":612,"line":782},15,[610,784,785],{},"  on public.projects for select\n",[610,787,789],{"class":612,"line":788},16,[610,790,791],{},"  to authenticated\n",[610,793,795],{"class":612,"line":794},17,[610,796,797],{},"  using ((select auth.uid()) = user_id);\n",[610,799,801],{"class":612,"line":800},18,[610,802,733],{"emptyLinePlaceholder":347},[610,804,806],{"class":612,"line":805},19,[610,807,808],{},"create policy \"Owners can add projects\"\n",[610,810,812],{"class":612,"line":811},20,[610,813,814],{},"  on public.projects for insert\n",[610,816,818],{"class":612,"line":817},21,[610,819,791],{},[610,821,823],{"class":612,"line":822},22,[610,824,825],{},"  with check ((select auth.uid()) = user_id);\n",[610,827,829],{"class":612,"line":828},23,[610,830,733],{"emptyLinePlaceholder":347},[610,832,834],{"class":612,"line":833},24,[610,835,836],{},"create policy \"Owners can update their projects\"\n",[610,838,840],{"class":612,"line":839},25,[610,841,842],{},"  on public.projects for update\n",[610,844,846],{"class":612,"line":845},26,[610,847,791],{},[610,849,851],{"class":612,"line":850},27,[610,852,853],{},"  using ((select auth.uid()) = user_id)\n",[610,855,857],{"class":612,"line":856},28,[610,858,825],{},[610,860,862],{"class":612,"line":861},29,[610,863,733],{"emptyLinePlaceholder":347},[610,865,867],{"class":612,"line":866},30,[610,868,869],{},"create policy \"Owners can delete their projects\"\n",[610,871,873],{"class":612,"line":872},31,[610,874,875],{},"  on public.projects for delete\n",[610,877,879],{"class":612,"line":878},32,[610,880,791],{},[610,882,884],{"class":612,"line":883},33,[610,885,797],{},[14,887,888],{},"A few details are easy to miss:",[214,890,891,904,923,946],{},[217,892,893,896,897,899,900,903],{},[17,894,895],{},"Grant only what each role needs."," A table that signed-out visitors should never touch gets no ",[33,898,417],{}," grant at all. Public read-only data gets ",[33,901,902],{},"grant select ... to anon"," and nothing more.",[217,905,906,912,913,915,916,919,920,303],{},[17,907,908,911],{},[33,909,910],{},"serial"," columns need a sequence grant."," If the table uses ",[33,914,910],{}," or ",[33,917,918],{},"bigserial"," rather than an identity column or a UUID, inserts also need ",[33,921,922],{},"grant usage, select on sequence public.projects_id_seq to authenticated;",[217,924,925,930,931,934,935,938,939,942,943,945],{},[17,926,927,928,303],{},"Functions called over the API need ",[33,929,537],{}," For a function your app calls with ",[33,932,933],{},"rpc()",", use ",[33,936,937],{},"grant execute on function public.your_function() to authenticated;",". A ",[33,940,941],{},"security definer"," function runs with its owner's rights and bypasses RLS, so grant ",[33,944,537],{}," only to roles that should call it, and review those functions.",[217,947,948,951,952,954],{},[17,949,950],{},"Grants are not a replacement for RLS."," With a grant and RLS off, any signed-in user, or anyone at all if ",[33,953,417],{}," is granted, can read every row. Turn RLS on for every exposed table.",[25,956,958],{"id":957},"local-development-already-works-this-way","Local development already works this way",[14,960,961,962,967,968,293,971,974,975,977,978,137,981,984,985,303],{},"If local tables work but the hosted project doesn't (or the other way round), check your Supabase CLI version. Since ",[41,963,966],{"href":964,"rel":965},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fcli\u002Freleases\u002Ftag\u002Fv2.106.0",[45],"Supabase CLI v2.106.0"," (June 11, 2026), ",[33,969,970],{},"supabase start",[33,972,973],{},"supabase db reset"," revoke the default Data API privileges for new tables, sequences and functions in ",[33,976,413],{},", because ",[33,979,980],{},"[api].auto_expose_new_tables",[33,982,983],{},"config.toml"," now defaults to ",[33,986,987],{},"false",[14,989,990,991,994],{},"So a migration without grants fails locally with the same 42501 error. That's useful: it shows the problem before you deploy. Setting ",[33,992,993],{},"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,996,998],{"id":997},"check-your-project-before-october-30","Check your project before October 30",[248,1000,1001,1022,1031,1039],{},[217,1002,1003,1006,1007,1010,1011,1014,1015,1017,1018,1021],{},[17,1004,1005],{},"List your newest migrations"," (",[33,1008,1009],{},"supabase\u002Fmigrations\u002F"," in the repo, or the SQL your AI tool generated) and look for ",[33,1012,1013],{},"create table"," statements in ",[33,1016,413],{}," without matching ",[33,1019,1020],{},"grant"," lines.",[217,1023,1024,1030],{},[17,1025,1026,1027,303],{},"Run them through the ",[41,1028,1029],{"href":193},"grants checker"," It lists each table that would need a grant, each table with RLS off, and each policy that lets anyone in.",[217,1032,1033,1038],{},[17,1034,1035,1036,303],{},"Test as a real user, not as ",[33,1037,681],{}," 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.",[217,1040,1041,1044,1045,1049,1050,1074,1077],{},[17,1042,1043],{},"Opt in early if you can."," Supabase lets existing projects switch to the new default before October 30. The ",[41,1046,1048],{"href":377,"rel":1047},[45],"changelog"," gives the SQL to run in the SQL editor; it changes only tables and sequences created afterwards:",[602,1051,1053],{"className":698,"code":1052,"language":700,"meta":305,"style":305},"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",[33,1054,1055,1060,1065,1069],{"__ignoreMap":305},[610,1056,1057],{"class":612,"line":613},[610,1058,1059],{},"alter default privileges for role postgres in schema public\n",[610,1061,1062],{"class":612,"line":306},[610,1063,1064],{},"  revoke select, insert, update, delete on tables from anon, authenticated, service_role;\n",[610,1066,1067],{"class":612,"line":636},[610,1068,1059],{},[610,1070,1071],{"class":612,"line":649},[610,1072,1073],{},"  revoke usage, select on sequences from anon, authenticated, service_role;\n",[1075,1076],"br",{},"Doing it on a staging project first shows you any missing grants on your schedule instead of the deadline's.",[14,1079,1080,1081,303],{},"For every other dated Apple, Google Play and Firebase requirement in 2026 and 2027, see ",[41,1082,1084],{"href":1083},"\u002Fblog\u002Fapp-store-google-play-deadlines-2026-2027\u002F","App Store and Google Play deadlines for 2026 and 2027",[25,1086,1088],{"id":1087},"if-you-use-lovable-bolt-or-another-ai-builder","If you use Lovable, Bolt or another AI builder",[14,1090,1091,1092,137,1094,1096,1097,1099,1100,1103,1104,1108,1109,303],{},"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 ",[33,1093,1013],{},[33,1095,413],{}," should be followed by a ",[33,1098,1020],{}," for each role that needs it and by ",[33,1101,1102],{},"enable row level security",". If you can't read the SQL yourself, paste it into the checker; it flags missing grants in plain language. On Lovable, the grants are one of ",[41,1105,1107],{"href":1106},"\u002Fblog\u002Flovable-app-rescue-2026\u002F","seven problems to check before launch",", and Bolt has ",[41,1110,1111],{"href":279},"its own pre-launch checklist",[14,1113,1114,1115,1117],{},"If your AI-built app has more than a couple of these problems, or users are already seeing empty screens, App369's ",[41,1116,275],{"href":274}," 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.",[1119,1120,1121],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":305,"searchDepth":306,"depth":306,"links":1123},[1124,1125,1126,1127,1128,1129],{"id":436,"depth":306,"text":437},{"id":588,"depth":306,"text":589},{"id":685,"depth":306,"text":686},{"id":957,"depth":306,"text":958},{"id":997,"depth":306,"text":998},{"id":1087,"depth":306,"text":1088},"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.",[1133,1136,1139,1142,1145],{"question":1134,"answer":1135},"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":1137,"answer":1138},"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":1140,"answer":1141},"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":1143,"answer":1144},"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":1146,"answer":1147},"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":1149,"steps":1150},"Check your Supabase project before October 30",[1151,1153,1156,1159],{"name":1005,"text":1152},"List your newest migrations and look for create table statements in public without matching grant lines.",{"name":1154,"text":1155},"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":1157,"text":1158},"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":1160,"text":1161},"Opt in early if you can","Supabase lets existing projects switch to the new default before October 30.",{"readingTimeStats":1163},{"text":1164,"minutes":1165,"time":1166,"words":1167},"6 min read",6.005,360300,1201,"\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026","supabase permission denied for table","7 min",{"title":400,"description":1131},[1173,1175,1177,1179],{"title":1174,"url":964},"Supabase CLI v2.106.0 release notes (June 11, 2026)",{"title":1176,"url":377},"Supabase changelog: tables not exposed to Data and GraphQL API automatically (April 28, 2026)",{"title":1178,"url":692},"Supabase docs: Securing your API",{"title":1180,"url":1181},"PostgreSQL docs: GRANT","https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-grant.html","blog\u002Fsupabase-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",[1185,1186,1187,1188,1189],"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-19T23:50:00.000Z","https:\u002F\u002Fapp369.com\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026\u002F","xqTfOTDFTyma2FYucqhgtCWL01vHENNqh4Amk3f4ZHI",{"id":1194,"title":1195,"author":1196,"body":1198,"category":317,"checker":321,"createdAt":2549,"description":2550,"disclosure":321,"extension":322,"faq":321,"howTo":321,"lastVerified":321,"location":340,"meta":2551,"navigation":347,"ogImage":321,"path":2557,"primaryKeyword":321,"readingTime":2558,"related":2559,"seo":2562,"sources":321,"stem":2563,"tags":2564,"takeaways":321,"updatedAt":2565,"url":2566,"__hash__":2567},"blog\u002Fblog\u002Ffirebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":1197},{"twitter":9},{"type":11,"value":1199,"toc":2533},[1200,1212,1220,1224,1227,1241,1244,1264,1267,1271,1287,1290,1336,1339,1345,1383,1386,1405,1409,1418,1421,1540,1543,1605,1608,1612,1621,1624,1677,1680,1732,1739,1742,1766,1770,1773,1776,1879,1882,1959,1962,1966,1975,1983,1988,2010,2019,2130,2133,2175,2179,2182,2185,2191,2280,2286,2336,2342,2407,2410,2414,2420,2464,2475,2479,2484,2492,2496,2499,2503,2518,2522,2530],[14,1201,1202,1205,1206,1211],{},[17,1203,1204],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," According to ",[41,1207,1210],{"href":1208,"rel":1209},"https:\u002F\u002Fwww.audityour.app\u002Fguides\u002Ffirebase-security-rules-guide",[45],"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,1213,1214,1215,1219],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[41,1216,1218],{"href":1217},"\u002Fservice\u002Fmobile-apps\u002F","App369"," uses to secure Firebase backends across production applications.",[25,1221,1223],{"id":1222},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,1225,1226],{},"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,1228,1229,1230,1235,1236,1240],{},"According to ",[41,1231,1234],{"href":1232,"rel":1233},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002F",[45],"Firebase's official documentation",", Security Rules stand between your data and malicious users, and ",[41,1237,1239],{"href":1232,"rel":1238},[45],"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,1242,1243],{},"Rules apply to three Firebase services independently:",[214,1245,1246,1252,1258],{},[217,1247,1248,1251],{},[17,1249,1250],{},"Cloud Firestore",": document-level read\u002Fwrite control with path-based matching",[217,1253,1254,1257],{},[17,1255,1256],{},"Realtime Database",": node-level access with cascading permissions",[217,1259,1260,1263],{},[17,1261,1262],{},"Cloud Storage",": file-level access based on path, metadata, and authentication state",[14,1265,1266],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[25,1268,1270],{"id":1269},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,1272,1273,1274,293,1277,1280,1281,1286],{},"Firebase's setup wizard offers two starting configurations: ",[17,1275,1276],{},"test mode",[17,1278,1279],{},"locked mode",". Test mode grants public read and write access to the entire database until an expiry date. ",[41,1282,1285],{"href":1283,"rel":1284},"https:\u002F\u002Fvibeappscanner.com\u002Fis-firebase-safe",[45],"VibeAppScanner's Firebase safety analysis"," calls test mode the number one cause of Firebase data breaches.",[14,1288,1289],{},"Test mode rules look like this:",[602,1291,1295],{"className":1292,"code":1293,"language":1294,"meta":305,"style":305},"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,1296,1297,1302,1307,1312,1317,1322,1327,1332],{"__ignoreMap":305},[610,1298,1299],{"class":612,"line":613},[610,1300,1301],{},"rules_version = '2';\n",[610,1303,1304],{"class":612,"line":306},[610,1305,1306],{},"service cloud.firestore {\n",[610,1308,1309],{"class":612,"line":636},[610,1310,1311],{},"  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n",[610,1313,1314],{"class":612,"line":649},[610,1315,1316],{},"    match \u002F{document=**} {\n",[610,1318,1319],{"class":612,"line":660},[610,1320,1321],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[610,1323,1324],{"class":612,"line":730},[610,1325,1326],{},"    }\n",[610,1328,1329],{"class":612,"line":736},[610,1330,1331],{},"  }\n",[610,1333,1334],{"class":612,"line":742},[610,1335,663],{},[14,1337,1338],{},"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,1340,1341,1342,1344],{},"The correct starting point is ",[17,1343,1279],{},", where all access is denied by default:",[602,1346,1348],{"className":1292,"code":1347,"language":1294,"meta":305,"style":305},"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,1349,1350,1354,1358,1362,1366,1371,1375,1379],{"__ignoreMap":305},[610,1351,1352],{"class":612,"line":613},[610,1353,1301],{},[610,1355,1356],{"class":612,"line":306},[610,1357,1306],{},[610,1359,1360],{"class":612,"line":636},[610,1361,1311],{},[610,1363,1364],{"class":612,"line":649},[610,1365,1316],{},[610,1367,1368],{"class":612,"line":660},[610,1369,1370],{},"      allow read, write: if false;\n",[610,1372,1373],{"class":612,"line":730},[610,1374,1326],{},[610,1376,1377],{"class":612,"line":736},[610,1378,1331],{},[610,1380,1381],{"class":612,"line":742},[610,1382,663],{},[14,1384,1385],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,1387,1388,1389,1392,1393,1395,1396,105,1399,105,1401,293,1403,303],{},"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 ",[41,1390,1391],{"href":274},"AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[41,1394,194],{"href":193}," lists the tables that will need explicit grants after that date. Builder-specific checklists: ",[41,1397,1398],{"href":1106},"Lovable",[41,1400,280],{"href":279},[41,1402,284],{"href":283},[41,1404,288],{"href":287},[25,1406,1408],{"id":1407},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,1410,1411,1412,1417],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[41,1413,1416],{"href":1414,"rel":1415},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Frules-structure",[45],"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,1419,1420],{},"Here is a production-ready pattern for a user-owned data collection:",[602,1422,1424],{"className":1292,"code":1423,"language":1294,"meta":305,"style":305},"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,1425,1426,1430,1434,1438,1442,1447,1452,1457,1461,1465,1470,1475,1480,1485,1490,1495,1500,1504,1508,1513,1518,1523,1528,1532,1536],{"__ignoreMap":305},[610,1427,1428],{"class":612,"line":613},[610,1429,1301],{},[610,1431,1432],{"class":612,"line":306},[610,1433,1306],{},[610,1435,1436],{"class":612,"line":636},[610,1437,1311],{},[610,1439,1440],{"class":612,"line":649},[610,1441,733],{"emptyLinePlaceholder":347},[610,1443,1444],{"class":612,"line":660},[610,1445,1446],{},"    \u002F\u002F Users can only read and write their own profile\n",[610,1448,1449],{"class":612,"line":730},[610,1450,1451],{},"    match \u002Fusers\u002F{userId} {\n",[610,1453,1454],{"class":612,"line":736},[610,1455,1456],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[610,1458,1459],{"class":612,"line":742},[610,1460,1326],{},[610,1462,1463],{"class":612,"line":748},[610,1464,733],{"emptyLinePlaceholder":347},[610,1466,1467],{"class":612,"line":754},[610,1468,1469],{},"    \u002F\u002F Posts are publicly readable, but only the author can write\n",[610,1471,1472],{"class":612,"line":759},[610,1473,1474],{},"    match \u002Fposts\u002F{postId} {\n",[610,1476,1477],{"class":612,"line":765},[610,1478,1479],{},"      allow read: if true;\n",[610,1481,1482],{"class":612,"line":771},[610,1483,1484],{},"      allow create: if request.auth != null\n",[610,1486,1487],{"class":612,"line":776},[610,1488,1489],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[610,1491,1492],{"class":612,"line":782},[610,1493,1494],{},"      allow update, delete: if request.auth != null\n",[610,1496,1497],{"class":612,"line":788},[610,1498,1499],{},"                            && resource.data.authorId == request.auth.uid;\n",[610,1501,1502],{"class":612,"line":794},[610,1503,1326],{},[610,1505,1506],{"class":612,"line":800},[610,1507,733],{"emptyLinePlaceholder":347},[610,1509,1510],{"class":612,"line":805},[610,1511,1512],{},"    \u002F\u002F Admin-only collection\n",[610,1514,1515],{"class":612,"line":811},[610,1516,1517],{},"    match \u002FadminConfig\u002F{docId} {\n",[610,1519,1520],{"class":612,"line":817},[610,1521,1522],{},"      allow read, write: if request.auth != null\n",[610,1524,1525],{"class":612,"line":822},[610,1526,1527],{},"                         && request.auth.token.admin == true;\n",[610,1529,1530],{"class":612,"line":828},[610,1531,1326],{},[610,1533,1534],{"class":612,"line":833},[610,1535,1331],{},[610,1537,1538],{"class":612,"line":839},[610,1539,663],{},[14,1541,1542],{},"Key principles for effective rules:",[214,1544,1545,1569,1578,1591],{},[217,1546,1547,1550,1551,293,1554,1557,1558,105,1561,1564,1565,1568],{},[17,1548,1549],{},"Separate read and write permissions."," Use ",[33,1552,1553],{},"allow read",[33,1555,1556],{},"allow write"," independently. Break write into ",[33,1559,1560],{},"create",[33,1562,1563],{},"update",", and ",[33,1566,1567],{},"delete"," when different logic applies.",[217,1570,1571,1577],{},[17,1572,1573,1574,303],{},"Always check ",[33,1575,1576],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[217,1579,1580,293,1587,1590],{},[17,1581,1582,1583,1586],{},"Use ",[33,1584,1585],{},"resource.data"," for existing document fields",[33,1588,1589],{},"request.resource.data"," for incoming data in create\u002Fupdate operations.",[217,1592,1593,1596,1597,1600,1601,1604],{},[17,1594,1595],{},"Avoid wildcards at the root level."," The pattern ",[33,1598,1599],{},"match \u002F{document=**}"," applies to every document in the database. Use it only with ",[33,1602,1603],{},"if false"," as a catch-all deny rule.",[14,1606,1607],{},"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,1609,1611],{"id":1610},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,1613,1614,1615,1620],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[41,1616,1619],{"href":1617,"rel":1618},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fadmin\u002Fcustom-claims",[45],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,1622,1623],{},"Set custom claims from a Cloud Function or admin server:",[602,1625,1627],{"className":1292,"code":1626,"language":1294,"meta":305,"style":305},"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,1628,1629,1634,1638,1643,1648,1652,1657,1662,1667,1672],{"__ignoreMap":305},[610,1630,1631],{"class":612,"line":613},[610,1632,1633],{},"const admin = require('firebase-admin');\n",[610,1635,1636],{"class":612,"line":306},[610,1637,733],{"emptyLinePlaceholder":347},[610,1639,1640],{"class":612,"line":636},[610,1641,1642],{},"\u002F\u002F Assign admin role\n",[610,1644,1645],{"class":612,"line":649},[610,1646,1647],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[610,1649,1650],{"class":612,"line":660},[610,1651,733],{"emptyLinePlaceholder":347},[610,1653,1654],{"class":612,"line":730},[610,1655,1656],{},"\u002F\u002F Assign editor role with department\n",[610,1658,1659],{"class":612,"line":736},[610,1660,1661],{},"await admin.auth().setCustomUserClaims(userId, {\n",[610,1663,1664],{"class":612,"line":742},[610,1665,1666],{},"  role: 'editor',\n",[610,1668,1669],{"class":612,"line":748},[610,1670,1671],{},"  department: 'marketing'\n",[610,1673,1674],{"class":612,"line":754},[610,1675,1676],{},"});\n",[14,1678,1679],{},"Reference claims in Security Rules:",[602,1681,1683],{"className":1292,"code":1682,"language":1294,"meta":305,"style":305},"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,1684,1685,1690,1695,1700,1704,1709,1714,1718,1723,1728],{"__ignoreMap":305},[610,1686,1687],{"class":612,"line":613},[610,1688,1689],{},"match \u002Fcontent\u002F{docId} {\n",[610,1691,1692],{"class":612,"line":306},[610,1693,1694],{},"  \u002F\u002F Admins have full access\n",[610,1696,1697],{"class":612,"line":636},[610,1698,1699],{},"  allow read, write: if request.auth.token.admin == true;\n",[610,1701,1702],{"class":612,"line":649},[610,1703,733],{"emptyLinePlaceholder":347},[610,1705,1706],{"class":612,"line":660},[610,1707,1708],{},"  \u002F\u002F Editors can read and update, but not delete\n",[610,1710,1711],{"class":612,"line":730},[610,1712,1713],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[610,1715,1716],{"class":612,"line":736},[610,1717,733],{"emptyLinePlaceholder":347},[610,1719,1720],{"class":612,"line":742},[610,1721,1722],{},"  \u002F\u002F All authenticated users can read\n",[610,1724,1725],{"class":612,"line":748},[610,1726,1727],{},"  allow read: if request.auth != null;\n",[610,1729,1730],{"class":612,"line":754},[610,1731,663],{},[14,1733,1734,1735,1738],{},"As ",[41,1736,1619],{"href":1617,"rel":1737},[45]," 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,1740,1741],{},"Implementation guidelines:",[214,1743,1744,1750,1760],{},[217,1745,1746,1749],{},[17,1747,1748],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[217,1751,1752,1755,1756,1759],{},[17,1753,1754],{},"Force token refresh after claim changes."," Call ",[33,1757,1758],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[217,1761,1762,1765],{},[17,1763,1764],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[25,1767,1769],{"id":1768},"data-validation-in-security-rules","Data Validation in Security Rules",[14,1771,1772],{},"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,1774,1775],{},"Validation patterns for common requirements:",[602,1777,1779],{"className":1292,"code":1778,"language":1294,"meta":305,"style":305},"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,1780,1781,1786,1791,1796,1801,1806,1811,1816,1821,1826,1831,1836,1841,1846,1851,1856,1860,1865,1870,1875],{"__ignoreMap":305},[610,1782,1783],{"class":612,"line":613},[610,1784,1785],{},"match \u002Fusers\u002F{userId} {\n",[610,1787,1788],{"class":612,"line":306},[610,1789,1790],{},"  allow create: if request.auth.uid == userId\n",[610,1792,1793],{"class":612,"line":636},[610,1794,1795],{},"    \u002F\u002F Required fields must exist\n",[610,1797,1798],{"class":612,"line":649},[610,1799,1800],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[610,1802,1803],{"class":612,"line":660},[610,1804,1805],{},"    \u002F\u002F Name must be a string between 2 and 100 characters\n",[610,1807,1808],{"class":612,"line":730},[610,1809,1810],{},"    && request.resource.data.name is string\n",[610,1812,1813],{"class":612,"line":736},[610,1814,1815],{},"    && request.resource.data.name.size() >= 2\n",[610,1817,1818],{"class":612,"line":742},[610,1819,1820],{},"    && request.resource.data.name.size() \u003C= 100\n",[610,1822,1823],{"class":612,"line":748},[610,1824,1825],{},"    \u002F\u002F Email must be a string and match the auth email\n",[610,1827,1828],{"class":612,"line":754},[610,1829,1830],{},"    && request.resource.data.email is string\n",[610,1832,1833],{"class":612,"line":759},[610,1834,1835],{},"    && request.resource.data.email == request.auth.token.email\n",[610,1837,1838],{"class":612,"line":765},[610,1839,1840],{},"    \u002F\u002F createdAt must be a timestamp\n",[610,1842,1843],{"class":612,"line":771},[610,1844,1845],{},"    && request.resource.data.createdAt is timestamp\n",[610,1847,1848],{"class":612,"line":776},[610,1849,1850],{},"    \u002F\u002F No unexpected fields allowed\n",[610,1852,1853],{"class":612,"line":782},[610,1854,1855],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[610,1857,1858],{"class":612,"line":788},[610,1859,733],{"emptyLinePlaceholder":347},[610,1861,1862],{"class":612,"line":794},[610,1863,1864],{},"  allow update: if request.auth.uid == userId\n",[610,1866,1867],{"class":612,"line":800},[610,1868,1869],{},"    \u002F\u002F Prevent changing the email field\n",[610,1871,1872],{"class":612,"line":805},[610,1873,1874],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']));\n",[610,1876,1877],{"class":612,"line":811},[610,1878,663],{},[14,1880,1881],{},"Validation capabilities in Firestore rules include:",[214,1883,1884,1912,1922,1935,1944,1950],{},[217,1885,1886,1889,1890,105,1893,105,1896,105,1899,105,1902,105,1905,1908,1909],{},[17,1887,1888],{},"Type checking",": verify that fields are ",[33,1891,1892],{},"string",[33,1894,1895],{},"int",[33,1897,1898],{},"float",[33,1900,1901],{},"bool",[33,1903,1904],{},"timestamp",[33,1906,1907],{},"list",", or ",[33,1910,1911],{},"map",[217,1913,1914,1917,1918,1921],{},[17,1915,1916],{},"String length",": use ",[33,1919,1920],{},".size()"," to enforce minimum and maximum lengths",[217,1923,1924,626,1927,1930,1931,1934],{},[17,1925,1926],{},"Field existence",[33,1928,1929],{},"hasAll()"," for required fields, ",[33,1932,1933],{},"hasOnly()"," to prevent extra fields",[217,1936,1937,1917,1940,1943],{},[17,1938,1939],{},"Field immutability",[33,1941,1942],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[217,1945,1946,1949],{},[17,1947,1948],{},"Numeric ranges",": enforce minimum and maximum values for numbers",[217,1951,1952,1955,1956,1958],{},[17,1953,1954],{},"List constraints",": check ",[33,1957,1920],{}," on lists to limit array lengths",[14,1960,1961],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[25,1963,1965],{"id":1964},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,1967,1968,1969,1974],{},"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; ",[41,1970,1973],{"href":1971,"rel":1972},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002Funit-tests",[45],"Firebase's testing documentation"," shows how to unit test rules with the emulator.",[14,1976,1977,1978,1982],{},"For a quick first pass, paste your rules into App369's free ",[41,1979,1981],{"href":1980},"\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,1984,1985],{},[17,1986,1987],{},"Use the Firebase Emulator Suite for local testing:",[602,1989,1993],{"className":1990,"code":1991,"language":1992,"meta":305,"style":305},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[33,1994,1995],{"__ignoreMap":305},[610,1996,1997,2001,2004,2007],{"class":612,"line":613},[610,1998,2000],{"class":1999},"sScJk","firebase",[610,2002,2003],{"class":629}," emulators:start",[610,2005,2006],{"class":622}," --only",[610,2008,2009],{"class":629}," firestore\n",[14,2011,2012],{},[17,2013,2014,2015,2018],{},"Write unit tests with the ",[33,2016,2017],{},"@firebase\u002Frules-unit-testing"," package:",[602,2020,2022],{"className":1292,"code":2021,"language":1294,"meta":305,"style":305},"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,2023,2024,2029,2033,2038,2043,2048,2052,2056,2061,2066,2071,2076,2080,2084,2089,2094,2099,2103,2107,2112,2117,2121,2126],{"__ignoreMap":305},[610,2025,2026],{"class":612,"line":613},[610,2027,2028],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n",[610,2030,2031],{"class":612,"line":306},[610,2032,733],{"emptyLinePlaceholder":347},[610,2034,2035],{"class":612,"line":636},[610,2036,2037],{},"const testEnv = await initializeTestEnvironment({\n",[610,2039,2040],{"class":612,"line":649},[610,2041,2042],{},"  projectId: 'your-project-id',\n",[610,2044,2045],{"class":612,"line":660},[610,2046,2047],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[610,2049,2050],{"class":612,"line":730},[610,2051,1676],{},[610,2053,2054],{"class":612,"line":736},[610,2055,733],{"emptyLinePlaceholder":347},[610,2057,2058],{"class":612,"line":742},[610,2059,2060],{},"\u002F\u002F Test: authenticated user can read their own document\n",[610,2062,2063],{"class":612,"line":748},[610,2064,2065],{},"const userContext = testEnv.authenticatedContext('user123');\n",[610,2067,2068],{"class":612,"line":754},[610,2069,2070],{},"await assertSucceeds(\n",[610,2072,2073],{"class":612,"line":759},[610,2074,2075],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[610,2077,2078],{"class":612,"line":765},[610,2079,727],{},[610,2081,2082],{"class":612,"line":771},[610,2083,733],{"emptyLinePlaceholder":347},[610,2085,2086],{"class":612,"line":776},[610,2087,2088],{},"\u002F\u002F Test: user cannot read another user's document\n",[610,2090,2091],{"class":612,"line":782},[610,2092,2093],{},"await assertFails(\n",[610,2095,2096],{"class":612,"line":788},[610,2097,2098],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[610,2100,2101],{"class":612,"line":794},[610,2102,727],{},[610,2104,2105],{"class":612,"line":800},[610,2106,733],{"emptyLinePlaceholder":347},[610,2108,2109],{"class":612,"line":805},[610,2110,2111],{},"\u002F\u002F Test: unauthenticated user cannot write\n",[610,2113,2114],{"class":612,"line":811},[610,2115,2116],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[610,2118,2119],{"class":612,"line":817},[610,2120,2093],{},[610,2122,2123],{"class":612,"line":822},[610,2124,2125],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[610,2127,2128],{"class":612,"line":828},[610,2129,727],{},[14,2131,2132],{},"Deployment workflow:",[248,2134,2135,2149,2155,2163,2169],{},[217,2136,2137,137,2140,105,2143,1564,2146],{},[17,2138,2139],{},"Write rules locally",[33,2141,2142],{},"firestore.rules",[33,2144,2145],{},"database.rules.json",[33,2147,2148],{},"storage.rules",[217,2150,2151,2154],{},[17,2152,2153],{},"Test with the emulator",": run the full test suite against the local emulator before every deployment",[217,2156,2157,2162],{},[17,2158,1582,2159],{},[33,2160,2161],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[217,2164,2165,2168],{},[17,2166,2167],{},"Version control your rules files",": commit them to Git and review changes in pull requests",[217,2170,2171,2174],{},[17,2172,2173],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[25,2176,2178],{"id":2177},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,2180,2181],{},"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,2183,2184],{},"Use Cloud Functions as a second validation layer for:",[14,2186,2187,2190],{},[17,2188,2189],{},"Financial transactions",": verify amounts, check account balances, and enforce rate limits server-side:",[602,2192,2194],{"className":1292,"code":2193,"language":1294,"meta":305,"style":305},"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,2195,2196,2201,2206,2211,2215,2220,2225,2230,2234,2238,2243,2248,2253,2258,2262,2266,2271,2276],{"__ignoreMap":305},[610,2197,2198],{"class":612,"line":613},[610,2199,2200],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[610,2202,2203],{"class":612,"line":306},[610,2204,2205],{},"  \u002F\u002F Verify authentication\n",[610,2207,2208],{"class":612,"line":636},[610,2209,2210],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[610,2212,2213],{"class":612,"line":649},[610,2214,733],{"emptyLinePlaceholder":347},[610,2216,2217],{"class":612,"line":660},[610,2218,2219],{},"  \u002F\u002F Server-side amount validation\n",[610,2221,2222],{"class":612,"line":730},[610,2223,2224],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[610,2226,2227],{"class":612,"line":736},[610,2228,2229],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[610,2231,2232],{"class":612,"line":742},[610,2233,1331],{},[610,2235,2236],{"class":612,"line":748},[610,2237,733],{"emptyLinePlaceholder":347},[610,2239,2240],{"class":612,"line":754},[610,2241,2242],{},"  \u002F\u002F Check user's balance from a trusted source\n",[610,2244,2245],{"class":612,"line":759},[610,2246,2247],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[610,2249,2250],{"class":612,"line":765},[610,2251,2252],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[610,2254,2255],{"class":612,"line":771},[610,2256,2257],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[610,2259,2260],{"class":612,"line":776},[610,2261,1331],{},[610,2263,2264],{"class":612,"line":782},[610,2265,733],{"emptyLinePlaceholder":347},[610,2267,2268],{"class":612,"line":788},[610,2269,2270],{},"  \u002F\u002F Process the transaction with admin privileges\n",[610,2272,2273],{"class":612,"line":794},[610,2274,2275],{},"  \u002F\u002F ...\n",[610,2277,2278],{"class":612,"line":800},[610,2279,1676],{},[14,2281,2282,2285],{},[17,2283,2284],{},"Data aggregation and denormalization",": use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[602,2287,2289],{"className":1292,"code":2288,"language":1294,"meta":305,"style":305},"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,2290,2291,2296,2301,2306,2311,2316,2321,2326,2331],{"__ignoreMap":305},[610,2292,2293],{"class":612,"line":613},[610,2294,2295],{},"exports.onPostCreated = functions.firestore\n",[610,2297,2298],{"class":612,"line":306},[610,2299,2300],{},"  .document('posts\u002F{postId}')\n",[610,2302,2303],{"class":612,"line":636},[610,2304,2305],{},"  .onCreate(async (snap, context) => {\n",[610,2307,2308],{"class":612,"line":649},[610,2309,2310],{},"    \u002F\u002F Increment the author's post count atomically\n",[610,2312,2313],{"class":612,"line":660},[610,2314,2315],{},"    const authorId = snap.data().authorId;\n",[610,2317,2318],{"class":612,"line":730},[610,2319,2320],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[610,2322,2323],{"class":612,"line":736},[610,2324,2325],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[610,2327,2328],{"class":612,"line":742},[610,2329,2330],{},"    });\n",[610,2332,2333],{"class":612,"line":748},[610,2334,2335],{},"  });\n",[14,2337,2338,2341],{},[17,2339,2340],{},"Rate limiting",": prevent abuse by tracking request frequency per user:",[602,2343,2345],{"className":1292,"code":2344,"language":1294,"meta":305,"style":305},"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,2346,2347,2352,2357,2362,2366,2371,2376,2381,2386,2390,2394,2398,2403],{"__ignoreMap":305},[610,2348,2349],{"class":612,"line":613},[610,2350,2351],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[610,2353,2354],{"class":612,"line":306},[610,2355,2356],{},"  const userId = context.auth.uid;\n",[610,2358,2359],{"class":612,"line":636},[610,2360,2361],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[610,2363,2364],{"class":612,"line":649},[610,2365,733],{"emptyLinePlaceholder":347},[610,2367,2368],{"class":612,"line":660},[610,2369,2370],{},"  if (lastSubmission.exists) {\n",[610,2372,2373],{"class":612,"line":730},[610,2374,2375],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[610,2377,2378],{"class":612,"line":736},[610,2379,2380],{},"    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n",[610,2382,2383],{"class":612,"line":742},[610,2384,2385],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[610,2387,2388],{"class":612,"line":748},[610,2389,1326],{},[610,2391,2392],{"class":612,"line":754},[610,2393,1331],{},[610,2395,2396],{"class":612,"line":759},[610,2397,733],{"emptyLinePlaceholder":347},[610,2399,2400],{"class":612,"line":765},[610,2401,2402],{},"  \u002F\u002F Process the submission...\n",[610,2404,2405],{"class":612,"line":771},[610,2406,1676],{},[14,2408,2409],{},"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,2411,2413],{"id":2412},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,2415,2416,2419],{},[41,2417,1218],{"href":2418},"\u002Fservice\u002Fflutter-development\u002F"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[214,2421,2422,2428,2434,2440,2446,2452],{},[217,2423,2424,2427],{},[17,2425,2426],{},"Locked-mode defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[217,2429,2430,2433],{},[17,2431,2432],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[217,2435,2436,2439],{},[17,2437,2438],{},"Comprehensive data validation"," in rules for every writable collection: type checking, field constraints, and immutability guards",[217,2441,2442,2445],{},[17,2443,2444],{},"Automated rules testing"," in CI\u002FCD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[217,2447,2448,2451],{},[17,2449,2450],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[217,2453,2454,2457,2458,2463],{},[17,2455,2456],{},"Security audits"," against the ",[41,2459,2462],{"href":2460,"rel":2461},"https:\u002F\u002Fowasp.org\u002Fwww-project-mobile-top-10\u002F",[45],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,2465,2466,2467,2470,2471,303],{},"App369 builds ",[41,2468,2469],{"href":2418},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[41,2472,2474],{"href":2473},"\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","Flutter security best practices",[25,2476,2478],{"id":2477},"faq","FAQ",[2480,2481,2483],"h3",{"id":2482},"what-happens-if-i-deploy-firebase-with-test-mode-rules","What happens if I deploy Firebase with test mode rules?",[14,2485,2486,2487,2491],{},"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 ",[41,2488,2490],{"href":1208,"rel":2489},[45],"AuditYourApp",", roughly 30% of Firebase apps in production still run with overly permissive rules. Always start from locked mode and add permissions explicitly.",[2480,2493,2495],{"id":2494},"can-firebase-security-rules-replace-server-side-validation","Can Firebase Security Rules replace server-side validation?",[14,2497,2498],{},"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.",[2480,2500,2502],{"id":2501},"how-do-i-implement-admin-only-access-in-firebase","How do I implement admin-only access in Firebase?",[14,2504,2505,2506,2509,2510,2513,2514,2517],{},"Use Firebase Authentication custom claims. Set ",[33,2507,2508],{},"{ admin: true }"," on a user's account via the Admin SDK in a Cloud Function, then reference ",[33,2511,2512],{},"request.auth.token.admin == true"," in your Security Rules. Do not use a Firestore field like ",[33,2515,2516],{},"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.",[2480,2519,2521],{"id":2520},"how-often-should-i-update-firebase-security-rules","How often should I update Firebase Security Rules?",[14,2523,2524,2525,2529],{},"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 ",[41,2526,2528],{"href":1232,"rel":2527},[45],"Firebase Security Rules documentation"," to ensure they follow updated best practices.",[1119,2531,2532],{},"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":305,"searchDepth":306,"depth":306,"links":2534},[2535,2536,2537,2538,2539,2540,2541,2542,2543],{"id":1222,"depth":306,"text":1223},{"id":1269,"depth":306,"text":1270},{"id":1407,"depth":306,"text":1408},{"id":1610,"depth":306,"text":1611},{"id":1768,"depth":306,"text":1769},{"id":1964,"depth":306,"text":1965},{"id":2177,"depth":306,"text":2178},{"id":2412,"depth":306,"text":2413},{"id":2477,"depth":306,"text":2478,"children":2544},[2545,2546,2547,2548],{"id":2482,"depth":636,"text":2483},{"id":2494,"depth":636,"text":2495},{"id":2501,"depth":636,"text":2502},{"id":2520,"depth":636,"text":2521},"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":2552},{"text":2553,"minutes":2554,"time":2555,"words":2556},"11 min read",10.4,624000,2080,"\u002Fblog\u002Ffirebase-security-guide-2026","11 min",[352,2560,2561],"ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":1195,"description":2550},"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","1TNumrK9wXS72BfrOz0BHGHFNffnt8s9BKMefZyutXs",[2569,2571,2574,2577,2580,2583],{"keyword":5,"url":395,"title":5,"relevance":2570},70,{"keyword":2572,"url":2573,"title":2572,"relevance":878},"Bolt App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbolt-app-rescue-2026\u002F",{"keyword":2575,"url":2576,"title":2575,"relevance":866},"Cursor App Rescue: 7 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fcursor-app-rescue-2026\u002F",{"keyword":2578,"url":2579,"title":2578,"relevance":845},"Base44 App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbase44-app-rescue-2026\u002F",{"keyword":2581,"url":2582,"title":2581,"relevance":839},"Replit App Rescue: 7 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Freplit-app-rescue-2026\u002F",{"keyword":2584,"url":2585,"title":2584,"relevance":833},"v0 App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fv0-app-rescue-2026\u002F",1792486032261]