Authorization in security rules, not in route guards
Client-side guards decide what is *shown*; Firestore rules decide what is *allowed*. Every privileged collection is gated by rules that read the caller's role server-side, so hiding a menu item is a courtesy rather than the protection. Bypassing the UI gets you nothing.
Roles as documents rather than custom claims
Roles live in a `user_roles` collection that rules can read directly, and client writes to it are denied outright — only the admin SDK can assign one. Custom claims would have avoided a read per check but require a token refresh to take effect, which means a demoted admin keeps their access until they sign out again.
A hand-written service worker instead of a PWA plugin
Network-first for navigation and Firebase so content is never stale; cache-first for static assets; video fetched but never cached, because filling a phone's storage with sermon recordings is a way to get uninstalled. Offline falls back to the cached shell rather than the browser's error page.
The transaction reference is the document ID
Donations are written with `setDoc` keyed on the Paystack reference rather than `addDoc` with a generated one. Paystack retries webhooks until it gets a 200, so duplicate delivery is expected rather than exceptional — and keying on the reference makes a repeat write land on the same document instead of creating a second record. Idempotency becomes a property of the data model rather than a check somebody has to remember to write.
The client proposes, the server confirms
The record is written as `pending` before Paystack opens, so an abandoned payment leaves something to reconcile instead of nothing. Promotion to `success` comes from a Cloud Function that verifies the `x-paystack-signature` HMAC against the raw request body — parsing first and re-serialising would silently never match — and rejects before doing any other work. Security rules enforce the split rather than trusting it: a client may only ever write `pending`, and `success` is reachable only through the admin SDK, which bypasses rules entirely. A forged donation record is not discouraged, it is unreachable.