US | Backend | 2026-08-16

Firebase Backend Planning for Business Apps

The Firebase choices that are cheap to make in week one and painful to change after launch.

Firebase Backend Planning for Business Apps

The Firebase invoice that catches people off guard is almost never storage. It is document reads from one list screen nobody thought about, multiplied by every app open, multiplied by every user. A conversations screen that pulls 40 threads on launch costs 40 reads per session. Ten thousand sessions a day is 400,000 reads a day, well past the 50,000-read daily free allowance. Firestore is cheap to start with and expensive to model badly, and most of what decides which one you get is settled in the first two weeks.

Model around screens, not around whiteboard entities

Relational instincts push toward normalized collections: users, orders, order_items, services. Firestore bills per document read, has no joins, and will only serve a query an index can satisfy. The useful question is not "what are my entities" but "what does this screen need in order to render from a single query".

Take a booking app. The provider dashboard lists today's appointments with the customer's name, photo, service name, and price. Normalized, that is one query for appointments plus one read per customer plus one read per service, so twenty appointments becomes 41 reads. Denormalized, each appointment document carries customerName, customerPhotoUrl, serviceName, and priceCents written at booking time, and the screen costs one query.

Two structural rules follow. Documents cap at 1 MiB, so anything unbounded such as message history or audit logs belongs in a subcollection rather than an array field. And a heavily read entity is worth splitting into a small public document and a private one, so a profile card does not drag phone numbers across the wire.

Every copied field needs a named owner

Denormalization converts a read problem into a write problem. Before duplicating a field, write down which document is the source of truth, what process updates the copies, and whether the copy is supposed to change at all.

That last point is where teams get it wrong in both directions. priceCents on a completed booking is a historical fact and must never follow a later price change, or last quarter's revenue silently rewrites itself. customerPhotoUrl is a cache and should be refreshed. Same technique, opposite policy.

Plan the fan-out before you need it. A user who changes their display name and has 3,000 bookings cannot be repaired in one batch, because batched writes cap at 500 operations. That is a paginated Cloud Function job with a cursor, far easier to write on day 20 than during an incident on day 200.

Security rules are your API contract, and they do not filter

The most common misunderstanding is that rules narrow results. They do not. Rules evaluate whether a query could return a document the caller is not allowed to read, and reject the whole query if so. A rule allowing reads of orders where ownerId equals the caller's UID will still fail an unqualified query on the orders collection, even when that user owns every order in it. The client query must carry the same constraint the rule expects.

Rules also cost money when they read. Each get() or exists() inside a rule is billed as a document read and capped at roughly ten access calls per single-document request, so a rule checking a membership document on every message read doubles the cost of your busiest screen. Where the check is coarse, covering role, plan tier, or organization, put it in a Firebase Auth custom claim: it rides inside the ID token and costs nothing to evaluate. The trade-off is propagation, since claims refresh when the token renews, so downgrades need a forced refresh.

Write your composite indexes down before launch

Any query combining several equality filters with an orderBy, or a range filter on one field with an orderBy on another, needs a composite index. Firestore allows roughly 200 per database, and index builds over a populated collection take minutes to hours. You cannot create one inside a hotfix window.

Keep firestore.indexes.json in version control and deploy it with the release that needs it, not after. Then set single-field index exemptions: long descriptions, serialized blobs, and large arrays are indexed by default though you never query them, and each adds write latency and storage.

Where the line sits between client and Cloud Functions

Server code should own what the client cannot be trusted with: payment entitlement, pricing, ranking, third-party keys, and any write touching another user's document. Most other work is cheaper straight from the SDK, since a function that reads Firestore and returns JSON costs an invocation, the read, and egress, where a direct query costs only the read.

Two specifics matter more than the rest. A customer who closes the tab during the post-checkout redirect has still paid, and Stripe keeps retrying its webhook on a backoff schedule stretching across three days, so entitlement has to be granted in the webhook handler and be safe to run twice. Signature verification also needs the raw request body, which Firebase's default JSON parsing destroys unless you read req.rawBody. Separately, Firestore triggers are at-least-once delivery: one write can invoke your function more than once, so anything that emails, charges, or increments a balance needs a dedupe document keyed on the event ID.

If a function sits on the checkout or sign-in path, set a minimum instance count; one warm instance costs less than a two to five second cold start at the moment a customer decides whether to trust you.

Storage rules and the paths you cannot rename later

Object paths encode ownership, so choose the shape first: /users/{uid}/uploads/{fileId} lets a rule authorize by path segment instead of by lookup. Enforce limits in the rule itself, checking request.resource.size and content type, or someone will push a 200 MB video into your avatar bucket.

Be deliberate about URLs. A Firebase download token produces a link that works for anyone holding it until the token is revoked: fine for marketing images, wrong for invoices or identity documents, which need short-lived signed URLs generated server-side. Plan resizing on day one too, or a list view will download 4 MB phone photos and users will blame the app for their data plan.

The cost traps that appear around 5,000 users

TrapWhy it bitesFix
Unbounded snapshot listenerReads the full result set on attach, then again after a long offline gapPaginate with limits, detach on screen dispose
Global counter documentSustained writes to one document throttle near one per secondSharded counters or a scheduled aggregation
Sequential timestamp keysMonotonic indexed values hotspot a single index rangePrefix the key with a shard value
Per-row count queriesEach aggregation is a separate billed queryKeep the count as a field on the parent
Verbose function loggingCloud Logging is free to about 50 GiB a month, then charged per GiBLog on failure paths, not every invocation

Decide six things before the first sprint: the query behind each screen, which fields are duplicated and who repairs them, whether authorization lives in claims or documents, the index list, the server-owned operations, and the storage path scheme. Changing any of them later means a migration under load, the most expensive way to learn Firestore.

CrateShip Studios
White-label Flutter apps, delivered in 30-60 days, from $2,500 - full source code included.
Get started