[{"data":1,"prerenderedAt":3527},["ShallowReactive",2],{"\u002Fblog\u002Fsupabase-security-review-before-launch-2026\u002F":3,"\u002Fblog\u002Fsupabase-security-review-before-launch-2026\u002F-related":507,"\u002Fblog\u002Fsupabase-security-review-before-launch-2026\u002F-resources":3513},{"id":4,"title":5,"author":6,"body":10,"category":413,"checker":414,"createdAt":415,"description":416,"disclosure":417,"extension":418,"faq":419,"heroActions":414,"howTo":414,"lastVerified":435,"location":436,"meta":437,"navigation":443,"ogImage":414,"path":444,"primaryKeyword":445,"readingTime":446,"related":447,"seo":452,"sources":453,"stem":496,"tags":497,"takeaways":498,"updatedAt":415,"url":505,"__hash__":506},"blog\u002Fblog\u002Fsupabase-security-review-before-launch-2026.md","Supabase Security Review Before Launch (2026)",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":392},"minimark",[13,17,22,30,33,97,100,104,109,130,144,148,155,160,177,181,190,194,209,213,229,242,246,253,258,262,269,278,282,289,293,296,300,307,310,314,317,353,356,360,376,380],[14,15,16],"p",{},"A Supabase security review before launch answers one question: can a stranger, or a signed-in user, read or change data they shouldn't? Start with the Security Advisor and fix its ERROR-level findings. Then work through the ten checks below, from secret keys and Row Level Security to storage, Edge Functions, email and backups. The checks come from Supabase's own documentation, read on October 21, 2026. The fix order is my own judgement. It applies whether you wrote the app yourself or built it with Lovable, Bolt or v0. I sell a review like this, and I say where that applies.",[18,19,21],"h2",{"id":20},"step-0-run-the-security-advisor","Step 0: run the Security Advisor",[14,23,24,25,29],{},"Supabase ships the Security Advisor: deterministic checks on your database. You can run it from the Security Advisor page in the dashboard, with ",[26,27,28],"code",{},"supabase db advisors"," in the CLI, or through the MCP server or Management API. Supabase notes that some findings may be intentional, so read each one rather than silencing it.",[14,31,32],{},"The findings Supabase rates ERROR are the place to start. They include:",[34,35,36,49],"table",{},[37,38,39],"thead",{},[40,41,42,46],"tr",{},[43,44,45],"th",{},"Lint",[43,47,48],{},"What it means",[50,51,52,63,73,87],"tbody",{},[40,53,54,60],{},[55,56,57],"td",{},[26,58,59],{},"rls_disabled_in_public",[55,61,62],{},"A table in the public schema has Row Level Security turned off",[40,64,65,70],{},[55,66,67],{},[26,68,69],{},"security_definer_view",[55,71,72],{},"A view bypasses Row Level Security",[40,74,75,80],{},[55,76,77],{},[26,78,79],{},"auth_users_exposed",[55,81,82,83,86],{},"User data from ",[26,84,85],{},"auth.users"," is exposed through a view",[40,88,89,94],{},[55,90,91],{},[26,92,93],{},"rls_references_user_metadata",[55,95,96],{},"A policy trusts data that users can edit",[14,98,99],{},"A clean Advisor run is a start, not the finish. It can't tell whether your policies match what your app should allow.",[18,101,103],{"id":102},"the-10-checks","The 10 checks",[105,106,108],"h3",{"id":107},"_1-no-secret-key-in-the-browser","1. No secret key in the browser",[14,110,111,112,115,116,119,120,115,123,126,127,129],{},"Supabase has two kinds of key. Publishable keys (",[26,113,114],{},"sb_publishable_...",", and the legacy ",[26,117,118],{},"anon"," key) are meant for the browser. Secret keys (",[26,121,122],{},"sb_secret_...",[26,124,125],{},"service_role"," key) bypass Row Level Security and must stay on the server. Supabase blocks secret keys used from a browser with a 401, but a secret or ",[26,128,125],{}," key that ships in your front-end code is still exposed and should be treated as leaked.",[14,131,132,136,137,139,140,143],{},[133,134,135],"strong",{},"Check:"," search your front-end code and built JavaScript for ",[26,138,125],{}," and ",[26,141,142],{},"sb_secret_",". Supabase's docs say the legacy keys are deprecated by the end of 2026, so plan to move to the new keys.",[105,145,147],{"id":146},"_2-row-level-security-on-every-exposed-table-with-real-policies","2. Row Level Security on every exposed table, with real policies",[14,149,150,151,154],{},"The publishable key only stays safe if Row Level Security (RLS) protects every table in an exposed schema. With RLS enabled and no policies, nothing is readable through the API with a publishable key. That is safe, but the app shows empty screens. The risky case is a policy that allows more than you meant, such as ",[26,152,153],{},"using (true)"," on a table of user data.",[14,156,157,159],{},[133,158,135],{}," sign in as a second test user and try to read, change and delete the first user's rows, directly through the API, not only through your screens.",[14,161,162,163,168,169,172,173,176],{},"Before a migration reaches the project, the free ",[164,165,167],"a",{"href":166},"\u002Ftools\u002Fsupabase-rls-policy-checker\u002F","Supabase RLS policy checker"," reads the SQL in your browser and flags policies of ",[26,170,171],{},"true",", sign-in-only checks and ",[26,174,175],{},"user_metadata"," roles.",[105,178,180],{"id":179},"_3-policies-that-trust-user_metadata","3. Policies that trust user_metadata",[14,182,183,184,186,187,189],{},"Supabase warns that a policy relying on the ",[26,185,175],{}," claim can create security issues, because signed-in users can modify it. A role or plan stored there can be set by the user. Keep roles in a table users can't write to, and check any policy the Advisor flags with ",[26,188,93],{},".",[105,191,193],{"id":192},"_4-views-and-functions-that-skip-your-rules","4. Views and functions that skip your rules",[14,195,196,197,200,201,204,205,208],{},"Views bypass RLS by default, because they are usually created by the ",[26,198,199],{},"postgres"," user. On Postgres 15 and later, create them with ",[26,202,203],{},"security_invoker = true",". A ",[26,206,207],{},"security definer"," function in an exposed schema can be called over the Data API with its creator's privileges, and Supabase says never to create one there.",[105,210,212],{"id":211},"_5-new-tables-after-october-30-2026","5. New tables after October 30, 2026",[14,214,215,216,219,220,222,223,139,226,228],{},"Supabase is changing how new tables reach the API. It announced the change on April 28, 2026, made it the default for new projects on May 30, 2026, and enforces it on all projects from October 30, 2026. From then on, a new table in ",[26,217,218],{},"public"," isn't exposed until you grant access to ",[26,221,118],{},", ",[26,224,225],{},"authenticated",[26,227,125],{},". Without the grant, the app gets error 42501, \"permission denied for table\". Tables you already have keep their grants. Supabase's recommended order is to grant, enable RLS, then add policies.",[14,230,231,232,236,237,241],{},"My ",[164,233,235],{"href":234},"\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026\u002F","October 30 permission denied guide"," has the SQL, and the free ",[164,238,240],{"href":239},"\u002Ftools\u002Fsupabase-oct-30-checker\u002F","Supabase October 30 grants checker"," checks a migration in your browser.",[105,243,245],{"id":244},"_6-storage-buckets","6. Storage buckets",[14,247,248,249,252],{},"A public bucket skips access control when files are read, so anyone with the URL can open them. Uploads and deletes still need policies on ",[26,250,251],{},"storage.objects",". The Advisor also flags public buckets with broad read policies that let anyone list every file.",[14,254,255,257],{},[133,256,135],{}," confirm that ID documents, invoices and private uploads sit in private buckets, with policies scoped to the owner.",[105,259,261],{"id":260},"_7-edge-functions","7. Edge Functions",[14,263,264,265,268],{},"Edge Functions verify the caller's JWT by default (",[26,266,267],{},"verify_jwt = true","). Supabase advises leaving it on for user-facing functions. Turning it off to make a 401 go away removes the platform's check. After that, the function has to authenticate callers itself.",[14,270,271,273,274,277],{},[133,272,135],{}," list every function with ",[26,275,276],{},"verify_jwt = false"," and confirm each one checks its caller in code.",[105,279,281],{"id":280},"_8-auth-email-and-redirects","8. Auth email and redirects",[14,283,284,285,288],{},"Supabase's built-in email provider sends 2 emails per hour, only to pre-authorized addresses, with no delivery guarantee. Supabase urges setting up custom SMTP for real use. Without it, sign-up confirmations and password resets won't reach real users. For redirects, Supabase recommends setting the exact redirect URL path for your production site, and the ",[26,286,287],{},"redirectTo"," in your code must match the Redirect URLs list. Custom SMTP starts with a limit of 30 messages per hour, which you can raise, so check it against your expected sign-ups.",[105,290,292],{"id":291},"_9-passwords-and-mfa","9. Passwords and MFA",[14,294,295],{},"Leaked password protection is available on the Pro plan and above. Supabase also advises against passwords shorter than 8 characters. Basic multi-factor authentication is available on all plans, and phone-based MFA is not on the Free plan and costs extra on Pro and Team.",[105,297,299],{"id":298},"_10-backups-and-network-access","10. Backups and network access",[14,301,302,303,306],{},"The Free plan has no automatic backups; you export with ",[26,304,305],{},"db dump"," yourself. Pro, from $25 a month, keeps daily backups for 7 days, and Team keeps them for 14 days. Point-in-time recovery is an add-on at $100 a month per 7 days of retention.",[14,308,309],{},"Network restrictions limit which IP addresses can connect to Postgres and its pooler. They don't cover the HTTPS APIs (PostgREST, Storage, Auth), so they don't replace RLS.",[18,311,313],{"id":312},"what-to-fix-first","What to fix first",[14,315,316],{},"If you only have a day before launch, fix things in this order:",[318,319,320,327,330,335,341,347,350],"ol",{},[321,322,323,324,326],"li",{},"Any secret or ",[26,325,125],{}," key in front-end code: treat it as leaked. Create a new secret key, put it in server-only settings, deploy, then disable the exposed key in Settings > API Keys (legacy keys stay active until you disable them). Removing it from the code alone does not help, because old builds and git history still contain it.",[321,328,329],{},"Tables without RLS, and policies that let one user read another's data.",[321,331,332,333,189],{},"Policies that read ",[26,334,175],{},[321,336,337,338,340],{},"Views and ",[26,339,207],{}," functions in exposed schemas.",[321,342,343,344,346],{},"Private files in public storage buckets, and Edge Functions with ",[26,345,276],{}," that don't check their caller.",[321,348,349],{},"Grants on any table created after October 30, 2026, so the app doesn't break.",[321,351,352],{},"Custom SMTP, so sign-up and password-reset emails arrive.",[14,354,355],{},"Password and backup settings come next, before you take payments.",[18,357,359],{"id":358},"built-with-lovable-bolt-or-v0","Built with Lovable, Bolt or v0?",[14,361,362,363,222,367,139,371,375],{},"AI builders create Supabase tables and policies for you, and it's easy to miss what they wrote. The same ten checks apply. The builder-specific details are in the ",[164,364,366],{"href":365},"\u002Fblog\u002Flovable-app-rescue-2026\u002F","Lovable",[164,368,370],{"href":369},"\u002Fblog\u002Fbolt-app-rescue-2026\u002F","Bolt",[164,372,374],{"href":373},"\u002Fblog\u002Fv0-app-rescue-2026\u002F","v0"," checklists.",[18,377,379],{"id":378},"want-it-checked-for-you","Want it checked for you?",[14,381,382,383,387,388,391],{},"The App369 ",[164,384,386],{"href":385},"\u002Ffixed-price\u002Fai-app-rescue\u002F","$450 Launch Check"," reviews your app from read-only access: your repository, a test login, and your migrations folder or a schema export with no user data. That covers the code and database checks above. Settings that exist only in the Supabase dashboard, such as your plan, email provider and backups, can't be seen from that access, so checks 8 to 10 are yours to do from this list. Within 3 business days you get each issue ranked critical, high or later, with where it is and how it was confirmed. You also get a fixed price to fix the worst of them. You can take that report to whoever you like, or fix it yourself. The ",[164,389,390],{"href":385},"AI-built app rescue page"," has the details. I'm one developer, based in Miami, and everything happens by email.",{"title":393,"searchDepth":394,"depth":394,"links":395},"",2,[396,397,410,411,412],{"id":20,"depth":394,"text":21},{"id":102,"depth":394,"text":103,"children":398},[399,401,402,403,404,405,406,407,408,409],{"id":107,"depth":400,"text":108},3,{"id":146,"depth":400,"text":147},{"id":179,"depth":400,"text":180},{"id":192,"depth":400,"text":193},{"id":211,"depth":400,"text":212},{"id":244,"depth":400,"text":245},{"id":260,"depth":400,"text":261},{"id":280,"depth":400,"text":281},{"id":291,"depth":400,"text":292},{"id":298,"depth":400,"text":299},{"id":312,"depth":394,"text":313},{"id":358,"depth":394,"text":359},{"id":378,"depth":394,"text":379},"AI & Technology",null,"2026-10-21T12:00:00.000Z","What to check in a Supabase project before launch: keys, Row Level Security, views, storage, Edge Functions, auth email and backups, with a fix-first order.","App369 publishes this guide and sells a fixed-price Launch Check for AI-built apps, including Supabase apps.","md",[420,423,426,429,432],{"question":421,"answer":422},"What does a Supabase security review check?","Which keys reach the browser, whether every exposed table has Row Level Security with correct policies, views and functions that skip those policies, storage bucket rules, Edge Function authentication, auth settings such as email delivery and redirect URLs, and backups.",{"question":424,"answer":425},"Is the Supabase anon key safe to put in my front end?","Yes, the anon key and the newer publishable key are designed for the browser. They are only safe if Row Level Security protects your tables. The service_role key and the newer secret keys must stay on the server.",{"question":427,"answer":428},"How do I check my Supabase project for security problems?","Open the Security Advisor in the dashboard, or run supabase db advisors in the CLI. Fix the ERROR-level findings first, then test your policies by signing in as a second user and trying to read the first user's rows.",{"question":430,"answer":431},"What happens if I turn off verify_jwt on a Supabase Edge Function?","The platform stops checking the caller's JWT before your code runs, so a 401 goes away but anyone can call the function unless your code authenticates callers itself. Supabase advises leaving verify_jwt on for user-facing functions.",{"question":433,"answer":434},"Do I need a paid Supabase plan to launch?","Not to launch, but check what you give up. The Free plan has no automatic daily backups and no leaked password protection; Pro starts at $25 a month with daily backups kept for 7 days.","2026-10-21","Miami, FL",{"readingTimeStats":438},{"text":439,"minutes":440,"time":441,"words":442},"7 min read",6.465,387900,1293,true,"\u002Fblog\u002Fsupabase-security-review-before-launch-2026","supabase security review","6 min",[448,449,450,451],"supabase-permission-denied-new-tables-october-30-2026","lovable-app-rescue-2026","bolt-app-rescue-2026","firebase-security-guide-2026",{"title":5,"description":416},[454,457,460,463,466,469,472,475,478,481,484,487,490,493],{"title":455,"url":456},"Supabase docs, Production checklist, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdeployment\u002Fgoing-into-prod",{"title":458,"url":459},"Supabase docs, Advisors, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fobservability\u002Fadvisors",{"title":461,"url":462},"Supabase docs, Database linter, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fdatabase-linter",{"title":464,"url":465},"Supabase docs, API keys, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fapi\u002Fapi-keys",{"title":467,"url":468},"Supabase docs, Row Level Security, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Frow-level-security",{"title":470,"url":471},"Supabase changelog, Tables not exposed to Data and GraphQL API automatically, published 2026-04-28","https:\u002F\u002Fsupabase.com\u002Fchangelog\u002F45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically",{"title":473,"url":474},"Supabase docs, Storage access control, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fstorage\u002Fsecurity\u002Faccess-control",{"title":476,"url":477},"Supabase docs, Edge Functions auth, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Ffunctions\u002Fauth",{"title":479,"url":480},"Supabase docs, Auth SMTP, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fauth-smtp",{"title":482,"url":483},"Supabase docs, Redirect URLs, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fredirect-urls",{"title":485,"url":486},"Supabase docs, Password security, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth\u002Fpassword-security",{"title":488,"url":489},"Supabase docs, Network restrictions, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fplatform\u002Fnetwork-restrictions",{"title":491,"url":492},"Supabase docs, Database backups, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fplatform\u002Fbackups",{"title":494,"url":495},"Supabase, pricing, fetched 2026-10-21","https:\u002F\u002Fsupabase.com\u002Fpricing","blog\u002Fsupabase-security-review-before-launch-2026","supabase security review, supabase security checklist, supabase row level security, supabase production checklist, supabase launch, vibe-coded app, ai app rescue",[499,500,501,502,503,504],"Start with the Security Advisor in the Supabase dashboard (or `supabase db advisors` in the CLI). Its ERROR-level findings are the first things to fix.","Secret keys (sb_secret_ or the legacy service_role key) must never reach the browser. Publishable and anon keys are meant to.","Every table in an exposed schema needs Row Level Security with real policies, and policies must not trust user_metadata, which signed-in users can edit.","Views bypass RLS by default; set security_invoker = true on Postgres 15 and later. Security definer functions in an exposed schema can be called over the API.","From October 30, 2026, new tables need explicit grants, or the app gets \"permission denied\".","The built-in email provider sends 2 emails per hour, only to pre-authorized addresses, so set up custom SMTP before launch. The Free plan has no automatic backups.","https:\u002F\u002Fapp369.com\u002Fblog\u002Fsupabase-security-review-before-launch-2026\u002F","1n36MYN8jkqak5gB6X7lblK_1aSXpFEM09qCjegGYU4",[508,1321,1714,2056],{"id":509,"title":510,"author":511,"body":513,"category":413,"checker":414,"createdAt":1253,"description":1254,"disclosure":414,"extension":418,"faq":1255,"heroActions":1271,"howTo":1278,"lastVerified":435,"location":436,"meta":1292,"navigation":443,"ogImage":414,"path":1297,"primaryKeyword":1298,"readingTime":1299,"related":414,"seo":1300,"sources":1301,"stem":1311,"tags":1312,"takeaways":1313,"updatedAt":415,"url":1319,"__hash__":1320},"blog\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026.md","Supabase \"Permission Denied\" on New Tables: the October 30 Fix",{"name":7,"social":512},{"twitter":9},{"type":11,"value":514,"toc":1245},[515,535,541,545,554,607,656,659,696,700,711,773,791,795,804,994,997,1061,1065,1095,1102,1106,1185,1192,1200,1204,1230,1241],[14,516,517,520,521,523,524,222,526,139,528,530,531,534],{},[133,518,519],{},"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 ",[26,522,218],{}," schema to the ",[26,525,118],{},[26,527,225],{},[26,529,125],{}," roles automatically. It now leaves that to you. Tables you already have keep working; a table created after the change returns ",[26,532,533],{},"permission denied for table"," until its migration includes the grants.",[14,536,537,538,540],{},"To see which tables in your migrations are affected, paste them into the free ",[164,539,240],{"href":239},". It runs in your browser and never uploads your SQL.",[18,542,544],{"id":543},"what-changes-and-when","What changes, and when",[14,546,547,548,553],{},"Supabase announced the change in its ",[164,549,552],{"href":471,"rel":550},[551],"nofollow","changelog on April 28, 2026",":",[34,555,556,566],{},[37,557,558],{},[40,559,560,563],{},[43,561,562],{},"Date",[43,564,565],{},"What happens",[50,567,568,576,587,595],{},[40,569,570,573],{},[55,571,572],{},"April 28, 2026",[55,574,575],{},"New projects can opt out of exposing new tables when they are created",[40,577,578,581],{},[55,579,580],{},"May 18, 2026",[55,582,583,586],{},[26,584,585],{},"pg_graphql"," is no longer enabled by default",[40,588,589,592],{},[55,590,591],{},"May 30, 2026",[55,593,594],{},"New projects stop exposing new tables by default (rolled out over a few weeks)",[40,596,597,602],{},[55,598,599],{},[133,600,601],{},"October 30, 2026",[55,603,604],{},[133,605,606],{},"The same default applies to all existing projects",[14,608,609,610,614,615,617,618,621,622,222,624,139,626,628,629,632,633,636,637,640,641,643,644,647,648,651,652,655],{},"What changes is Postgres's ",[611,612,613],"em",{},"default privileges"," in the ",[26,616,218],{}," schema. Before, every new table was granted ",[26,619,620],{},"select, insert, update, delete"," to ",[26,623,118],{},[26,625,225],{},[26,627,125],{},", and new sequences got ",[26,630,631],{},"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 ",[26,634,635],{},"EXECUTE"," by default, so grant ",[26,638,639],{},"execute"," explicitly on any function you call through the Data API. Watch the other direction too: Postgres grants ",[26,642,639],{}," on new functions to ",[26,645,646],{},"PUBLIC"," through a global default, and a revoke limited to one schema, including the opt-in SQL below, doesn't remove it. New functions stay callable by every role until you revoke that default, which the same guide explains. (The local CLI revokes the defaults for new functions as well; see below.) Without those defaults, the Data API (REST at ",[26,649,650],{},"\u002Frest\u002Fv1"," and GraphQL at ",[26,653,654],{},"\u002Fgraphql\u002Fv1",") can't see a new table until a role is granted on it.",[14,657,658],{},"What doesn't change:",[660,661,662,668,674,690],"ul",{},[321,663,664,667],{},[133,665,666],{},"Existing tables"," keep the grants they already have.",[321,669,670,673],{},[133,671,672],{},"Row-level security"," works exactly as before.",[321,675,676,679,680,222,683,139,686,689],{},[133,677,678],{},"Other schemas",", such as ",[26,681,682],{},"auth",[26,684,685],{},"storage",[26,687,688],{},"realtime",", aren't affected.",[321,691,692,695],{},[133,693,694],{},"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.",[18,697,699],{"id":698},"the-symptom","The symptom",[14,701,702,703,707,708,553],{},"A request to a new table without grants fails with Postgres error 42501. This is the response ",[164,704,706],{"href":471,"rel":705},[551],"Supabase's changelog"," shows, with its example table renamed to ",[26,709,710],{},"projects",[712,713,717],"pre",{"className":714,"code":715,"language":716,"meta":393,"style":393},"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",[26,718,719,728,744,756,767],{"__ignoreMap":393},[720,721,724],"span",{"class":722,"line":723},"line",1,[720,725,727],{"class":726},"sVt8B","{\n",[720,729,730,734,737,741],{"class":722,"line":394},[720,731,733],{"class":732},"sj4cs","  \"code\"",[720,735,736],{"class":726},": ",[720,738,740],{"class":739},"sZZnC","\"42501\"",[720,742,743],{"class":726},",\n",[720,745,746,749,751,754],{"class":722,"line":400},[720,747,748],{"class":732},"  \"message\"",[720,750,736],{"class":726},[720,752,753],{"class":739},"\"permission denied for table projects\"",[720,755,743],{"class":726},[720,757,759,762,764],{"class":722,"line":758},4,[720,760,761],{"class":732},"  \"hint\"",[720,763,736],{"class":726},[720,765,766],{"class":739},"\"Grant the required privileges to the current role with: GRANT SELECT ON public.projects TO anon;\"\n",[720,768,770],{"class":722,"line":769},5,[720,771,772],{"class":726},"}\n",[14,774,775,776,779,780,783,784,787,788,790],{},"In supabase-js, the ",[26,777,778],{},"error"," object comes back filled and ",[26,781,782],{},"data"," is ",[26,785,786],{},"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 ",[26,789,199],{},", the role that owns the table and has every privilege, and fails in the app. That is why this one can be confusing.",[18,792,794],{"id":793},"the-fix-grant-in-the-same-migration","The fix: grant in the same migration",[14,796,797,798,803],{},"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 ",[164,799,802],{"href":800,"rel":801},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fapi\u002Fsecuring-your-api",[551],"Securing your API"," guide:",[712,805,809],{"className":806,"code":807,"language":808,"meta":393,"style":393},"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",[26,810,811,816,821,826,831,836,842,848,854,860,865,871,877,882,888,894,900,906,911,917,923,928,934,939,945,951,956,962,967,972,978,984,989],{"__ignoreMap":393},[720,812,813],{"class":722,"line":723},[720,814,815],{},"create table public.projects (\n",[720,817,818],{"class":722,"line":394},[720,819,820],{},"  id uuid primary key default gen_random_uuid(),\n",[720,822,823],{"class":722,"line":400},[720,824,825],{},"  user_id uuid not null references auth.users on delete cascade,\n",[720,827,828],{"class":722,"line":758},[720,829,830],{},"  name text not null\n",[720,832,833],{"class":722,"line":769},[720,834,835],{},");\n",[720,837,839],{"class":722,"line":838},6,[720,840,841],{"emptyLinePlaceholder":443},"\n",[720,843,845],{"class":722,"line":844},7,[720,846,847],{},"-- 1. Who can reach the table at all\n",[720,849,851],{"class":722,"line":850},8,[720,852,853],{},"grant select, insert, update, delete on table public.projects to authenticated;\n",[720,855,857],{"class":722,"line":856},9,[720,858,859],{},"grant select, insert, update, delete on table public.projects to service_role;\n",[720,861,863],{"class":722,"line":862},10,[720,864,841],{"emptyLinePlaceholder":443},[720,866,868],{"class":722,"line":867},11,[720,869,870],{},"-- 2. Which rows they can see and change\n",[720,872,874],{"class":722,"line":873},12,[720,875,876],{},"alter table public.projects enable row level security;\n",[720,878,880],{"class":722,"line":879},13,[720,881,841],{"emptyLinePlaceholder":443},[720,883,885],{"class":722,"line":884},14,[720,886,887],{},"create policy \"Owners can read their projects\"\n",[720,889,891],{"class":722,"line":890},15,[720,892,893],{},"  on public.projects for select\n",[720,895,897],{"class":722,"line":896},16,[720,898,899],{},"  to authenticated\n",[720,901,903],{"class":722,"line":902},17,[720,904,905],{},"  using ((select auth.uid()) = user_id);\n",[720,907,909],{"class":722,"line":908},18,[720,910,841],{"emptyLinePlaceholder":443},[720,912,914],{"class":722,"line":913},19,[720,915,916],{},"create policy \"Owners can add projects\"\n",[720,918,920],{"class":722,"line":919},20,[720,921,922],{},"  on public.projects for insert\n",[720,924,926],{"class":722,"line":925},21,[720,927,899],{},[720,929,931],{"class":722,"line":930},22,[720,932,933],{},"  with check ((select auth.uid()) = user_id);\n",[720,935,937],{"class":722,"line":936},23,[720,938,841],{"emptyLinePlaceholder":443},[720,940,942],{"class":722,"line":941},24,[720,943,944],{},"create policy \"Owners can update their projects\"\n",[720,946,948],{"class":722,"line":947},25,[720,949,950],{},"  on public.projects for update\n",[720,952,954],{"class":722,"line":953},26,[720,955,899],{},[720,957,959],{"class":722,"line":958},27,[720,960,961],{},"  using ((select auth.uid()) = user_id)\n",[720,963,965],{"class":722,"line":964},28,[720,966,933],{},[720,968,970],{"class":722,"line":969},29,[720,971,841],{"emptyLinePlaceholder":443},[720,973,975],{"class":722,"line":974},30,[720,976,977],{},"create policy \"Owners can delete their projects\"\n",[720,979,981],{"class":722,"line":980},31,[720,982,983],{},"  on public.projects for delete\n",[720,985,987],{"class":722,"line":986},32,[720,988,899],{},[720,990,992],{"class":722,"line":991},33,[720,993,905],{},[14,995,996],{},"A few details are easy to miss:",[660,998,999,1012,1031,1052],{},[321,1000,1001,1004,1005,1007,1008,1011],{},[133,1002,1003],{},"Grant only what each role needs."," A table that signed-out visitors should never touch gets no ",[26,1006,118],{}," grant at all. Public read-only data gets ",[26,1009,1010],{},"grant select ... to anon"," and nothing more.",[321,1013,1014,1020,1021,1023,1024,1027,1028,189],{},[133,1015,1016,1019],{},[26,1017,1018],{},"serial"," columns need a sequence grant."," If the table uses ",[26,1022,1018],{}," or ",[26,1025,1026],{},"bigserial"," rather than an identity column or a UUID, inserts also need ",[26,1029,1030],{},"grant usage, select on sequence public.projects_id_seq to authenticated;",[321,1032,1033,1038,1039,1042,1043,204,1046,1048,1049,1051],{},[133,1034,1035,1036,189],{},"Functions called over the API need ",[26,1037,639],{}," For a function your app calls with ",[26,1040,1041],{},"rpc()",", use ",[26,1044,1045],{},"grant execute on function public.your_function() to authenticated;",[26,1047,207],{}," function runs with its owner's rights and bypasses RLS, so grant ",[26,1050,639],{}," only to roles that should call it, and review those functions.",[321,1053,1054,1057,1058,1060],{},[133,1055,1056],{},"Grants are not a replacement for RLS."," With a grant and RLS off, any signed-in user, or anyone at all if ",[26,1059,118],{}," is granted, can read every row. Turn RLS on for every exposed table.",[18,1062,1064],{"id":1063},"local-development-already-works-this-way","Local development already works this way",[14,1066,1067,1068,1073,1074,139,1077,1080,1081,1083,1084,1087,1088,1091,1092,189],{},"If local tables work but the hosted project doesn't (or the other way round), check your Supabase CLI version. Since ",[164,1069,1072],{"href":1070,"rel":1071},"https:\u002F\u002Fgithub.com\u002Fsupabase\u002Fcli\u002Freleases\u002Ftag\u002Fv2.106.0",[551],"Supabase CLI v2.106.0"," (June 11, 2026), ",[26,1075,1076],{},"supabase start",[26,1078,1079],{},"supabase db reset"," revoke the default Data API privileges for new tables, sequences and functions in ",[26,1082,218],{},", because ",[26,1085,1086],{},"[api].auto_expose_new_tables"," in ",[26,1089,1090],{},"config.toml"," now defaults to ",[26,1093,1094],{},"false",[14,1096,1097,1098,1101],{},"So a migration without grants fails locally with the same 42501 error. That's useful: it shows the problem before you deploy. Setting ",[26,1099,1100],{},"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.",[18,1103,1105],{"id":1104},"check-your-project-before-october-30","Check your project before October 30",[318,1107,1108,1129,1138,1146],{},[321,1109,1110,1113,1114,1117,1118,1121,1122,1124,1125,1128],{},[133,1111,1112],{},"List your newest migrations"," (",[26,1115,1116],{},"supabase\u002Fmigrations\u002F"," in the repo, or the SQL your AI tool generated) and look for ",[26,1119,1120],{},"create table"," statements in ",[26,1123,218],{}," without matching ",[26,1126,1127],{},"grant"," lines.",[321,1130,1131,1137],{},[133,1132,1133,1134,189],{},"Run them through the ",[164,1135,1136],{"href":239},"grants checker"," It lists each table that would need a grant, each table with RLS off, and each policy that lets anyone in.",[321,1139,1140,1145],{},[133,1141,1142,1143,189],{},"Test as a real user, not as ",[26,1144,199],{}," 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.",[321,1147,1148,1151,1152,1156,1157,1181,1184],{},[133,1149,1150],{},"Opt in early if you can."," Supabase lets existing projects switch to the new default before October 30. The ",[164,1153,1155],{"href":471,"rel":1154},[551],"changelog"," gives the SQL to run in the SQL editor; it changes only tables and sequences created afterwards:",[712,1158,1160],{"className":806,"code":1159,"language":808,"meta":393,"style":393},"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",[26,1161,1162,1167,1172,1176],{"__ignoreMap":393},[720,1163,1164],{"class":722,"line":723},[720,1165,1166],{},"alter default privileges for role postgres in schema public\n",[720,1168,1169],{"class":722,"line":394},[720,1170,1171],{},"  revoke select, insert, update, delete on tables from anon, authenticated, service_role;\n",[720,1173,1174],{"class":722,"line":400},[720,1175,1166],{},[720,1177,1178],{"class":722,"line":758},[720,1179,1180],{},"  revoke usage, select on sequences from anon, authenticated, service_role;\n",[1182,1183],"br",{},"Doing it on a staging project first shows you any missing grants on your schedule instead of the deadline's.",[14,1186,1187,1188,189],{},"For every other dated Apple, Google Play and Firebase requirement in 2026 and 2027, see ",[164,1189,1191],{"href":1190},"\u002Fblog\u002Fapp-store-google-play-deadlines-2026-2027\u002F","App Store and Google Play deadlines for 2026 and 2027",[14,1193,1194,1195,1199],{},"Grants are one item on a longer list. The ",[164,1196,1198],{"href":1197},"\u002Fblog\u002Fsupabase-security-review-before-launch-2026\u002F","Supabase security review before launch"," covers the rest: keys, RLS policies, views and functions, storage, Edge Functions, auth email and backups.",[18,1201,1203],{"id":1202},"if-you-use-lovable-bolt-v0-or-another-ai-builder","If you use Lovable, Bolt, v0 or another AI builder",[14,1205,1206,1207,1087,1209,1211,1212,1214,1215,1218,1219,1222,1223,1226,1227,189],{},"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 ",[26,1208,1120],{},[26,1210,218],{}," should be followed by a ",[26,1213,1127],{}," for each role that needs it and by ",[26,1216,1217],{},"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 ",[164,1220,1221],{"href":365},"seven problems to check before launch",", Bolt has ",[164,1224,1225],{"href":369},"its own pre-launch checklist",", and so do ",[164,1228,1229],{"href":373},"v0 apps that use Supabase",[14,1231,1232,1233,1236,1237,189],{},"If your AI-built app has more than a couple of these problems, or users are already seeing empty screens, App369's ",[164,1234,1235],{"href":385},"AI-Built App Rescue"," starts with a $450 Launch Check. Within 3 business days of read-only access, you get each issue it finds in sign-in, data access, payments and deploys, ranked, with where it is, plus a fixed price to fix the worst of them. The $450 is credited to that fix. If you're weighing other options, see ",[164,1238,1240],{"href":1239},"\u002Fblog\u002Fhire-developer-finish-vibe-coded-app-2026\u002F","who to hire to finish a vibe-coded app",[1242,1243,1244],"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":393,"searchDepth":394,"depth":394,"links":1246},[1247,1248,1249,1250,1251,1252],{"id":543,"depth":394,"text":544},{"id":698,"depth":394,"text":699},{"id":793,"depth":394,"text":794},{"id":1063,"depth":394,"text":1064},{"id":1104,"depth":394,"text":1105},{"id":1202,"depth":394,"text":1203},"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.",[1256,1259,1262,1265,1268],{"question":1257,"answer":1258},"Will my app break on October 30?","Not on the day itself. Supabase says existing tables keep their current grants, so what works today keeps working. The risk is the next table you create: without a grant, every request to it from supabase-js, REST or GraphQL fails with \"permission denied for table\" (error 42501). Add the grants in the migration that creates the table.",{"question":1260,"answer":1261},"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":1263,"answer":1264},"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":1266,"answer":1267},"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":1269,"answer":1270},"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.",[1272,1275],{"label":1273,"to":1274},"Check my migrations free","\u002Ftools\u002Fsupabase-oct-30-checker\u002F#check",{"label":1276,"to":1277},"Scan my live app","\u002Ftools\u002Fsupabase-oct-30-checker\u002F#leak-scan",{"name":1279,"steps":1280},"Check your Supabase project before October 30",[1281,1283,1286,1289],{"name":1112,"text":1282},"List your newest migrations and look for create table statements in public without matching grant lines.",{"name":1284,"text":1285},"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":1287,"text":1288},"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":1290,"text":1291},"Opt in early if you can","Supabase lets existing projects switch to the new default before October 30.",{"readingTimeStats":1293},{"text":439,"minutes":1294,"time":1295,"words":1296},6.445,386700,1289,"\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026","supabase permission denied for table","7 min",{"title":510,"description":1254},[1302,1304,1306,1308],{"title":1303,"url":1070},"Supabase CLI v2.106.0 release notes (June 11, 2026)",{"title":1305,"url":471},"Supabase changelog: tables not exposed to Data and GraphQL API automatically (April 28, 2026)",{"title":1307,"url":800},"Supabase docs: Securing your API",{"title":1309,"url":1310},"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",[1314,1315,1316,1317,1318],"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.","https:\u002F\u002Fapp369.com\u002Fblog\u002Fsupabase-permission-denied-new-tables-october-30-2026\u002F","0-dCvlQg_aW1dkqJUtkHH81l2N9b1uxFuGID-CyXTIg",{"id":1322,"title":1323,"author":1324,"body":1326,"category":413,"checker":1643,"createdAt":1644,"description":1645,"disclosure":414,"extension":418,"faq":1646,"heroActions":414,"howTo":414,"lastVerified":1662,"location":436,"meta":1663,"navigation":443,"ogImage":414,"path":1669,"primaryKeyword":1670,"readingTime":1671,"related":1672,"seo":1673,"sources":1674,"stem":1703,"tags":1704,"takeaways":1705,"updatedAt":1711,"url":1712,"__hash__":1713},"blog\u002Fblog\u002Flovable-app-rescue-2026.md","Lovable App Rescue: 7 Problems to Fix Before Launch",{"name":7,"social":1325},{"twitter":9},{"type":11,"value":1327,"toc":1631},[1328,1334,1337,1341,1348,1363,1369,1379,1383,1386,1391,1396,1400,1406,1419,1424,1428,1434,1445,1450,1454,1457,1462,1478,1482,1485,1488,1500,1505,1509,1512,1532,1535,1540,1544,1561,1565,1590,1594,1621],[14,1329,1330,1333],{},[133,1331,1332],{},"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,1335,1336],{},"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.",[18,1338,1340],{"id":1339},"_1-tables-anyone-can-read-or-change","1. Tables anyone can read or change",[14,1342,1343,1344,1347],{},"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 ",[26,1345,1346],{},"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,1349,1350,1351,1356,1357,1362],{},"This is not a hypothetical. ",[164,1352,1355],{"href":1353,"rel":1354},"https:\u002F\u002Fnvd.nist.gov\u002Fvuln\u002Fdetail\u002FCVE-2025-48757",[551],"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 ",[164,1358,1361],{"href":1359,"rel":1360},"https:\u002F\u002Fmattpalmer.io\u002Fposts\u002Fstatement-on-CVE-2025-48757\u002F",[551],"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,1364,1365,1368],{},[133,1366,1367],{},"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,1370,1371,1374,1375,1378],{},[133,1372,1373],{},"How to fix:"," turn on RLS for every table in the public schema and write policies that tie rows to ",[26,1376,1377],{},"auth.uid()",". Remove any policy that lets everyone in.",[18,1380,1382],{"id":1381},"_2-a-green-scan-with-the-wrong-policies","2. A green scan with the wrong policies",[14,1384,1385],{},"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,1387,1388,1390],{},[133,1389,1367],{}," 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,1392,1393,1395],{},[133,1394,1373],{}," 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.",[18,1397,1399],{"id":1398},"_3-secret-keys-in-the-browser","3. Secret keys in the browser",[14,1401,1402,1403,1405],{},"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 ",[26,1404,125],{}," 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,1407,1408,1410,1411,222,1413,222,1415,1418],{},[133,1409,1367],{}," search your GitHub repository and the built JavaScript for ",[26,1412,125],{},[26,1414,142],{},[26,1416,1417],{},"sk_live"," and your providers' key prefixes.",[14,1420,1421,1423],{},[133,1422,1373],{}," 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.",[18,1425,1427],{"id":1426},"_4-server-functions-that-dont-check-who-is-calling","4. Server functions that don't check who is calling",[14,1429,1430,1431,1433],{},"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 ",[26,1432,267],{},", so \"the platform validates the JWT before your handler runs\", and it warns against turning that off just to clear an error.",[14,1435,1436,1438,1439,1087,1441,1444],{},[133,1437,1367],{}," call each function with no token, then with a normal user's token for an action only an admin should take. Look for ",[26,1440,276],{},[26,1442,1443],{},"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,1446,1447,1449],{},[133,1448,1373],{}," 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.",[18,1451,1453],{"id":1452},"_5-views-and-storage-that-skip-your-rules","5. Views and storage that skip your rules",[14,1455,1456],{},"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,1458,1459,1461],{},[133,1460,1367],{}," in the Security Advisor, look for \"security definer view\" and \"auth users exposed\". In Storage, list which buckets are public.",[14,1463,1464,1466,1467,1470,1471,1474,1475,1477],{},[133,1465,1373],{}," recreate views with ",[26,1468,1469],{},"security_invoker = on",", copy the user fields you need into a ",[26,1472,1473],{},"profiles"," table instead of exposing ",[26,1476,85],{},", and make buckets with private files private, with storage policies.",[18,1479,1481],{"id":1480},"_6-the-supabase-october-30-2026-change","6. The Supabase October 30, 2026 change",[14,1483,1484],{},"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,1486,1487],{},"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,1489,1490,1492,1493,1495,1496,1499],{},[133,1491,1367],{}," paste your migrations into the free ",[164,1494,240],{"href":239},". The ",[164,1497,1498],{"href":234},"full guide to the change"," has the exact grant lines.",[14,1501,1502,1504],{},[133,1503,1373],{}," add a grant per role in the same migration that creates each table, next to RLS and its policies.",[18,1506,1508],{"id":1507},"_7-no-safe-path-between-the-preview-and-production","7. No safe path between the preview and production",[14,1510,1511],{},"Three facts from Lovable's docs catch teams out once real users arrive:",[660,1513,1514,1520,1526],{},[321,1515,1516,1519],{},[133,1517,1518],{},"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.",[321,1521,1522,1525],{},[133,1523,1524],{},"The live site is a snapshot."," It only changes when you publish again, so the preview and the live app can drift apart.",[321,1527,1528,1531],{},[133,1529,1530],{},"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,1533,1534],{},"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,1536,1537,1539],{},[133,1538,1373],{}," 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.",[18,1541,1543],{"id":1542},"what-to-do-this-week","What to do this week",[318,1545,1546,1549,1552,1555,1558],{},[321,1547,1548],{},"Run the free leak scan below against your live site.",[321,1550,1551],{},"Do the two-user test on every page with private data.",[321,1553,1554],{},"Search your repository for secret keys and rotate anything you find.",[321,1556,1557],{},"Run Lovable's Deep scan and read every finding.",[321,1559,1560],{},"Check your newest migrations for grants before October 30.",[18,1562,1564],{"id":1563},"after-the-fixes-can-google-and-ai-assistants-read-it","After the fixes: can Google and AI assistants read it?",[14,1566,1567,1568,1573,1574,1579,1580,1584,1585,1589],{},"Check one thing before you announce it: open your live page, view the page source (not the inspector), and search for a sentence from your page. If it isn't there, the text only appears after JavaScript runs. Google renders JavaScript, but ",[164,1569,1572],{"href":1570,"rel":1571},"https:\u002F\u002Fvercel.com\u002Fblog\u002Fthe-rise-of-the-ai-crawler",[551],"Vercel's December 2024 crawler study"," found that OpenAI's, Anthropic's and Perplexity's crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot and PerplexityBot) don't, so those crawlers see a page with no text, and an assistant that relies on them can't quote it. Lovable's ",[164,1575,1578],{"href":1576,"rel":1577},"https:\u002F\u002Fdocs.lovable.dev\u002Fintroduction\u002Ffaq",[551],"FAQ"," says apps created from May 13, 2026 render pages on the server, while older apps render in the browser and get pre-rendered copies served to search engines and AI crawlers. If search matters for this app, the ",[164,1581,1583],{"href":1582},"\u002Fservice\u002Fseo-geo-aeo\u002F","SEO, GEO and AEO page"," explains what makes a site readable for Google and AI assistants, and the ",[164,1586,1588],{"href":1587},"\u002Ffixed-price\u002Fai-search-visibility-audit\u002F","$750 AI Search Visibility Audit"," checks it for you.",[18,1591,1593],{"id":1592},"need-a-hand","Need a hand?",[14,1595,1596,1597,1599,1600,222,1602,222,1606,222,1610,139,1614,1616,1617,189],{},"If you'd rather have someone else check it, the ",[164,1598,1235],{"href":385}," 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 ",[164,1601,370],{"href":369},[164,1603,1605],{"href":1604},"\u002Fblog\u002Freplit-app-rescue-2026\u002F","Replit",[164,1607,1609],{"href":1608},"\u002Fblog\u002Fcursor-app-rescue-2026\u002F","Cursor",[164,1611,1613],{"href":1612},"\u002Fblog\u002Fbase44-app-rescue-2026\u002F","Base44",[164,1615,374],{"href":373}," checklists. Or ",[164,1618,1620],{"href":1619},"\u002Fcontact\u002F","send me a short description",[14,1622,1623,1624,1626,1627,1630],{},"On Supabase? The ",[164,1625,1198],{"href":1197}," covers every Supabase setting to check, with an order for fixing them. Comparing who should do the work? ",[164,1628,1629],{"href":1239},"Hiring someone to finish a vibe-coded app"," compares freelancers, agencies, builder partners and fixed-price checks, with 10 questions to ask before you hire.",{"title":393,"searchDepth":394,"depth":394,"links":1632},[1633,1634,1635,1636,1637,1638,1639,1640,1641,1642],{"id":1339,"depth":394,"text":1340},{"id":1381,"depth":394,"text":1382},{"id":1398,"depth":394,"text":1399},{"id":1426,"depth":394,"text":1427},{"id":1452,"depth":394,"text":1453},{"id":1480,"depth":394,"text":1481},{"id":1507,"depth":394,"text":1508},{"id":1542,"depth":394,"text":1543},{"id":1563,"depth":394,"text":1564},{"id":1592,"depth":394,"text":1593},"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 the Supabase October 30 change.",[1647,1650,1653,1656,1659],{"question":1648,"answer":1649},"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":1651,"answer":1652},"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":1654,"answer":1655},"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":1657,"answer":1658},"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":1660,"answer":1661},"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",{"readingTimeStats":1664},{"text":1665,"minutes":1666,"time":1667,"words":1668},"8 min read",7.905,474300,1581,"\u002Fblog\u002Flovable-app-rescue-2026","lovable app rescue","8 min",[448,451],{"title":1323,"description":1645},[1675,1678,1681,1684,1687,1690,1692,1694,1696,1699,1701],{"title":1676,"url":1677},"Lovable docs, Security","https:\u002F\u002Fdocs.lovable.dev\u002Ffeatures\u002Fsecurity",{"title":1679,"url":1680},"Lovable docs, Supabase integration","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fsupabase",{"title":1682,"url":1683},"Lovable docs, Lovable Cloud","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fcloud",{"title":1685,"url":1686},"Lovable docs, GitHub integration","https:\u002F\u002Fdocs.lovable.dev\u002Fintegrations\u002Fgithub",{"title":1688,"url":1689},"Lovable docs, Best practices","https:\u002F\u002Fdocs.lovable.dev\u002Ftips-tricks\u002Fbest-practice",{"title":1691,"url":1353},"NVD, CVE-2025-48757",{"title":1693,"url":1359},"Matt Palmer, Statement on CVE-2025-48757",{"title":1695,"url":471},"Supabase changelog, tables not exposed to the Data API automatically",{"title":1697,"url":1698},"Supabase docs, Database advisors","https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fdatabase-advisors",{"title":1700,"url":465},"Supabase docs, API keys",{"title":1702,"url":477},"Supabase docs, Edge Functions auth","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",[1706,1707,1708,1709,1710],"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.","2026-10-21T09:00:00.000Z","https:\u002F\u002Fapp369.com\u002Fblog\u002Flovable-app-rescue-2026\u002F","TXRoHM0jLnEFb2yjX-NeIrzhOJzvw5b3NTR3Jgy_2PU",{"id":1715,"title":1716,"author":1717,"body":1719,"category":413,"checker":1643,"createdAt":1974,"description":1975,"disclosure":414,"extension":418,"faq":1976,"heroActions":414,"howTo":414,"lastVerified":1662,"location":436,"meta":1992,"navigation":443,"ogImage":414,"path":1997,"primaryKeyword":1998,"readingTime":1671,"related":1999,"seo":2000,"sources":2001,"stem":2046,"tags":2047,"takeaways":2048,"updatedAt":1711,"url":2054,"__hash__":2055},"blog\u002Fblog\u002Fbolt-app-rescue-2026.md","Bolt App Rescue: 8 Problems to Fix Before Launch",{"name":7,"social":1718},{"twitter":9},{"type":11,"value":1720,"toc":1961},[1721,1731,1734,1738,1741,1746,1751,1767,1771,1774,1791,1796,1800,1807,1812,1817,1821,1824,1829,1834,1838,1841,1846,1851,1855,1858,1863,1868,1872,1875,1880,1885,1889,1896,1901,1906,1908,1925,1927,1937,1939,1955],[14,1722,1723,1726,1727,1730],{},[133,1724,1725],{},"Problems with real Bolt apps often show up in a handful of places Bolt's own documentation already warns about: database access rules, secret keys, sign-in emails, publishing and hosting limits."," Bolt builds your app from a prompt, publishes it on Bolt's own hosting at a ",[26,1728,1729],{},"bolt.host"," address, and stores data in Bolt Database, which you can later claim into your own Supabase account, or in a Supabase project you choose on a paid plan. The free scan at the end of this page works for Supabase projects only.",[14,1732,1733],{},"Below are the eight problems to check before launch, how to test each one, and how to fix it. The facts come from Bolt's help center and Supabase's documentation, and I re-read each source on October 20, 2026. Bolt's public GitHub repository has issues turned off, so this page relies on what Bolt documents, not on complaint counts. The how-to-check and how-to-fix steps are my own advice.",[18,1735,1737],{"id":1736},"_1-database-tables-anyone-can-read-or-change","1. Database tables anyone can read or change",[14,1739,1740],{},"Bolt's database security page puts it plainly: \"A table without RLS can be read and changed by anyone.\" It also warns that when you haven't asked for sign-in, \"Bolt might write policies that allow any visitor to view, add, change, and delete the data in your database.\" Row-level security (RLS) is what decides which rows each visitor can reach.",[14,1742,1743,1745],{},[133,1744,1367],{}," open the database's Security tab, which Bolt says \"uses the same database checks as the Security Advisor in Supabase\", and look for warnings about policies that are always true or tables without RLS. Then test as a logged-out visitor, and as one user trying to reach another user's rows. The free scan at the end of this page works only if your app uses a Supabase project (a supabase.co address and a public key your site ships). If your app is on Bolt Database, use the Security tab and the logged-out test above instead.",[14,1747,1748,1750],{},[133,1749,1373],{}," add real sign-in first, then rewrite each table's policies: read-only for public content, insert-only for forms, owner-only for private data. Turn RLS on for every table and re-run the check.",[14,1752,1753,1756,1757,222,1759,139,1761,1763,1764,1766],{},[133,1754,1755],{},"Supabase's October 30 change."," If your Bolt app uses a Supabase project, from October 30, 2026 new tables in existing projects are no longer granted to the ",[26,1758,118],{},[26,1760,225],{},[26,1762,125],{}," roles automatically, so a table added after that date returns a \"permission denied\" error until you grant it. Paste your migrations into the free ",[164,1765,240],{"href":239}," to see which tables need grants.",[18,1768,1770],{"id":1769},"_2-secret-keys-in-front-end-code","2. Secret keys in front-end code",[14,1772,1773],{},"A Bolt app's front end runs in each visitor's browser, so a key placed there is public. Bolt's security audit checks that private keys and passwords \"aren't stored in your project's code or sent to visitors' browsers\", and its secrets guide says: \"Never store secrets in plain text in your code.\" A leaked Supabase secret key bypasses every access rule. A leaked payment, email or AI key can be used on your bill.",[14,1775,1776,1778,1779,222,1782,222,1784,1786,1787,1790],{},[133,1777,1367],{}," search the code for ",[26,1780,1781],{},"sk_",[26,1783,125],{},[26,1785,142],{}," and your providers' key prefixes, including any ",[26,1788,1789],{},"VITE_"," variables, and look at the JavaScript your live site loads.",[14,1792,1793,1795],{},[133,1794,1373],{}," rotate any key that ever appeared in code, store it in the database's Secrets, and call the provider from a server function instead of the browser.",[18,1797,1799],{"id":1798},"_3-sign-up-and-password-reset-links-that-break-on-the-live-site","3. Sign-up and password-reset links that break on the live site",[14,1801,1802,1803,1806],{},"Bolt's authentication guide says: \"By default, Bolt projects use ",[26,1804,1805],{},"localhost:3000"," as the Site URL. This does not work for live applications.\" Redirects also only work for addresses on the URI Allow List. And for emails to send at all, Bolt says \"you still need to add your own Resend API key\".",[14,1808,1809,1811],{},[133,1810,1367],{}," on the live site, sign up with a fresh address, click the link in the email, and run Forgot password from start to finish.",[14,1813,1814,1816],{},[133,1815,1373],{}," set the Site URL to your live domain, add every domain and reset page to the URI Allow List (again after you attach a custom domain), and add your email provider's key.",[18,1818,1820],{"id":1819},"_4-the-live-site-doesnt-match-the-preview","4. The live site doesn't match the preview",[14,1822,1823],{},"Bolt's publishing guide says \"changes you make to your Bolt project aren't automatically applied to your published site.\" Fixing something in the editor and seeing it work in the preview doesn't mean customers see it.",[14,1825,1826,1828],{},[133,1827,1367],{}," open the live URL in a private window after every change and compare it with the preview. In the Publish menu, look for a pending update.",[14,1830,1831,1833],{},[133,1832,1373],{}," publish an update after each change, then run a short checklist on the live URL: sign-up, sign-in, payment and email.",[18,1835,1837],{"id":1836},"_5-the-site-goes-offline-at-the-hosting-limit-or-wont-publish","5. The site goes offline at the hosting limit, or won't publish",[14,1839,1840],{},"On Bolt's hosting plans, \"Free plan sites stop serving content once they hit their monthly data limits,\" and if you publish several sites, \"they share the same monthly bandwidth and request allowance.\" Separately, publishing fails above a size cap: \"The maximum allowed size is 100MB.\"",[14,1842,1843,1845],{},[133,1844,1367],{}," look at usage and the spending limit in your account settings before a launch, and check how much of your project's size is images and video.",[14,1847,1848,1850],{},[133,1849,1373],{}," move to a paid plan with a spending limit before launch day, and move large media to external storage so the project stays well under the cap.",[18,1852,1854],{"id":1853},"_6-undo-doesnt-undo-your-data","6. Undo doesn't undo your data",[14,1856,1857],{},"Bolt's backup guide says \"Restoring to an earlier project version will not change your current Bolt or Supabase databases.\" After a bad change, the old code can end up running against newer tables. Bolt also warns that connecting a Supabase database to a project that already has a Bolt database \"will replace the connection, which may cause data loss\", and that there is \"no supported way to migrate an existing Bolt database to Supabase\" other than claiming it.",[14,1859,1860,1862],{},[133,1861,1367],{}," confirm the project is on GitHub, confirm which database the live app really uses, and try exporting your tables.",[14,1864,1865,1867],{},[133,1866,1373],{}," export your data and connect GitHub before launch. If you need your own Supabase account for backups and SQL access, claim the Bolt database rather than connecting a new one to a project that holds data.",[18,1869,1871],{"id":1870},"_7-files-anyone-with-the-link-can-open","7. Files anyone with the link can open",[14,1873,1874],{},"Bolt's file storage guide explains that in a public bucket, \"Anyone with the link can see these files, even if they're not signed in to your app,\" and tells you to \"Always confirm a bucket's visibility yourself.\" User uploads such as IDs, invoices and photos belong in private buckets.",[14,1876,1877,1879],{},[133,1878,1367],{}," in File Storage, read whether each bucket is public or private, and open a user's file URL in a private window.",[14,1881,1882,1884],{},[133,1883,1373],{}," make user-upload buckets private, add per-user storage policies, and serve files through signed links. Then test again with two accounts.",[18,1886,1888],{"id":1887},"_8-fix-loops-that-spend-your-budget","8. Fix loops that spend your budget",[14,1890,1891,1892,1895],{},"When an error won't go away, each attempt is another AI edit. Bolt's own advice is to \"Avoid clicking ",[133,1893,1894],{},"Attempt fix"," over and over, hoping for things to eventually work out.\" Repeated attempts change more code, spend tokens, and can leave the app half-fixed.",[14,1897,1898,1900],{},[133,1899,1367],{}," count how many fix attempts in a row went into one error, and whether the files being edited have grown very large.",[14,1902,1903,1905],{},[133,1904,1373],{}," stop, read the real error in the browser console, ask for one change at a time, split large files, or hand the code to a developer with the exact error.",[18,1907,1543],{"id":1542},[318,1909,1910,1913,1916,1919,1922],{},[321,1911,1912],{},"If your app runs on a Supabase project, run the free leak scan below against your live site. If it uses Bolt Database, test the live site as a logged-out visitor.",[321,1914,1915],{},"Fix the Site URL and test sign-up and password reset on the live domain.",[321,1917,1918],{},"Search the code for secret keys and rotate anything you find.",[321,1920,1921],{},"Check every storage bucket's visibility.",[321,1923,1924],{},"Export your data and connect GitHub before launch day.",[18,1926,1564],{"id":1563},[14,1928,1567,1929,1932,1933,1584,1935,1589],{},[164,1930,1572],{"href":1570,"rel":1931},[551]," found that OpenAI's, Anthropic's and Perplexity's crawlers (GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot and PerplexityBot) don't, so those crawlers see a page with no text, and an assistant that relies on them can't quote it. If search matters for this app, the ",[164,1934,1583],{"href":1582},[164,1936,1588],{"href":1587},[18,1938,1593],{"id":1592},[14,1940,1596,1941,1599,1943,222,1945,222,1947,222,1949,139,1951,1616,1953,189],{},[164,1942,1235],{"href":385},[164,1944,366],{"href":365},[164,1946,1605],{"href":1604},[164,1948,1609],{"href":1608},[164,1950,1613],{"href":1612},[164,1952,374],{"href":373},[164,1954,1620],{"href":1619},[14,1956,1623,1957,1626,1959,1630],{},[164,1958,1198],{"href":1197},[164,1960,1629],{"href":1239},{"title":393,"searchDepth":394,"depth":394,"links":1962},[1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973],{"id":1736,"depth":394,"text":1737},{"id":1769,"depth":394,"text":1770},{"id":1798,"depth":394,"text":1799},{"id":1819,"depth":394,"text":1820},{"id":1836,"depth":394,"text":1837},{"id":1853,"depth":394,"text":1854},{"id":1870,"depth":394,"text":1871},{"id":1887,"depth":394,"text":1888},{"id":1542,"depth":394,"text":1543},{"id":1563,"depth":394,"text":1564},{"id":1592,"depth":394,"text":1593},"2026-10-20T10:00:00.000Z","Your Bolt app works in the preview, but real users are coming. The 8 problems to check first, from open tables and broken sign-up links to hosting limits.",[1977,1980,1983,1986,1989],{"question":1978,"answer":1979},"Is my Bolt app secure if the security audit passes?","The audit is a good start, but Bolt itself tells you to confirm some settings by hand, such as whether each storage bucket is public. Test as a logged-out visitor and with two accounts before launch.",{"question":1981,"answer":1982},"Why do my Bolt app's sign-up emails link to localhost?","Bolt projects use localhost:3000 as the default Site URL, which does not work for live apps. Set the Site URL to your live domain and add every domain and reset page to the URI Allow List in the database authentication settings.",{"question":1984,"answer":1985},"Why does my published Bolt site still show the old version?","Publishing is a separate step. Changes in the editor are not applied to the live site until you publish an update from the Publish menu, so always test the live URL after each publish.",{"question":1987,"answer":1988},"Can I move my Bolt app to Supabase or another host?","Bolt says there is no supported way to migrate an existing Bolt database to Supabase, other than claiming the database into your own Supabase account. Bolt warns that connecting a Supabase database to a project that already has a Bolt database may cause data loss, so export first.",{"question":1990,"answer":1991},"Do I own the code Bolt writes?","Yes. Bolt says code you create with Bolt and StackBlitz is your own and can be used for commercial purposes. Connect the project to GitHub so a developer can work on it outside Bolt.",{"readingTimeStats":1993},{"text":1665,"minutes":1994,"time":1995,"words":1996},7.655,459300,1531,"\u002Fblog\u002Fbolt-app-rescue-2026","bolt app rescue",[449,448],{"title":1716,"description":1975},[2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044],{"title":2003,"url":2004},"Bolt docs, Database security","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Fsecurity",{"title":2006,"url":2007},"Bolt docs, Security audit","https:\u002F\u002Fsupport.bolt.new\u002Fbuilding\u002Fsecurity",{"title":2009,"url":2010},"Bolt docs, Database authentication","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Fauthentication",{"title":2012,"url":2013},"Bolt docs, Send emails","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Fsend-emails",{"title":2015,"url":2016},"Bolt docs, Publish","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fhosting\u002Fpublish",{"title":2018,"url":2019},"Bolt docs, Hosting plans","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fhosting\u002Fplans",{"title":2021,"url":2022},"Bolt docs, Publish issues","https:\u002F\u002Fsupport.bolt.new\u002Ftroubleshooting\u002Fpublish-issues",{"title":2024,"url":2025},"Bolt docs, Rollback and backup","https:\u002F\u002Fsupport.bolt.new\u002Fmanaging-projects\u002Frollback-backup",{"title":2027,"url":2028},"Bolt docs, Bolt Database","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase",{"title":2030,"url":2031},"Bolt docs, File storage","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Ffile-storage",{"title":2033,"url":2034},"Bolt docs, Maximizing token efficiency","https:\u002F\u002Fsupport.bolt.new\u002Fprompting\u002Fmaximizing-token-efficiency",{"title":2036,"url":2037},"Bolt docs, Database secrets","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Fsecrets",{"title":2039,"url":2040},"Bolt docs, Advanced database settings","https:\u002F\u002Fsupport.bolt.new\u002Fbolt-cloud\u002Fdatabase\u002Fadvanced",{"title":2042,"url":2043},"Bolt docs, Corporate and commercial","https:\u002F\u002Fsupport.bolt.new\u002Faccount\u002Fcorporate-commercial",{"title":2045,"url":468},"Supabase docs, Row Level Security","blog\u002Fbolt-app-rescue-2026","bolt app rescue, fix bolt app, bolt.new security, bolt database rls, bolt cloud, bolt.new production, ai app rescue, vibe coded app, supabase row level security",[2049,2050,2051,2052,2053],"Bolt's own docs warn that \"a table without RLS can be read and changed by anyone,\" and that if you haven't asked for sign-in, Bolt may write policies that let any visitor view, add, change and delete data.","Sign-up and password-reset emails break on the live site until you change the default Site URL, which Bolt sets to localhost:3000.","Publishing is a manual step: changes in the preview \"aren't automatically applied to your published site.\"","Restoring an earlier version does not change your Bolt or Supabase database, so the old code can end up running against newer data.","Free plan sites stop serving once they hit the monthly hosting limit, and that limit is shared by every site on your account.","https:\u002F\u002Fapp369.com\u002Fblog\u002Fbolt-app-rescue-2026\u002F","3aEWmZ-ibmJwBYPJLHTtZie2qZiFmC1BkagdPBFZ_Sg",{"id":2057,"title":2058,"author":2059,"body":2061,"category":413,"checker":414,"createdAt":3446,"description":3447,"disclosure":414,"extension":418,"faq":3448,"heroActions":414,"howTo":414,"lastVerified":435,"location":436,"meta":3470,"navigation":443,"ogImage":414,"path":3476,"primaryKeyword":3477,"readingTime":3478,"related":3479,"seo":3482,"sources":3483,"stem":3502,"tags":3503,"takeaways":3504,"updatedAt":415,"url":3511,"__hash__":3512},"blog\u002Fblog\u002Ffirebase-security-guide-2026.md","Firebase Security Rules: The Complete Guide for App Developers (2026)",{"name":7,"social":2060},{"twitter":9},{"type":11,"value":2062,"toc":3436},[2063,2075,2088,2092,2095,2109,2112,2132,2135,2139,2155,2158,2209,2212,2218,2256,2259,2277,2281,2290,2293,2427,2430,2492,2495,2499,2508,2511,2564,2567,2619,2626,2629,2653,2657,2660,2663,2789,2792,2869,2872,2876,2889,2895,2900,2922,2931,3042,3045,3087,3091,3094,3112,3118,3207,3213,3263,3269,3362,3365,3368,3372,3378,3422,3433],[14,2064,2065,2068,2069,2074],{},[133,2066,2067],{},"Firebase Security Rules are the primary access control mechanism between your users and your data."," ",[164,2070,2073],{"href":2071,"rel":2072},"https:\u002F\u002Fwww.audityour.app\u002Fguides\u002Ffirebase-security-rules-guide",[551],"AuditYourApp"," says its own scanning data shows roughly 30% of Firebase apps in production have overly permissive rules on at least one collection. When you create a database, Firebase lets you choose production 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,2076,2077,2078,2082,2083,2087],{},"This guide covers the specific rule patterns, validation techniques, and deployment workflows that ",[164,2079,2081],{"href":2080},"\u002Fservice\u002Fmobile-apps\u002F","App369"," uses to secure Firebase backends across production applications. To check rules you already have, paste them into the free ",[164,2084,2086],{"href":2085},"\u002Ftools\u002Ffirebase-rules-checker\u002F","Firebase Security Rules checker",", which runs in your browser.",[18,2089,2091],{"id":2090},"why-firebase-security-rules-matter","Why Firebase Security Rules Matter",[14,2093,2094],{},"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,2096,2097,2098,2103,2104,2108],{},"According to ",[164,2099,2102],{"href":2100,"rel":2101},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002F",[551],"Firebase's official documentation",", Security Rules stand between your data and malicious users, and ",[164,2105,2107],{"href":2100,"rel":2106},[551],"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,2110,2111],{},"Rules apply to three Firebase services independently:",[660,2113,2114,2120,2126],{},[321,2115,2116,2119],{},[133,2117,2118],{},"Cloud Firestore",": document-level read\u002Fwrite control with path-based matching",[321,2121,2122,2125],{},[133,2123,2124],{},"Realtime Database",": node-level access with cascading permissions",[321,2127,2128,2131],{},[133,2129,2130],{},"Cloud Storage",": file-level access based on path, metadata, and authentication state",[14,2133,2134],{},"Each service requires its own rules file. Securing Firestore does not secure Storage, and vice versa.",[18,2136,2138],{"id":2137},"the-test-mode-trap-firebases-biggest-vulnerability","The Test Mode Trap: Firebase's Biggest Vulnerability",[14,2140,2141,2142,139,2145,2148,2149,2154],{},"Firebase's setup wizard offers two starting configurations: ",[133,2143,2144],{},"test mode",[133,2146,2147],{},"production mode",". Test mode grants public read and write access to the entire database until an expiry date. ",[164,2150,2153],{"href":2151,"rel":2152},"https:\u002F\u002Fvibeappscanner.com\u002Fis-firebase-safe",[551],"VibeAppScanner",", which sells a scanner, calls test mode the number one cause of Firebase data breaches.",[14,2156,2157],{},"Test mode rules look like this:",[712,2159,2163],{"className":2160,"code":2161,"language":2162,"meta":393,"style":393},"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      \u002F\u002F Test mode: open to everyone until a date set when the database was created\n      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n    }\n  }\n}\n","javascript",[26,2164,2165,2170,2175,2180,2185,2190,2195,2200,2205],{"__ignoreMap":393},[720,2166,2167],{"class":722,"line":723},[720,2168,2169],{},"rules_version = '2';\n",[720,2171,2172],{"class":722,"line":394},[720,2173,2174],{},"service cloud.firestore {\n",[720,2176,2177],{"class":722,"line":400},[720,2178,2179],{},"  match \u002Fdatabases\u002F{database}\u002Fdocuments {\n",[720,2181,2182],{"class":722,"line":758},[720,2183,2184],{},"    match \u002F{document=**} {\n",[720,2186,2187],{"class":722,"line":769},[720,2188,2189],{},"      \u002F\u002F Test mode: open to everyone until a date set when the database was created\n",[720,2191,2192],{"class":722,"line":838},[720,2193,2194],{},"      allow read, write: if request.time \u003C timestamp.date(2026, 4, 7);\n",[720,2196,2197],{"class":722,"line":844},[720,2198,2199],{},"    }\n",[720,2201,2202],{"class":722,"line":850},[720,2203,2204],{},"  }\n",[720,2206,2207],{"class":722,"line":856},[720,2208,772],{},[14,2210,2211],{},"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,2213,2214,2215,2217],{},"The correct starting point is ",[133,2216,2147],{},", where all client access is denied by default:",[712,2219,2221],{"className":2160,"code":2220,"language":2162,"meta":393,"style":393},"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",[26,2222,2223,2227,2231,2235,2239,2244,2248,2252],{"__ignoreMap":393},[720,2224,2225],{"class":722,"line":723},[720,2226,2169],{},[720,2228,2229],{"class":722,"line":394},[720,2230,2174],{},[720,2232,2233],{"class":722,"line":400},[720,2234,2179],{},[720,2236,2237],{"class":722,"line":758},[720,2238,2184],{},[720,2240,2241],{"class":722,"line":769},[720,2242,2243],{},"      allow read, write: if false;\n",[720,2245,2246],{"class":722,"line":838},[720,2247,2199],{},[720,2249,2250],{"class":722,"line":844},[720,2251,2204],{},[720,2253,2254],{"class":722,"line":850},[720,2255,772],{},[14,2257,2258],{},"From this baseline, add permissions incrementally for each collection, specifying exactly who can perform which operations.",[14,2260,2261,2262,2265,2266,2268,2269,222,2271,222,2273,139,2275,189],{},"AI app builders often leave test-mode rules in place instead of a deny-all baseline like this one. If your Lovable, Bolt or Cursor app has open data, sign-in loops or failing deploys, the ",[164,2263,2264],{"href":385},"AI-built app rescue"," starts with a $450 Launch Check. On Supabase rather than Firebase, the free ",[164,2267,240],{"href":239}," lists the tables that will need explicit grants after that date. Builder-specific checklists: ",[164,2270,366],{"href":365},[164,2272,370],{"href":369},[164,2274,1605],{"href":1604},[164,2276,1609],{"href":1608},[18,2278,2280],{"id":2279},"writing-firestore-security-rules-that-actually-work","Writing Firestore Security Rules That Actually Work",[14,2282,2283,2284,2289],{},"Firestore rules use path matching with wildcard variables to control access at the collection and document level. According to ",[164,2285,2288],{"href":2286,"rel":2287},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Frules-structure",[551],"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,2291,2292],{},"Here is a production-ready pattern for a user-owned data collection:",[712,2294,2296],{"className":2160,"code":2295,"language":2162,"meta":393,"style":393},"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: if request.auth != null\n                    && resource.data.authorId == request.auth.uid\n                    && request.resource.data.authorId == resource.data.authorId;\n      allow 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",[26,2297,2298,2302,2306,2310,2314,2319,2324,2329,2333,2337,2342,2347,2352,2357,2362,2367,2372,2377,2382,2387,2391,2395,2400,2405,2410,2415,2419,2423],{"__ignoreMap":393},[720,2299,2300],{"class":722,"line":723},[720,2301,2169],{},[720,2303,2304],{"class":722,"line":394},[720,2305,2174],{},[720,2307,2308],{"class":722,"line":400},[720,2309,2179],{},[720,2311,2312],{"class":722,"line":758},[720,2313,841],{"emptyLinePlaceholder":443},[720,2315,2316],{"class":722,"line":769},[720,2317,2318],{},"    \u002F\u002F Users can only read and write their own profile\n",[720,2320,2321],{"class":722,"line":838},[720,2322,2323],{},"    match \u002Fusers\u002F{userId} {\n",[720,2325,2326],{"class":722,"line":844},[720,2327,2328],{},"      allow read, write: if request.auth != null && request.auth.uid == userId;\n",[720,2330,2331],{"class":722,"line":850},[720,2332,2199],{},[720,2334,2335],{"class":722,"line":856},[720,2336,841],{"emptyLinePlaceholder":443},[720,2338,2339],{"class":722,"line":862},[720,2340,2341],{},"    \u002F\u002F Posts are publicly readable, but only the author can write\n",[720,2343,2344],{"class":722,"line":867},[720,2345,2346],{},"    match \u002Fposts\u002F{postId} {\n",[720,2348,2349],{"class":722,"line":873},[720,2350,2351],{},"      allow read: if true;\n",[720,2353,2354],{"class":722,"line":879},[720,2355,2356],{},"      allow create: if request.auth != null\n",[720,2358,2359],{"class":722,"line":884},[720,2360,2361],{},"                    && request.resource.data.authorId == request.auth.uid;\n",[720,2363,2364],{"class":722,"line":890},[720,2365,2366],{},"      allow update: if request.auth != null\n",[720,2368,2369],{"class":722,"line":896},[720,2370,2371],{},"                    && resource.data.authorId == request.auth.uid\n",[720,2373,2374],{"class":722,"line":902},[720,2375,2376],{},"                    && request.resource.data.authorId == resource.data.authorId;\n",[720,2378,2379],{"class":722,"line":908},[720,2380,2381],{},"      allow delete: if request.auth != null\n",[720,2383,2384],{"class":722,"line":913},[720,2385,2386],{},"                    && resource.data.authorId == request.auth.uid;\n",[720,2388,2389],{"class":722,"line":919},[720,2390,2199],{},[720,2392,2393],{"class":722,"line":925},[720,2394,841],{"emptyLinePlaceholder":443},[720,2396,2397],{"class":722,"line":930},[720,2398,2399],{},"    \u002F\u002F Admin-only collection\n",[720,2401,2402],{"class":722,"line":936},[720,2403,2404],{},"    match \u002FadminConfig\u002F{docId} {\n",[720,2406,2407],{"class":722,"line":941},[720,2408,2409],{},"      allow read, write: if request.auth != null\n",[720,2411,2412],{"class":722,"line":947},[720,2413,2414],{},"                         && request.auth.token.admin == true;\n",[720,2416,2417],{"class":722,"line":953},[720,2418,2199],{},[720,2420,2421],{"class":722,"line":958},[720,2422,2204],{},[720,2424,2425],{"class":722,"line":964},[720,2426,772],{},[14,2428,2429],{},"Key principles for effective rules:",[660,2431,2432,2456,2465,2478],{},[321,2433,2434,2437,2438,139,2441,2444,2445,222,2448,2451,2452,2455],{},[133,2435,2436],{},"Separate read and write permissions."," Use ",[26,2439,2440],{},"allow read",[26,2442,2443],{},"allow write"," independently. Break write into ",[26,2446,2447],{},"create",[26,2449,2450],{},"update",", and ",[26,2453,2454],{},"delete"," when different logic applies.",[321,2457,2458,2464],{},[133,2459,2460,2461,189],{},"Always check ",[26,2462,2463],{},"request.auth != null"," This confirms the user is authenticated. Without this check, unauthenticated users can access the data.",[321,2466,2467,139,2474,2477],{},[133,2468,2469,2470,2473],{},"Use ",[26,2471,2472],{},"resource.data"," for existing document fields",[26,2475,2476],{},"request.resource.data"," for incoming data in create\u002Fupdate operations.",[321,2479,2480,2483,2484,2487,2488,2491],{},[133,2481,2482],{},"Avoid wildcards at the root level."," The pattern ",[26,2485,2486],{},"match \u002F{document=**}"," applies to every document in the database. Use it only with ",[26,2489,2490],{},"if false"," as a catch-all deny rule.",[14,2493,2494],{},"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.",[18,2496,2498],{"id":2497},"role-based-access-control-with-custom-claims","Role-Based Access Control with Custom Claims",[14,2500,2501,2502,2507],{},"Firebase Authentication custom claims provide the foundation for role-based access control (RBAC). According to ",[164,2503,2506],{"href":2504,"rel":2505},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fauth\u002Fadmin\u002Fcustom-claims",[551],"Firebase's custom claims documentation",", claims are set server-side using the Admin SDK and propagated to the client through the ID token.",[14,2509,2510],{},"Set custom claims from a Cloud Function or admin server:",[712,2512,2514],{"className":2160,"code":2513,"language":2162,"meta":393,"style":393},"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",[26,2515,2516,2521,2525,2530,2535,2539,2544,2549,2554,2559],{"__ignoreMap":393},[720,2517,2518],{"class":722,"line":723},[720,2519,2520],{},"const admin = require('firebase-admin');\n",[720,2522,2523],{"class":722,"line":394},[720,2524,841],{"emptyLinePlaceholder":443},[720,2526,2527],{"class":722,"line":400},[720,2528,2529],{},"\u002F\u002F Assign admin role\n",[720,2531,2532],{"class":722,"line":758},[720,2533,2534],{},"await admin.auth().setCustomUserClaims(userId, { admin: true });\n",[720,2536,2537],{"class":722,"line":769},[720,2538,841],{"emptyLinePlaceholder":443},[720,2540,2541],{"class":722,"line":838},[720,2542,2543],{},"\u002F\u002F Assign editor role with department\n",[720,2545,2546],{"class":722,"line":844},[720,2547,2548],{},"await admin.auth().setCustomUserClaims(userId, {\n",[720,2550,2551],{"class":722,"line":850},[720,2552,2553],{},"  role: 'editor',\n",[720,2555,2556],{"class":722,"line":856},[720,2557,2558],{},"  department: 'marketing'\n",[720,2560,2561],{"class":722,"line":862},[720,2562,2563],{},"});\n",[14,2565,2566],{},"Reference claims in Security Rules:",[712,2568,2570],{"className":2160,"code":2569,"language":2162,"meta":393,"style":393},"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",[26,2571,2572,2577,2582,2587,2591,2596,2601,2605,2610,2615],{"__ignoreMap":393},[720,2573,2574],{"class":722,"line":723},[720,2575,2576],{},"match \u002Fcontent\u002F{docId} {\n",[720,2578,2579],{"class":722,"line":394},[720,2580,2581],{},"  \u002F\u002F Admins have full access\n",[720,2583,2584],{"class":722,"line":400},[720,2585,2586],{},"  allow read, write: if request.auth.token.admin == true;\n",[720,2588,2589],{"class":722,"line":758},[720,2590,841],{"emptyLinePlaceholder":443},[720,2592,2593],{"class":722,"line":769},[720,2594,2595],{},"  \u002F\u002F Editors can read and update, but not delete\n",[720,2597,2598],{"class":722,"line":838},[720,2599,2600],{},"  allow read, update: if request.auth.token.role == 'editor';\n",[720,2602,2603],{"class":722,"line":844},[720,2604,841],{"emptyLinePlaceholder":443},[720,2606,2607],{"class":722,"line":850},[720,2608,2609],{},"  \u002F\u002F All authenticated users can read\n",[720,2611,2612],{"class":722,"line":856},[720,2613,2614],{},"  allow read: if request.auth != null;\n",[720,2616,2617],{"class":722,"line":862},[720,2618,772],{},[14,2620,2621,2622,2625],{},"As ",[164,2623,2506],{"href":2504,"rel":2624},[551]," 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,2627,2628],{},"Implementation guidelines:",[660,2630,2631,2637,2647],{},[321,2632,2633,2636],{},[133,2634,2635],{},"Limit claim size."," Custom claims are included in every authenticated request. Keep them under 1,000 bytes total.",[321,2638,2639,2642,2643,2646],{},[133,2640,2641],{},"Force token refresh after claim changes."," Call ",[26,2644,2645],{},"user.getIdToken(true)"," on the client after updating claims server-side, or the old claims persist until the token auto-refreshes.",[321,2648,2649,2652],{},[133,2650,2651],{},"Use hierarchical roles."," Define roles with clear inheritance (viewer \u003C editor \u003C admin) so rules remain readable as the permission model grows.",[18,2654,2656],{"id":2655},"data-validation-in-security-rules","Data Validation in Security Rules",[14,2658,2659],{},"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,2661,2662],{},"Validation patterns for common requirements:",[712,2664,2666],{"className":2160,"code":2665,"language":2162,"meta":393,"style":393},"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    \u002F\u002F Keep the same shape and types as on create\n    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl'])\n    && request.resource.data.name is string\n    && request.resource.data.name.size() >= 2\n    && request.resource.data.name.size() \u003C= 100;\n}\n",[26,2667,2668,2673,2678,2683,2688,2693,2698,2703,2708,2713,2718,2723,2728,2733,2738,2743,2747,2752,2757,2762,2767,2772,2776,2780,2785],{"__ignoreMap":393},[720,2669,2670],{"class":722,"line":723},[720,2671,2672],{},"match \u002Fusers\u002F{userId} {\n",[720,2674,2675],{"class":722,"line":394},[720,2676,2677],{},"  allow create: if request.auth.uid == userId\n",[720,2679,2680],{"class":722,"line":400},[720,2681,2682],{},"    \u002F\u002F Required fields must exist\n",[720,2684,2685],{"class":722,"line":758},[720,2686,2687],{},"    && request.resource.data.keys().hasAll(['name', 'email', 'createdAt'])\n",[720,2689,2690],{"class":722,"line":769},[720,2691,2692],{},"    \u002F\u002F Name must be a string between 2 and 100 characters\n",[720,2694,2695],{"class":722,"line":838},[720,2696,2697],{},"    && request.resource.data.name is string\n",[720,2699,2700],{"class":722,"line":844},[720,2701,2702],{},"    && request.resource.data.name.size() >= 2\n",[720,2704,2705],{"class":722,"line":850},[720,2706,2707],{},"    && request.resource.data.name.size() \u003C= 100\n",[720,2709,2710],{"class":722,"line":856},[720,2711,2712],{},"    \u002F\u002F Email must be a string and match the auth email\n",[720,2714,2715],{"class":722,"line":862},[720,2716,2717],{},"    && request.resource.data.email is string\n",[720,2719,2720],{"class":722,"line":867},[720,2721,2722],{},"    && request.resource.data.email == request.auth.token.email\n",[720,2724,2725],{"class":722,"line":873},[720,2726,2727],{},"    \u002F\u002F createdAt must be a timestamp\n",[720,2729,2730],{"class":722,"line":879},[720,2731,2732],{},"    && request.resource.data.createdAt is timestamp\n",[720,2734,2735],{"class":722,"line":884},[720,2736,2737],{},"    \u002F\u002F No unexpected fields allowed\n",[720,2739,2740],{"class":722,"line":890},[720,2741,2742],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl']);\n",[720,2744,2745],{"class":722,"line":896},[720,2746,841],{"emptyLinePlaceholder":443},[720,2748,2749],{"class":722,"line":902},[720,2750,2751],{},"  allow update: if request.auth.uid == userId\n",[720,2753,2754],{"class":722,"line":908},[720,2755,2756],{},"    \u002F\u002F Prevent changing the email field\n",[720,2758,2759],{"class":722,"line":913},[720,2760,2761],{},"    && (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['email', 'createdAt']))\n",[720,2763,2764],{"class":722,"line":919},[720,2765,2766],{},"    \u002F\u002F Keep the same shape and types as on create\n",[720,2768,2769],{"class":722,"line":925},[720,2770,2771],{},"    && request.resource.data.keys().hasOnly(['name', 'email', 'createdAt', 'bio', 'avatarUrl'])\n",[720,2773,2774],{"class":722,"line":930},[720,2775,2697],{},[720,2777,2778],{"class":722,"line":936},[720,2779,2702],{},[720,2781,2782],{"class":722,"line":941},[720,2783,2784],{},"    && request.resource.data.name.size() \u003C= 100;\n",[720,2786,2787],{"class":722,"line":947},[720,2788,772],{},[14,2790,2791],{},"Validation capabilities in Firestore rules include:",[660,2793,2794,2822,2832,2845,2854,2860],{},[321,2795,2796,2799,2800,222,2803,222,2806,222,2809,222,2812,222,2815,2818,2819],{},[133,2797,2798],{},"Type checking",": verify that fields are ",[26,2801,2802],{},"string",[26,2804,2805],{},"int",[26,2807,2808],{},"float",[26,2810,2811],{},"bool",[26,2813,2814],{},"timestamp",[26,2816,2817],{},"list",", or ",[26,2820,2821],{},"map",[321,2823,2824,2827,2828,2831],{},[133,2825,2826],{},"String length",": use ",[26,2829,2830],{},".size()"," to enforce minimum and maximum lengths",[321,2833,2834,736,2837,2840,2841,2844],{},[133,2835,2836],{},"Field existence",[26,2838,2839],{},"hasAll()"," for required fields, ",[26,2842,2843],{},"hasOnly()"," to prevent extra fields",[321,2846,2847,2827,2850,2853],{},[133,2848,2849],{},"Field immutability",[26,2851,2852],{},".diff().affectedKeys()"," to prevent modification of specific fields after creation",[321,2855,2856,2859],{},[133,2857,2858],{},"Numeric ranges",": enforce minimum and maximum values for numbers",[321,2861,2862,2865,2866,2868],{},[133,2863,2864],{},"List constraints",": check ",[26,2867,2830],{}," on lists to limit array lengths",[14,2870,2871],{},"Data validation in rules complements client-side validation but must never depend on it. Treat every incoming write as potentially malicious.",[18,2873,2875],{"id":2874},"testing-and-deploying-rules-safely","Testing and Deploying Rules Safely",[14,2877,2878,2879,2884,2885,189],{},"Firebase provides the Rules Playground 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; ",[164,2880,2883],{"href":2881,"rel":2882},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Frules\u002Funit-tests",[551],"Firebase's testing documentation"," shows how to unit test rules with the emulator. For a step-by-step version, see ",[164,2886,2888],{"href":2887},"\u002Fblog\u002Fhow-to-test-firebase-security-rules-2026\u002F","how to test Firebase Security Rules",[14,2890,2891,2892,2894],{},"For a quick first pass, paste your rules into App369's free ",[164,2893,2086],{"href":2085},". It flags public paths, leftover test-mode rules and sign-in-only checks, and runs in your browser.",[14,2896,2897],{},[133,2898,2899],{},"Use the Firebase Emulator Suite for local testing:",[712,2901,2905],{"className":2902,"code":2903,"language":2904,"meta":393,"style":393},"language-bash shiki shiki-themes github-light github-dark","firebase emulators:start --only firestore\n","bash",[26,2906,2907],{"__ignoreMap":393},[720,2908,2909,2913,2916,2919],{"class":722,"line":723},[720,2910,2912],{"class":2911},"sScJk","firebase",[720,2914,2915],{"class":739}," emulators:start",[720,2917,2918],{"class":732}," --only",[720,2920,2921],{"class":739}," firestore\n",[14,2923,2924],{},[133,2925,2926,2927,2930],{},"Write unit tests with the ",[26,2928,2929],{},"@firebase\u002Frules-unit-testing"," package:",[712,2932,2934],{"className":2160,"code":2933,"language":2162,"meta":393,"style":393},"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",[26,2935,2936,2941,2945,2950,2955,2960,2964,2968,2973,2978,2983,2988,2992,2996,3001,3006,3011,3015,3019,3024,3029,3033,3038],{"__ignoreMap":393},[720,2937,2938],{"class":722,"line":723},[720,2939,2940],{},"const { initializeTestEnvironment, assertSucceeds, assertFails } = require('@firebase\u002Frules-unit-testing');\n",[720,2942,2943],{"class":722,"line":394},[720,2944,841],{"emptyLinePlaceholder":443},[720,2946,2947],{"class":722,"line":400},[720,2948,2949],{},"const testEnv = await initializeTestEnvironment({\n",[720,2951,2952],{"class":722,"line":758},[720,2953,2954],{},"  projectId: 'your-project-id',\n",[720,2956,2957],{"class":722,"line":769},[720,2958,2959],{},"  firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }\n",[720,2961,2962],{"class":722,"line":838},[720,2963,2563],{},[720,2965,2966],{"class":722,"line":844},[720,2967,841],{"emptyLinePlaceholder":443},[720,2969,2970],{"class":722,"line":850},[720,2971,2972],{},"\u002F\u002F Test: authenticated user can read their own document\n",[720,2974,2975],{"class":722,"line":856},[720,2976,2977],{},"const userContext = testEnv.authenticatedContext('user123');\n",[720,2979,2980],{"class":722,"line":862},[720,2981,2982],{},"await assertSucceeds(\n",[720,2984,2985],{"class":722,"line":867},[720,2986,2987],{},"  userContext.firestore().collection('users').doc('user123').get()\n",[720,2989,2990],{"class":722,"line":873},[720,2991,835],{},[720,2993,2994],{"class":722,"line":879},[720,2995,841],{"emptyLinePlaceholder":443},[720,2997,2998],{"class":722,"line":884},[720,2999,3000],{},"\u002F\u002F Test: user cannot read another user's document\n",[720,3002,3003],{"class":722,"line":890},[720,3004,3005],{},"await assertFails(\n",[720,3007,3008],{"class":722,"line":896},[720,3009,3010],{},"  userContext.firestore().collection('users').doc('other-user').get()\n",[720,3012,3013],{"class":722,"line":902},[720,3014,835],{},[720,3016,3017],{"class":722,"line":908},[720,3018,841],{"emptyLinePlaceholder":443},[720,3020,3021],{"class":722,"line":913},[720,3022,3023],{},"\u002F\u002F Test: unauthenticated user cannot write\n",[720,3025,3026],{"class":722,"line":919},[720,3027,3028],{},"const unauthedContext = testEnv.unauthenticatedContext();\n",[720,3030,3031],{"class":722,"line":925},[720,3032,3005],{},[720,3034,3035],{"class":722,"line":930},[720,3036,3037],{},"  unauthedContext.firestore().collection('users').doc('user123').set({ name: 'test' })\n",[720,3039,3040],{"class":722,"line":936},[720,3041,835],{},[14,3043,3044],{},"Deployment workflow:",[318,3046,3047,3061,3067,3075,3081],{},[321,3048,3049,1087,3052,222,3055,2451,3058],{},[133,3050,3051],{},"Write rules locally",[26,3053,3054],{},"firestore.rules",[26,3056,3057],{},"database.rules.json",[26,3059,3060],{},"storage.rules",[321,3062,3063,3066],{},[133,3064,3065],{},"Test with the emulator",": run the full test suite against the local emulator before every deployment",[321,3068,3069,3074],{},[133,3070,2469,3071],{},[26,3072,3073],{},"firebase deploy --only firestore:rules"," to deploy rules independently from other Firebase resources",[321,3076,3077,3080],{},[133,3078,3079],{},"Version control your rules files",": commit them to Git and review changes in pull requests",[321,3082,3083,3086],{},[133,3084,3085],{},"Monitor denied requests"," in the Firebase Console to identify legitimate access patterns blocked by overly restrictive rules",[18,3088,3090],{"id":3089},"server-side-validation-with-cloud-functions","Server-Side Validation with Cloud Functions",[14,3092,3093],{},"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,3095,3096,3097,3100,3101,3104,3105,139,3108,3111],{},"Use Cloud Functions as a second validation layer for the cases below. The samples use the 1st gen style, ",[26,3098,3099],{},"functions.https.onCall(async (data, context) => ...)",". In 2nd gen the handler takes a single ",[26,3102,3103],{},"request"," object with ",[26,3106,3107],{},"request.data",[26,3109,3110],{},"request.auth",", so adapt them for new code.",[14,3113,3114,3117],{},[133,3115,3116],{},"Financial transactions",": verify amounts, check account balances, and enforce rate limits server-side:",[712,3119,3121],{"className":2160,"code":3120,"language":2162,"meta":393,"style":393},"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. Read the balance and write the charge in one Firestore transaction, or two parallel requests can both pass the check above.\n  \u002F\u002F ...\n});\n",[26,3122,3123,3128,3133,3138,3142,3147,3152,3157,3161,3165,3170,3175,3180,3185,3189,3193,3198,3203],{"__ignoreMap":393},[720,3124,3125],{"class":722,"line":723},[720,3126,3127],{},"exports.processPayment = functions.https.onCall(async (data, context) => {\n",[720,3129,3130],{"class":722,"line":394},[720,3131,3132],{},"  \u002F\u002F Verify authentication\n",[720,3134,3135],{"class":722,"line":400},[720,3136,3137],{},"  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\n",[720,3139,3140],{"class":722,"line":758},[720,3141,841],{"emptyLinePlaceholder":443},[720,3143,3144],{"class":722,"line":769},[720,3145,3146],{},"  \u002F\u002F Server-side amount validation\n",[720,3148,3149],{"class":722,"line":838},[720,3150,3151],{},"  if (data.amount \u003C= 0 || data.amount > 10000) {\n",[720,3153,3154],{"class":722,"line":844},[720,3155,3156],{},"    throw new functions.https.HttpsError('invalid-argument', 'Invalid amount');\n",[720,3158,3159],{"class":722,"line":850},[720,3160,2204],{},[720,3162,3163],{"class":722,"line":856},[720,3164,841],{"emptyLinePlaceholder":443},[720,3166,3167],{"class":722,"line":862},[720,3168,3169],{},"  \u002F\u002F Check user's balance from a trusted source\n",[720,3171,3172],{"class":722,"line":867},[720,3173,3174],{},"  const userDoc = await admin.firestore().collection('accounts').doc(context.auth.uid).get();\n",[720,3176,3177],{"class":722,"line":873},[720,3178,3179],{},"  if (userDoc.data().balance \u003C data.amount) {\n",[720,3181,3182],{"class":722,"line":879},[720,3183,3184],{},"    throw new functions.https.HttpsError('failed-precondition', 'Insufficient balance');\n",[720,3186,3187],{"class":722,"line":884},[720,3188,2204],{},[720,3190,3191],{"class":722,"line":890},[720,3192,841],{"emptyLinePlaceholder":443},[720,3194,3195],{"class":722,"line":896},[720,3196,3197],{},"  \u002F\u002F Process the transaction with admin privileges. Read the balance and write the charge in one Firestore transaction, or two parallel requests can both pass the check above.\n",[720,3199,3200],{"class":722,"line":902},[720,3201,3202],{},"  \u002F\u002F ...\n",[720,3204,3205],{"class":722,"line":908},[720,3206,2563],{},[14,3208,3209,3212],{},[133,3210,3211],{},"Data aggregation and denormalization",": use Firestore triggers to maintain counts, totals, and derived fields that users should not control directly:",[712,3214,3216],{"className":2160,"code":3215,"language":2162,"meta":393,"style":393},"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",[26,3217,3218,3223,3228,3233,3238,3243,3248,3253,3258],{"__ignoreMap":393},[720,3219,3220],{"class":722,"line":723},[720,3221,3222],{},"exports.onPostCreated = functions.firestore\n",[720,3224,3225],{"class":722,"line":394},[720,3226,3227],{},"  .document('posts\u002F{postId}')\n",[720,3229,3230],{"class":722,"line":400},[720,3231,3232],{},"  .onCreate(async (snap, context) => {\n",[720,3234,3235],{"class":722,"line":758},[720,3236,3237],{},"    \u002F\u002F Increment the author's post count atomically\n",[720,3239,3240],{"class":722,"line":769},[720,3241,3242],{},"    const authorId = snap.data().authorId;\n",[720,3244,3245],{"class":722,"line":838},[720,3246,3247],{},"    await admin.firestore().collection('users').doc(authorId).update({\n",[720,3249,3250],{"class":722,"line":844},[720,3251,3252],{},"      postCount: admin.firestore.FieldValue.increment(1)\n",[720,3254,3255],{"class":722,"line":850},[720,3256,3257],{},"    });\n",[720,3259,3260],{"class":722,"line":856},[720,3261,3262],{},"  });\n",[14,3264,3265,3268],{},[133,3266,3267],{},"Rate limiting",": prevent abuse by tracking request frequency per user:",[712,3270,3272],{"className":2160,"code":3271,"language":2162,"meta":393,"style":393},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');\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 Record the time, then process the submission. Use a transaction if two\n  \u002F\u002F requests could arrive at once.\n  await admin.firestore().collection('rateLimits').doc(userId).set({\n    timestamp: admin.firestore.Timestamp.now()\n  });\n  \u002F\u002F Process the submission...\n});\n",[26,3273,3274,3279,3283,3288,3293,3297,3302,3307,3312,3317,3321,3325,3329,3334,3339,3344,3349,3353,3358],{"__ignoreMap":393},[720,3275,3276],{"class":722,"line":723},[720,3277,3278],{},"exports.submitFeedback = functions.https.onCall(async (data, context) => {\n",[720,3280,3281],{"class":722,"line":394},[720,3282,3137],{},[720,3284,3285],{"class":722,"line":400},[720,3286,3287],{},"  const userId = context.auth.uid;\n",[720,3289,3290],{"class":722,"line":758},[720,3291,3292],{},"  const lastSubmission = await admin.firestore().collection('rateLimits').doc(userId).get();\n",[720,3294,3295],{"class":722,"line":769},[720,3296,841],{"emptyLinePlaceholder":443},[720,3298,3299],{"class":722,"line":838},[720,3300,3301],{},"  if (lastSubmission.exists) {\n",[720,3303,3304],{"class":722,"line":844},[720,3305,3306],{},"    const lastTime = lastSubmission.data().timestamp.toMillis();\n",[720,3308,3309],{"class":722,"line":850},[720,3310,3311],{},"    if (Date.now() - lastTime \u003C 60000) { \u002F\u002F 1 minute cooldown\n",[720,3313,3314],{"class":722,"line":856},[720,3315,3316],{},"      throw new functions.https.HttpsError('resource-exhausted', 'Please wait before submitting again');\n",[720,3318,3319],{"class":722,"line":862},[720,3320,2199],{},[720,3322,3323],{"class":722,"line":867},[720,3324,2204],{},[720,3326,3327],{"class":722,"line":873},[720,3328,841],{"emptyLinePlaceholder":443},[720,3330,3331],{"class":722,"line":879},[720,3332,3333],{},"  \u002F\u002F Record the time, then process the submission. Use a transaction if two\n",[720,3335,3336],{"class":722,"line":884},[720,3337,3338],{},"  \u002F\u002F requests could arrive at once.\n",[720,3340,3341],{"class":722,"line":890},[720,3342,3343],{},"  await admin.firestore().collection('rateLimits').doc(userId).set({\n",[720,3345,3346],{"class":722,"line":896},[720,3347,3348],{},"    timestamp: admin.firestore.Timestamp.now()\n",[720,3350,3351],{"class":722,"line":902},[720,3352,3262],{},[720,3354,3355],{"class":722,"line":908},[720,3356,3357],{},"  \u002F\u002F Process the submission...\n",[720,3359,3360],{"class":722,"line":913},[720,3361,2563],{},[14,3363,3364],{},"Cloud Functions complement Security Rules: they do not replace them. Server client libraries, including the Admin SDK that functions use, bypass Security Rules entirely, so a function that writes with admin privileges has to do its own authorization checks. Rules should still enforce basic authentication and authorization checks. Functions handle the logic that rules cannot express.",[14,3366,3367],{},"Firebase App Check adds a separate layer: once enforced, Cloud Firestore, Realtime Database, Cloud Storage and callable Cloud Functions reject requests that lack a valid attestation token from your real app. Firebase describes App Check and Authentication as complementary and says App Check does not stop all abuse, so it does not replace rules.",[18,3369,3371],{"id":3370},"how-app369-secures-firebase-applications","How App369 Secures Firebase Applications",[14,3373,3374,3377],{},[164,3375,2081],{"href":3376},"\u002Fservice\u002Fflutter-development\u002F"," implements Firebase security as a standard component of every project that uses the Firebase stack:",[660,3379,3380,3386,3392,3398,3404,3410],{},[321,3381,3382,3385],{},[133,3383,3384],{},"Production-mode (deny-all) defaults"," on all new projects, with permissions added per-collection based on documented access requirements",[321,3387,3388,3391],{},[133,3389,3390],{},"Custom claims-based RBAC"," for multi-role applications, with claim management handled exclusively through admin-level Cloud Functions",[321,3393,3394,3397],{},[133,3395,3396],{},"Comprehensive data validation"," in rules for every writable collection: type checking, field constraints, and immutability guards",[321,3399,3400,3403],{},[133,3401,3402],{},"Automated rules testing"," in CI\u002FCD pipelines using the Firebase Emulator Suite, running the full test suite before every deployment",[321,3405,3406,3409],{},[133,3407,3408],{},"Server-side business logic"," via Cloud Functions for financial operations, rate limiting, and any validation that depends on data the client should not access",[321,3411,3412,3415,3416,3421],{},[133,3413,3414],{},"Security audits"," against the ",[164,3417,3420],{"href":3418,"rel":3419},"https:\u002F\u002Fowasp.org\u002Fwww-project-mobile-top-10\u002F",[551],"OWASP Mobile Top 10"," before production release, with specific attention to Firebase configuration",[14,3423,3424,3425,3428,3429,189],{},"App369 builds ",[164,3426,3427],{"href":3376},"Flutter applications"," with Firebase backends that meet enterprise security requirements. For projects with additional security needs, read the companion guide on ",[164,3430,3432],{"href":3431},"\u002Fblog\u002Fflutter-security-best-practices-2026\u002F","Flutter security best practices",[1242,3434,3435],{},"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":393,"searchDepth":394,"depth":394,"links":3437},[3438,3439,3440,3441,3442,3443,3444,3445],{"id":2090,"depth":394,"text":2091},{"id":2137,"depth":394,"text":2138},{"id":2279,"depth":394,"text":2280},{"id":2497,"depth":394,"text":2498},{"id":2655,"depth":394,"text":2656},{"id":2874,"depth":394,"text":2875},{"id":3089,"depth":394,"text":3090},{"id":3370,"depth":394,"text":3371},"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.",[3449,3452,3455,3458,3461,3464,3467],{"question":3450,"answer":3451},"What happens if I deploy Firebase with test mode rules?","Until the expiry date in the rule, anyone on the internet can read, change and delete every document, because your project ID ships inside your app. After the date, every request is denied and the app stops working. Start from production mode, which denies client access, and add permissions per collection.",{"question":3453,"answer":3454},"Can Firebase Security Rules replace server-side validation?","Partially. Rules can enforce authentication, field-level authorization, types, string lengths and immutable fields, and can read a few other documents with get() and exists(). They cannot run queries, call external APIs or rate-limit, so use Cloud Functions for that and keep rules as the first layer for every request.",{"question":3456,"answer":3457},"How do I implement admin-only access in Firebase?","Set a custom claim such as { admin: true } with the Admin SDK from trusted server code, then check request.auth.token.admin == true in your rules. Don't rely on an isAdmin field in a user document that clients may be able to write.",{"question":3459,"answer":3460},"Do Firebase Security Rules apply to the Admin SDK and Cloud Functions?","No. Firebase says the server client libraries bypass all Cloud Firestore Security Rules. A function that writes with admin privileges has to check authorization itself.",{"question":3462,"answer":3463},"How big can Firebase custom claims be?","The claims payload must not exceed 1000 bytes. Keep claims to role identifiers and permission flags, and call getIdToken(true) if the client needs a changed claim before its one-hour ID token refreshes.",{"question":3465,"answer":3466},"Does App Check replace Security Rules?","No. App Check checks that a request comes from your real app, and Authentication identifies the user. Firebase describes them as complementary, and you still need rules to decide who can read or write what.",{"question":3468,"answer":3469},"How often should I update Firebase Security Rules?","Whenever you add a collection, change a data schema or change role definitions. Keep the rules file in version control, review changes like code, and run the emulator tests before every deploy.",{"readingTimeStats":3471},{"text":3472,"minutes":3473,"time":3474,"words":3475},"11 min read",10.295,617700,2059,"\u002Fblog\u002Ffirebase-security-guide-2026","firebase security rules","11 min",[448,3480,3481],"ai-assisted-app-development-guide-2026","app-maintenance-cost-guide-2026",{"title":2058,"description":3447},[3484,3487,3489,3492,3494,3497,3499],{"title":3485,"url":3486},"Firebase docs, Get started with Cloud Firestore Security Rules","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Fget-started",{"title":3488,"url":2286},"Firebase docs, Structure Cloud Firestore Security Rules",{"title":3490,"url":3491},"Firebase docs, Writing conditions for Cloud Firestore Security Rules","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fsecurity\u002Frules-conditions",{"title":3493,"url":2504},"Firebase docs, Control access with custom claims",{"title":3495,"url":3496},"Firebase docs, Firebase App Check","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fapp-check",{"title":3498,"url":2881},"Firebase docs, Build unit tests for Security Rules",{"title":3500,"url":3501},"Firebase docs, Understand Cloud Firestore billing","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fpricing","blog\u002Ffirebase-security-guide-2026","firebase security rules, firestore security, firebase authentication, cloud functions, role-based access control, firebase guide",[3505,3506,3507,3508,3509,3510],"Firebase says test mode \"allows anyone to read and overwrite your data\", while production mode denies all reads and writes from mobile and web clients. Start from the deny-all baseline and add access per collection.","If several match statements cover a request, access is allowed when any one of them allows it, so a broad rule cannot be narrowed by a more specific one.","On updates, check both resource.data (the stored document) and request.resource.data (the incoming one), or a user can hand their document to someone else or add fields you never validated.","Rules can read other documents with get() and exists(): 10 calls for a single-document request or query, 20 for batched writes and transactions. Each call is billed as a document read, even when the request is denied.","Server client libraries, including the Admin SDK in Cloud Functions, bypass Security Rules, so functions need their own authorization checks.","Test rules with the Emulator Suite and @firebase\u002Frules-unit-testing before every deploy, using assertSucceeds and assertFails.","https:\u002F\u002Fapp369.com\u002Fblog\u002Ffirebase-security-guide-2026\u002F","DNs35dfdoz4rcQy3pQKQqv18Gs_7yLpVZ2z060o9JBI",[3514,3516,3519,3520,3523,3524],{"keyword":5,"url":505,"title":5,"relevance":3515},63,{"keyword":3517,"url":3518,"title":3517,"relevance":902},"Cursor App Rescue: 7 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fcursor-app-rescue-2026\u002F",{"keyword":1323,"url":1712,"title":1323,"relevance":902},{"keyword":3521,"url":3522,"title":3521,"relevance":896},"Hire Someone to Finish Your Vibe-Coded App (2026)","https:\u002F\u002Fapp369.com\u002Fblog\u002Fhire-developer-finish-vibe-coded-app-2026\u002F",{"keyword":1716,"url":2054,"title":1716,"relevance":896},{"keyword":3525,"url":3526,"title":3525,"relevance":884},"v0 App Rescue: 8 Problems to Fix Before Launch","https:\u002F\u002Fapp369.com\u002Fblog\u002Fv0-app-rescue-2026\u002F",1792583246309]