SYS://FIELD-REPORT-02
Your Firebase Database Was in Test Mode
The app worked. It looked finished. Its entire database was also readable and writable by anyone on the internet with a browser — no login, no trick, no cleverness required. And the owner would never have known, because nothing was watching. Here’s the full teardown, worst finding first.
This is a representative audit — the exact pattern I run into over and over on apps built with Lovable, Bolt, v0, and Cursor. Names and paths are changed, but every finding below is real-shaped and drawn straight from the kind of report the launchworthy skill produces. Call the app rosterly: a Firebase (Firestore) + React roster tool built with Bolt, pre-launch, about to be handed to its first paying gym.
It got a scorecard. The scorecard was not kind.
One critical does not mean the app is bad. rosterly is genuinely nice — clean UI, real auth screens, the works. It means the app is finished-looking, which is a different and more dangerous thing, because finished-looking is exactly the state in which people hit deploy and hand out the URL.
The finding that ends the app: the rules were still in test mode
The number one critical bug in a Firebase app is not exotic. It’s a database still running the security rules Firebase hands you on day one.
rosterly’s firestore.rules looked like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 8, 1);
}
}
}
That is test mode. Firebase writes it for you so you can build without fighting permissions, and it’s fine — for about thirty days, on your laptop, while you’re the only user. But allow read, write: if request.time < timestamp.date(2026, 8, 1) means every document in the database is readable and writable by anyone, right up until an expiry date nobody remembers setting. Members, gym owners, billing records, the lot. No auth check. No ownership check. if true with a countdown timer.
And here’s the part people miss: this isn’t a “someone determined enough could get in” problem. Firebase project IDs are guessable and the REST endpoint is public. Open databases are indexed and scanned continuously. You don’t need to know the app — you need to know the project, and that ships in the frontend by design.
CRITICAL · CAUGHT
Firestore rules left in test/open mode
Every collection in rosterly — members, rosters, billing — is readable and writable by anyone with the project ID. That’s not “user A can see user B.” That’s “a stranger with curl can dump and overwrite the entire database.” And you’d find out from an angry customer, because nothing is tracked.
The fix is not exotic either. Security in Firebase comes from Rules, and Rules are just code you write once and deploy.
THE FIX · IN ORDER
Lock the rules to the owner
- Replace the catch-all with per-collection ownership rules. Documents carry a
userId, so gate reads and writes on it:allow read: if request.auth != null && resource.data.userId == request.auth.uid;and userequest.resource.data.userId == request.auth.uidon create so nobody can write a doc owned by someone else. - Commit
firestore.rulesto the repo. If the rules only live in the console, they can’t be reviewed or restored — that alone is a[HIGH]. - Deploy and verify:
firebase deploy --only firestore:rules, then use the Rules Playground to simulate User A reading User B’s doc. It must deny.
Verify it by hand
Signed in as User A, try to read a document that belongs to User B from the client. Before the fix you get data. After, you get a permission-denied. Paste that error into your notes — that’s your evidence the rules actually landed, not just that they deployed without a syntax error.
The false alarm that wastes everyone’s time
Now the part that matters most for credibility, and the reason I trust an audit that gets it right.
Point a generic “review my code” session at rosterly and it will almost certainly flag this, two files away from the real critical:
FALSE POSITIVE
⚠️ Your Firebase apiKey is hardcoded in the client!
This is wrong. The Firebase config object — apiKey, authDomain, projectId, all of it — is public by design. It’s meant to ship to the browser; it’s how the SDK finds your project. It is not a secret and there is nothing to hide. Flagging it is the tell of a reviewer pattern-matching on the word “apiKey” instead of understanding Firebase’s threat model.
The distinction is the whole game:
- Firebase config in the frontend → fine, by design. It identifies your project; it grants nothing on its own.
- Test-mode Rules → catastrophic. In Firebase, Rules are the security boundary, not the config.
Hiding the apiKey would have changed nothing — the database was open regardless. A reviewer that spends its one flag on the config while allow read, write: if true-with-a-timer sits unmentioned in firestore.rules has it exactly backwards. It burns your attention on a non-issue while the thing that actually breaches you goes unnamed.
The rest of the punch list
The critical is the blocker. The rest is the week-two work, ranked so the scope stays honest:
FIX THIS WEEK
After the critical
- [HIGH] No error tracking anywhere. No Sentry, no global handler. When something breaks in production, you find out from a user leaving, not from a system. Install Sentry client + server and wrap the app in an error boundary.
- [HIGH] No rate limit on the callable function in
functions/src/sendInvite.ts:18, which hits a paid email API. One script in a loop overnight is a four-figure bill — rate limiting here is an abuse and cost control, not premature optimization. - [HIGH]
firestore.ruleswasn’t tracked in the repo before the fix — the rules only lived in the console, so nobody could review or restore them. Committing the file (step 2 above) closes this too.
Four findings total. But note the shape: one of them can end the business, and three are homework. That ranking is the point. A flat list of four items reads as four equal chores. Worst-first with severities reads as “fix the open database before you hand out the link, then breathe.”
ASK YOURSELF
“Firebase handles security for me.” It gives you the tools; it does not turn them on. Firebase ships in test mode that expires into either open or closed — and rosterly’s expired into open. The platform is secure. Your configuration was not. “It’s meant to be public” is true of the config and false of the database behind it; the config is only safe because the Rules are supposed to do the actual restricting. When the Rules say if true, “meant to be public” is precisely how the breach happens.
Why this keeps happening
None of this means AI builders are bad, or that rosterly’s owner did anything foolish. The tools are extraordinary at getting you to looks finished. They are, by their nature, silent about the gap between that and safe to launch — because that gap is invisible on your own screen, where you’re logged in as yourself, the test-mode timer hasn’t mattered yet, and there’s exactly one user: you.
That gap is the whole reason launchworthy exists. It plays bouncer at the door of production: it knows the difference between the Firebase config (fine, ship it) and test-mode Rules (critical, close them now), it refuses to mark anything safe it can’t actually verify, and it hands you the fix, not just the finding.
The hard part was never writing the rules — that’s twenty lines and one deploy command. It was knowing that the scariest-sounding thing everyone flags, the apiKey in the bundle, wasn’t the problem at all — and that the actual open door was sitting in a file the generic reviewer never opened.