Skip to content

Secure Multi-User Apps with Firestore Security Rules

This is the article I wish I had read before I learned secure multi-user apps with firestore security rules the hard way — through production incidents, surprise bills,...

8 min read Firebase #firebase#auth#firestore#security

This is the article I wish I had read before I learned secure multi-user apps with firestore security rules the hard way — through production incidents, surprise bills, and late-night restores. Every paragraph comes from operating real applications, not from a marketing page.

Introduction

Authentication and data are the heart of every multi-user application, and Firebase provides both with an unusual combination: authentication is a five-line SDK call, while the real complexity lives in how you model data and write security rules. Mastery of Firebase is not mastering APIs — it is mastering the data model and the rule language.

This guide goes deep on the two parts that separate Firebase apps that age well from Firebase apps that get rewritten: authentication flows (email, Google, and anonymous, with their edge cases) and Firestore data modeling (collections, subcollections, and the deliberate denormalization that the document model rewards).

Everything here follows the patterns used in real production apps: ownership-aware rules, denormalized counters, real-time listeners used sparingly, and offline support that works by design rather than by accident.

Firebase concept

The model in practice: the pieces fit together before the first line of application code.

Why It Matters

Authentication done right is invisible: the user signs in once and never thinks about it again, while every access check behind the scenes uses the same identity. Firebase gives you that identity layer for free — email/password, Google, and anonymous upgrade flows — with token handling and session management included.

The data model decides everything that comes after: how fast queries run, how much the app costs per read, how hard the security rules are to write, and whether offline mode works. In Firestore, modeling for how the UI reads data is not a style choice — it is the whole game.

  • Auth identities are the access primitive: rules check request.auth, nothing else
  • Anonymous-to-account upgrade keeps the session while gaining real identity
  • Denormalized data matches the read path — the document model rewards it
  • Real-time listeners turn polling into push updates with zero infrastructure
  • Offline persistence makes the app usable where the network is not

The Problem

The auth problems are subtle: email flows that silently fail (providers disabled, redirect misconfigured, domains blocked), anonymous sessions that lose data on upgrade because the account wasn't linked properly, and client-side 'isAdmin' flags that live in app code instead of rules.

The modeling problems are the classic SQL transplant: users' posts crammed into a users document, query-time joins that don't exist, and counter fields that go stale because they are updated by client writes that fail. Firestore punishes thinking in tables — it rewards thinking in screens.

The Approach

Authentication: provide sign-in with the providers that fit your product — email/password as the universal fallback, Google for frictionless entry, and anonymous sessions for users who haven't committed. The anonymous flow is the power move: start the experience anonymously, then upgrade to a real account with linkWithCredential, preserving every document they created.

Data modeling: design documents to match screens. A profile screen reads one document; a feed screen reads one collection with a filter; a user's posts are a subcollection with a query on authorId. When data is read in two places with different shapes, denormalize — store the summary where it's read, and keep it updated with server-side functions or batched writes.

Three lines of app code and one rule. The anonymous user gets a stable UID immediately; documents are written with that UID; the upgrade links the credential without changing the UID. The rule then does the entire access job: this user, this UID, this document.


// The anonymous upgrade flow that preserves user data

async function signInAnonymously() {

  return await signInAnonymously(auth);

}



async function upgradeToGoogle() {

  const provider = new GoogleAuthProvider();

  const cred = await signInWithPopup(auth, provider);

  // link keeps the same UID — every document keeps working

  await linkWithCredential(auth.currentUser, cred);

}



// A rule that makes the UID the entire access story

allow read, write: if request.auth != null && request.auth.uid == resource.data.uid;

Firebase workflow

The workflow applied: configuration and discipline, not heroics.

Modeling Approaches

FactorNested in one docSubcollectionsDenormalized reads
Read pathOne doc, but grows unboundedQuery by parent, organizedExactly what the screen needs
Write pathOne write, but contentionMultiple writes, fineMore writes, with cleanup
Query flexibilityPoor beyond 2 levelsGood, filteredGood, purpose-built
Offline behaviorWhole doc cachedPer-collectionPer-collection
Best forSmall fixed shapesHierarchical dataRead-heavy UI data

The best model is usually a mix: subcollections for hierarchy, denormalized summary fields for the read path, and functions or batched writes to keep duplicates consistent. Model for the screens, not for the tables.

Implementation

Implement auth first and completely: providers configured in the console, email templates set, redirect domains verified, and the anonymous-upgrade path tested end to end. Then model the data with the screens in front of you, naming collections after the UI concepts and including the ownership field (uid) in every user-scoped document.

Then write the rules that encode the model: every collection gets explicit allow rules referencing request.auth and document fields; writes validate structure (required fields, allowed statuses) with request.resource checks; and reads deny-by-default. Finally, enable offline persistence and add real-time listeners only where live updates are a feature, not everywhere they are possible.

  • Configure every auth provider in the console and verify redirect domains
  • Anonymous-first flow with linkWithCredential upgrade preserving the UID
  • Collections named for screens; ownership field in every user-scoped document
  • Rules validate structure with request.resource — not just identity
  • Offline persistence enabled; listeners added per feature, not per habit
  • Denormalized counters updated in functions or batched writes, never client-only

Key Decisions

Denormalize or normalize?

In Firestore, denormalize for the read path. Store the display name and photo on the post document, not a join to the user document. The cost is write-time duplication; the benefit is single-query reads and simpler offline behavior. Consistency lives in functions and batch writes.

Subcollections or flat collections with filters?

Flat with filters when the parent relationship is just one field (authorId) and you need cross-user queries. Subcollections when data is strictly owned by the parent and always accessed through it. When in doubt, flat with a filter — it stays queryable.

Common Mistakes to Avoid

The auth mistakes: shipping with email verification disabled, anonymous flows that create a new UID on every visit (no persistence), and upgrade flows that lose data because they didn't link credentials. All three are detectable with a five-minute test session.

The modeling mistakes: counters updated by client code that can be called twice, denormalized data with no consistency mechanism, and real-time listeners on everything — each one billing reads on every keystroke. Functions and batched writes fix the first two; restraint fixes the third.

  • Email verification off: every account is one typo from being stolen
  • Anonymous sessions with persistence disabled — 'I lost everything' support tickets
  • Client-only counter updates — double taps double the count
  • Denormalized duplicates with no function or batch to reconcile them
  • Listeners on data that doesn't need to be live — the per-read billing tax
Firebase results

The payoff: infrastructure that runs quietly so the product gets the attention.

Putting It Into Practice

Audit one production Firebase app this week: check that email verification is on, that anonymous sessions persist and upgrade without data loss, and that every collection has an explicit deny-by-default rule. Then look at the listeners — every one that could be a fetch is a future bill.

For new features, write the model on a whiteboard first: what screen reads this data, what one query serves it, and what keeps the duplicates consistent.

Key Takeaways

  • Auth is the easy half: providers, anonymous upgrade, and one rule per user
  • Model Firestore for the read path: one screen, one query, ownership field present
  • Denormalization is a feature in Firestore — consistency lives in functions
  • Rules validate identity AND structure with request.resource checks
  • Listeners are features, not defaults — every one bills reads
  • The anonymous-to-account flow is the power move for user retention

Frequently Asked Questions

How do I handle admin roles in Firebase?

Never in client code. Store the role on the user document (written by a function that checks the existing admin list or a verified domain), and check it in rules with resource.data.role == 'admin'. Client-side role flags are decoration, not security.

Why do my Firestore queries sometimes feel slow?

Usually the query shape: unindexed filters and sorts force the platform to scan. Create composite indexes for every multi-field filter (the console suggests them when a query fails), and keep single-document reads hot with denormalized summaries.

Can Firebase auth work with my existing backend?

Yes — verify ID tokens server-side with the Admin SDK (or the public JWT key set) in any language. Firebase becomes your identity provider, and your API trusts its tokens. That is the standard pattern for hybrid apps.

Conclusion

Firebase mastery is a data-model skill with an auth wrapper: design documents for the screens, write rules that encode ownership, and use anonymous upgrades and real-time listeners as deliberate features.

Get the model and the rules right on paper first, and the app writes itself — with an auth layer that is five lines of code and a security model that lives in one reviewable file.

Related posts