Using Firebase Functions for backend logic
Android clients are good at presentation, but privileged work belongs on a server. Using Firebase Functions for backend logic lets an app validate a request, call a third-party API, read authoritative records, or trigger a notification without embedding credentials in the APK. Business rules then behave consistently across phones, tablets, and future releases.
This approach is useful when an app has more than one moving part. A signed-in customer might request a delivery quote, create a support case, or refresh a membership summary. The work can run through a callable function or a database, authentication, webhook, or schedule event. The client becomes smaller, while the backend scales independently and remains easier to audit.
Keep sensitive logic away from the device
An Android device cannot be trusted to enforce access rules. A modified app could change a price, replay a request, or supply another user’s ID. A function receives the caller’s verified Firebase identity, checks the payload, and fetches authoritative data such as an account tier from Firestore. API keys belong in Secret Manager or environment configuration, never in Kotlin source control.
Move work server-side when it must be consistent, privileged, or costly to repeat. Validating a promotional price, checking fraud signals, or sending a receipt usually qualifies. Keep simple UI state on the device, use Firestore Security Rules for direct access, and reserve functions for trusted execution.
Prepare a Firebase project for functions
Create the Firebase project matching the Android app, enable Authentication if needed, and select the Blaze plan required for production functions. Choose Firestore’s location before creating the database: australia-southeast1 places it in Sydney, and a Firestore location cannot be changed later. Deploy functions in the same region for lower latency.
Install the CLI, sign in, and initialise TypeScript functions:
npm install -g firebase-tools
firebase login
firebase init functions
Use the Functions and Firestore emulators locally, and commit firebase.json, source files, and the lock file without service-account keys. Store secrets with firebase functions:secrets:set; the Admin SDK uses the project service identity automatically, so a downloaded JSON key is unnecessary.
Call server logic securely from Android
A callable function is the usual bridge for an Android action. Unlike a generic HTTPS endpoint, the callable protocol handles CORS and exposes the caller’s authentication context. Still, check request.auth, authorise the user against your records, and validate types and size limits before using any client value.
Here is a minimal support-request function:
import { initializeApp } from "firebase-admin/app";
import { getFirestore } from "firebase-admin/firestore";
import { onCall, HttpsError } from "firebase-functions/v2/https";
initializeApp();
const db = getFirestore();
export const createSupportRequest = onCall(
{ region: "australia-southeast1" },
async (request) => {
if (!request.auth) {
throw new HttpsError("unauthenticated", "Sign in first.");
}
const message = String(request.data?.message ?? "").trim();
if (message.length < 10 || message.length > 500) {
throw new HttpsError("invalid-argument", "Use 10 to 500 characters.");
}
const ref = await db.collection("supportRequests").add({
uid: request.auth.uid,
message,
status: "open",
createdAt: new Date().toISOString(),
});
return { requestId: ref.id, status: "open" };
}
);
The app sends the message after signing in. Missing authentication returns unauthenticated; malformed text returns invalid-argument. On Android, call the function named in the deployment:
import android.util.Log
import com.google.firebase.functions.Firebase
private val createRequest =
Firebase.functions.getHttpsCallable("createSupportRequest")
createRequest.call(mapOf("message" to messageText))
.addOnSuccessListener { result ->
val requestId = (result.data as? Map<*, *>)?.get("requestId")
Log.d("Support", "Created $requestId")
}
.addOnFailureListener { error ->
Log.e("Support", "Could not create request", error)
}
Model loading, empty, success, and failure states in the interface. Never treat local UI state as proof of a paid entitlement. For long jobs, return an identifier and let the app poll or receive a notification instead of holding a connection open.
Match each event to the right trigger
Choose a trigger according to who starts the work and whether an immediate response matters. Callable functions suit interactive actions, while event functions decouple background processing from the interface.
| Mechanism | Best fit | Main caution |
|---|---|---|
| Callable | Android-initiated actions | Authenticate, authorise, and validate every call |
| Firestore trigger | Database-driven side effects | Delivery can repeat or arrive out of order |
| Auth trigger | Sign-up and account workflows | Blocking behaviour requires Identity Platform |
| HTTPS request | Webhooks and external callbacks | Verify signatures and rate-limit traffic |
| Schedule | Clean-up, reminders, and digests | Runs on a timetable rather than instantly |
Callable and HTTPS calls receive a response, but clients can time out or retry them. Firestore, Authentication, and Storage triggers are asynchronous and should be treated as at-least-once delivery. They can arrive out of order, so do not use a trigger for a calculation that must appear on screen instantly.
Use scheduled functions for clean-up, reminders, or daily digests. For payment webhooks, verify signatures, reject stale timestamps, and make every handler safe to run again.
React safely to database and sign-up events
A Firestore trigger can provision a membership record when a user document is created. Using set with merge makes repeated delivery harmless:
import { onDocumentCreated } from "firebase-functions/v2/firestore";
export const provisionMember = onDocumentCreated(
{ document: "users/{userId}", region: "australia-southeast1" },
async (event) => {
const snapshot = event.data;
if (!snapshot) return;
await getFirestore()
.doc(`memberships/${event.params.userId}`)
.set(
{ status: "pending", plan: "free" },
{ merge: true }
);
}
);
Never increment a counter blindly, because an event may be delivered twice. Store an event identifier or use a transaction with an idempotency marker. Avoid writing back to the triggering document unless a condition prevents a loop.
Authentication triggers can enrich a profile or call a CRM, although blocking user creation requires Identity Platform. Do not place sensitive details in custom claims: they travel to clients and have a size limit. Give integrations separate service-account permissions, and log identifiers rather than message bodies.
Control region, cost, privacy, and rollout
Keep timestamps in UTC and convert them on the device. Australia/Sydney observes AEDT or AEST, Brisbane uses AEST, and local dates are commonly written day first. This prevents an expiry time or delivery promise from shifting unexpectedly.
Function invocations, outbound calls, storage, and logs contribute to a Blaze bill; set a budget alert, cap retries, and inspect usage before enabling min instances. Secret Manager protects configuration, while App Check reduces abuse but does not replace server-side authorisation. If personal information is handled, assess the Privacy Act 1988 and Australian Privacy Principles, and apply the Spam Act 2003 to commercial messages. For paid memberships, calculate GST at 10%, store money in integer AUD cents, and display GST-inclusive consumer prices.
An Australian team may call this “moving the fiddly bits off the phone”; a friendly “no worries” belongs in chat, but runbooks need exact regions and timestamps. Before production, use these checks:
Android checks
- Test signed-in, signed-out, and expired-token paths.
- Show progress and make retries safe during network loss.
- Read entitlements from the server, not local flags.
- Use a staged Google Play rollout and watch latency.
Backend checks
- Re-check identity and authorisation on every request.
- Make retried writes and webhook handling idempotent.
- Set timeouts, structured logs, and request identifiers.
- Review function, network, and database spending.
Emulator tests, a production canary, and monitored logs provide a safe rollout while preserving a clear server boundary for the Android codebase.