How to run Faraan Alumni Network
Every admin task, end-to-end. Search the index above, or scroll through.
1 Getting started
The first 5 minutes — open the panel, recognise the layout, know what you can do.
Where is the admin panel?
- Live: https://faraan.in/admin/ (or the legacy URL
https://faraan-alumni-network.web.app/admin/— both serve the same app while the new domain is being rolled out) - Local dev:
http://localhost:8765/admin/(withnpm run dev)
Two ways to open it
- From any device, visit
/admin/on the live URL. You'll see a phone-OTP login screen unless you're already signed in. - If you're already signed in on the main app, tap the dark Admin pill in the home tab's top-right (visible only when your account has the
adminorsuperAdminclaim).
The layout, top to bottom
- Left sidebar
- Section nav — Dashboard, Alumni, Verification, Batches, Groups, Initiatives, Memories, Causes, Shoutouts, Reports, Blocked users, Volunteers, Broadcast, Site content, Audit log, Super admins (super-admin only).
- Top bar
- Page title on the left; View site → link on the right to jump back to the public app.
- Bottom-left
- Your phone + role + Sign out.
2 Roles & powers
Who can do what. Roles are stored as Firebase custom claims — server-enforced, not just UI.
| Role | What they can do | How it's granted |
|---|---|---|
| Super admin | Everything below + add/remove other admins, manage config/super_admins, hard-delete users. |
Phone must be in config/super_admins.phones. claimAdminIfEligible elevates them on next sign-in. |
| Admin | Verify alumni, moderate all content, manage batches/groups/initiatives/causes, broadcast, block users, edit any profile, set Conveners. Cannot add other admins. | Set by a super admin via the Role button on the alumni row. |
| Staff | Moderate memories/shoutouts/reports, run verification queue. Cannot delete users, manage admins, broadcast. | Set by an admin via the Role button. |
| Verified alumni | Post memories, comments, shoutouts, join groups, contribute to initiatives. | Set when an admin approves their verification (queue or direct). |
| Alumni | Browse, edit own profile, view shoutouts (no posting). | Default for any signed-in user. |
Convener (separate concept)
A Convener leads one of the 12 causes. This is not a custom claim — it's stored in Firestore (causes/{key}.convenerUid + users/{uid}.convenerOf[]). A Convener has no extra system powers by default; the role is operational, not technical.
3 Sign in / sign out
Sign in
- Open
/admin/. - Enter your phone in the field (10 digits — country code
+91is prepended automatically). - Tap Send OTP. You'll get a 6-digit code via SMS.
- Enter the 6 digits (or use your phone's SMS autofill — the first input has
autocomplete="one-time-code"). - Tap Verify & enter. If your phone is on the admin or super-admin list, you'll land on the Dashboard. If not, you'll be signed out with a "Not a super admin" toast.
Sign out
Bottom-left of the sidebar → Sign out. Your phone stays remembered for next time (Firebase persistence), but your auth tokens are cleared.
role or superAdmin claim immediately, sign out and sign back in. Otherwise it'll auto-refresh within an hour.
4 All users — search & filter
The Alumni tab is the master list of every signed-in user — alumni, supporters, staff, admins, and anyone still pending the role fork. Up to 500 are loaded into memory and filtered as you type.
Filter chips (top of the table)
Four chips let you slice the list by user type:
- All
- Everyone except soft-deleted users (default).
- Alumni
- Users with
role: 'alumni'— or legacy users created before the role field existed (anyone withbatch/ SSLC / PUC info is implicitly an alumnus). - 🤝 Supporters
- Well-wishers, parents, family, donors, ex-staff — anyone who picked the supporter track on the role-fork screen. They're auto-active and never enter the verification queue.
- Pending verification
- Brand-new users who signed in but haven't yet picked alumnus or supporter. Usually empty — useful for catching anyone stuck mid-fork.
Searching
- The search box matches name, phone, email, or city — case insensitive, substring.
- Search composes with the filter chip: chip narrows the population first, then search filters within it.
- Soft-deleted users are hidden from this list by default.
The row at a glance
- Photo + name
- Either their uploaded avatar or an initials chip. Phone (or email) underneath.
- Auth column
- How they signed in — 📱 Phone (OTP-verified by Firebase) or ✉️ Google (Google-verified email). A faded chip means the data was inferred for users created before this field was captured. Every user has passed one of these first-stage identity checks — including supporters.
- Institution / Relationship
- For alumni: Faraan College / Faraan High School / Lilly Rose.
For supporters: their declared relationship (🤝 Parent, 🤝 Donor, 🤝 Family friend, etc.). - Batch
- SSLC year for alumni.
—for supporters. - City
- Free-text or one of the city pills.
- Role chip
- admin / staff / alumni / supporter / pending.
- Status chip
- For alumni: Verified / Unverified.
For supporters: Active (auto-active, no verification queue).
For everyone: Blocked / Deleted if applicable. - Actions
- View · Verify · Edit · Role · Delete/Block — Verify only appears for unverified rows.
The View modal
Tap View for a read-only summary. The header line shows "Signed in via 📱 Phone OTP (verified)" or similar so you can see first-stage verification at a glance. For supporters specifically:
- Relationship row — what they declared on the supporter quick form
- Supporter note row — their optional short message ("My daughter studies here, happy to help with library books")
Phone (call) and WhatsApp are listed as separate rows — alumni who live abroad often have an Indian WhatsApp number and a Gulf calling number. The View modal surfaces both.
4b Supporters — the well-wisher track
A parent, donor, family member, or friend who arrives via a shared WhatsApp link doesn't have to fake being an alumnus to join. The supporter track gives them a slim, read-and-contribute view.
What is a supporter?
Anyone who, after signing in with phone OTP or Google, picks "🤝 I'm a well-wisher / supporter" on the role-fork screen. They go through a 60-second quick form (name + relationship + optional message) and land straight in the app — no verification queue.
The 6 relationship buckets
- Parent of current student — most common; shows up around contribution drives
- Family of alumnus — siblings, spouses, children of alumni
- Family friend — close to the school community
- Donor / philanthropist — wants to contribute to initiatives
- Former staff — ex-teachers, ex-administrators
- Other — catch-all
What supporters can do
- Read the Memory Wall (no posting)
- Browse Causes & the Impact dashboard
- Contribute to initiatives (same flow as alumni)
- Edit their own profile, see contribution history, manage notifications
What supporters cannot do
- Post memories or shoutouts (composer hidden + Firestore rule blocks)
- Like or comment on memories (CSS-hidden + Firestore rule blocks likes)
- Browse the Alumni directory / Community tab (privacy)
- Be assigned as a Cause convener
First-stage verification still applies
Supporters are not anonymous. Before they ever reach the role fork, Firebase has either verified their phone number (real SMS OTP challenge) or their Google email. You'll see this in the Auth column of the user list.
If a supporter is actually an alumnus
- Open their row, tap Role.
- Change to alumni, Save.
- They'll need to fill in batch info next time they open Edit Profile (or you can do it from the Edit modal).
- Click Verify on the same row to grant posting access.
If an alumnus is actually a supporter
Open their row → Role → set to supporter. Their verified claim stays as-is but client-side gates immediately hide posting UI on their next sign-in.
5 Verifying alumni
Two ways: directly from the Alumni row (you recognise them) or through the Verification queue (they uploaded proof).
Path A — Direct verify (you know them)
- Click the green Verify button (visible only for unverified rows).
- Read the warning. Optionally type an internal note ("Saw them at the 2023 reunion", "Friend of Ayesha", etc.).
- Tap Verify now.
What happens server-side
verified: truecustom claim is set on their Firebase Auth user.users/{uid}doc getsverified: true, verifiedAt, verifiedBy, verifiedDirect: true, verifiedNote.- They get an inbox notification, a push (if FCM token exists), and an email (if email on file): "You are verified ✓ — Welcome aboard!"
- An entry is written to
audit_logasuser.verified_directwith your UID + the note.
Path B — Verification queue (they submitted proof)
The red badge next to "Verification" in the sidebar shows pending count. Each row shows:
- Submitter's name + photo + claimed institution + claimed batch
- Either a proof image OR batchmate-reference details (name + phone of the reference)
- Submitted-at timestamp
- Click the proof thumbnail to enlarge it.
- Cross-check name, institution, batch against the proof.
- For batchmate references: tap the reference phone link to WhatsApp / call them and confirm.
- Tap Approve or Reject.
Approve does this
- Sets
status: 'approved'on the request - Sets
verified: trueclaim + mirrors on user doc - Sends inbox + push + email same as Path A
- Closes the request with
decidedBy: yourUid, decidedAt: now
Reject does this
- Sets
status: 'rejected' - Sends inbox + push: "Verification needs another try" — they can resubmit with clearer proof
- No claim change
Editing an already-verified user
Verification is sticky once set. If you need to revoke it, use the Role button or contact a super admin — there is no one-click "Unverify" in the UI (intentional, to prevent admin foot-guns). To remove the claim manually, run npm run set-admin with the appropriate flags from the dev machine.
6 Editing a profile
What you can change
- Name, institution, batch (SSLC year), city, country, present job, company, email, WhatsApp, bio
- Stream / degree
- Linked social URLs (LinkedIn, Facebook, Instagram, X)
What you can't change here
- Role — use the separate Role button. Role changes always require a re-sign-in by the user.
- Verified status — use the Verify button or Verification queue.
- Their phone number (the Auth identity) — that requires a server-side admin SDK script.
- Their UID — never. UIDs are immutable.
audit_log with action user.updated, your UID, and the patch contents.
7 Assigning roles
Tap Role on any alumni row. A dropdown shows the three roles you can assign.
The rules baked in
- Anyone with the
adminclaim can promote to staff. - Only super admins can promote to admin (the option is disabled for non-supers).
- Demoting a super admin: only another super admin can do this. Reduces the role and clears the
superAdmin: trueflag. - You cannot change your own role.
What happens after a role change
The target user gets a fresh custom claim on their Firebase Auth user. They need to sign out and back in for the change to take effect (or wait ~1 hour for the auto token refresh).
8 Delete vs Block vs Soft delete
One button, three actions inside. Pick the one that matches your intent.
| Action | What happens | Can they sign back in? |
|---|---|---|
| Soft delete | Sets deleted: true on the user doc. They disappear from the Alumni list but Firestore + Auth records survive. |
Yes — but they won't see content meant for verified alumni until restored. |
| Delete | Removes the user doc and the Firebase Auth user entirely. Their memories/shoutouts stay (they become "ghost-authored"). | Yes — they can register fresh with the same phone. New UID, blank profile. |
| Block | Disables their Firebase Auth account. Adds their phone + email to config/blocked. Sign-in fails outright. Reversible via Unblock. |
No, until you unblock. |
Manage block (existing blocks)
If a user is already blocked, the button label becomes Manage block. You'll see an Unblock action that:
- Re-enables their Auth account
- Removes their phone / email from
config/blocked.phones/.emails - Sets
blocked: falseon the user doc withunblockedAt, unblockedBy
When to use what
- Soft delete — duplicate / test account, want to keep history
- Delete — they asked for GDPR removal but might rejoin later
- Block — they violated terms; you want them off, with the door locked
9 Blocked users tab
Two distinct concepts share this tab:
- System-blocked accounts — users an admin has Blocked via the Alumni row (above). These are also in
config/blocked. - User-to-user blocks — a regular alumnus can block another alumnus via the ⋯ menu on a memory card. That mutes the other person from their feed only. This view lets admins see "who's muting whom" for moderation context.
This tab is read-only. To unblock at the admin level, use the Alumni tab → Manage block.
10 Merging duplicate accounts
When the same person has accidentally signed up twice — once with phone, once with Google.
The auto-merge UX (no admin action needed)
Users can self-merge through Edit Profile → Connect Google (or Add phone). The app detects the conflict and offers "Combine your profiles?". On tap, the server runs mergeAccountByPhone or mergeAccountByEmail depending on direction — moving memories, comments, contributions, group memberships, FCM tokens, inbox, blocked list, and verification claim from the orphan into the surviving account. The orphan is then deleted from Firestore + Auth.
When you may need to merge manually
- The user can't access one of the accounts (lost phone, can't sign in)
- The auto-merge failed mid-way (rare, but possible if a content collection hit a quota)
- Two accounts exist with no overlapping identifier (different phone AND different Google email — these can't be auto-detected)
Manual merge — admin SDK script
Run from the dev machine with the service-account JSON:
GOOGLE_APPLICATION_CREDENTIALS=./sa.json node -e "
const admin = require('./functions/node_modules/firebase-admin');
admin.initializeApp({ projectId: 'faraan-alumni-network' });
// ... callerUid + orphanUid + merge logic
// (see scripts/migrate-batch-schema.mjs for a similar pattern)
"
The pattern: copy orphan fields → patch caller doc → re-author memories/shoutouts/comments → move FCM tokens + inbox → delete orphan doc + Auth user → audit account.merged_admin_sdk.
audit_log with action: 'account.merged_admin_sdk', fromUid, toUid, and a reason note. Future you will need this.
11 Memories — hide & delete
Table of the 50 most recent memory posts, newest first.
What each column shows
- When — created timestamp
- Author — resolved name + photo + batch (no more raw UIDs)
- Text — first 80 chars
- Likes — count
- State — Visible or Hidden
- Actions — View · Hide/Restore · Delete
View — for full context
Shows the entire memory text + any attached photo. Has Hide/Restore + Delete buttons in the modal footer too, so you can act without dismissing the dialog.
Hide
Sets hidden: true on the memory doc. The post disappears from feeds and search but the doc + likes + comments + media are preserved. Reversible with Restore.
Delete
Hard-deletes the memory doc. Likes and comments are orphaned (still exist as subcollection docs, but unreachable from the UI). Photos and voice notes in Cloud Storage are NOT auto-deleted — use scripts/cleanup-orphan-media.mjs (TODO) for housekeeping.
12 Shoutouts moderation
Identical UX to Memories — Type chip · Author cell · Text · State · View / Hide / Delete actions. The only difference is the underlying collection (shoutouts) and the CF (adminModerateShoutout).
Common reasons to moderate
- Birthday wishes posted with sensitive private info (phone, address) — hide and ask the author to repost
- "Need help" posts that look like scams — delete
- Spam from a hacked account — delete + Block the user (Alumni tab)
13 Reported content
Anyone signed in can flag a memory, shoutout, comment, or user. Reports land here. The red badge next to "Reports" shows the open count.
Filter
Top-right dropdown: Open (default) · Resolved · Dismissed · All.
Per-row actions
- View — fetches the target doc and shows it as JSON so you can read the full content + metadata.
- Remove content — calls the right moderation CF for the target type:
adminModerateMemory,adminModerateShoutout, oradminDeleteUser(soft). Also auto-resolves the report. - Resolve — marks the report as resolved without touching the target. Use when you took action outside the panel.
- Dismiss — marks the report as dismissed (false alarm).
Decision framework
| Report says | What to check | Likely action |
|---|---|---|
| Spam | Author's recent posts pattern | Remove content, possibly Block the user |
| Sensitive info exposed | The content itself | Remove content; DM author to repost |
| Wrong batch claim | User's profile vs proof | Resolve (no content action); review verification |
| Personal dispute | Context of the conversation | Usually Dismiss; encourage user-block instead |
14 Causes — the 12 pillars
The Trust runs 12 causes — Education Support, Orphan Care, Elderly Care, Healthcare Support, Marriage Assistance, Widow & Single-Mother Support, Needy Family Support, Skill Development & Employment, Awareness & Outreach, Religious & Spiritual Support, Disaster & Emergency Relief, Media & Documentation. Alumni opt into up to 3 from their Profile.
The table
- Sort
- The display order (1–12). Currently fixed by the seed script.
- Cause
- Icon + name + short description.
- Active
- Toggle chip. Inactive causes are hidden from new pickers but stay visible for alumni who already chose them.
- Convener
- Photo + name of the assigned alumnus, or "Unassigned".
- Members
- Live count of alumni who have this cause in their
users.causes[]array. - Actions
- Edit · Assign / Change convener · Unassign (when assigned)
Editing a cause
The Edit modal lets you change name, icon (emoji), color (hex picker), short description, and long description. active, convenerUid, and sortOrder are managed separately and not in this form.
Toggling active
Tap the Active chip in the row. No confirmation — it just flips. Use this when:
- The Trust pauses a cause for a quarter (e.g. health camps off-season)
- A cause is renamed and replaced — deactivate the old, seed the new
15 Assigning Conveners
Assigning
- On the Causes table, tap Assign convener (or Change convener if one exists).
- A modal opens with a search box. Type a name / phone / email — up to 50 matching alumni are shown.
- Tap a row → confirm → done.
What happens server-side
The setConvener CF runs in a single transaction:
- If the cause already had a convener, that person's
users.convenerOf[]has the cause key removed. - The new convener's
users.convenerOf[]gets the cause key added (arrayUnion). causes/{key}.convenerUidis set to the new UID.- Audit logged as
cause.convener_set.
role: 'admin' separately. This is deliberate — Convener-ness is per-cause, not global.
Unassigning
Tap Unassign in the actions column. Confirm. Both sides of the link clear and the cause goes back to "Unassigned".
16 Initiatives — create, edit, close
An "initiative" is a specific fundraiser or programme — "Sponsor a Faraan student 2026", "Free health camp · Gulbarga", etc.
Creating a new initiative
- Tap + New initiative top-right.
- Fill: Title · Category · Cause (optional, picks one of the 12) · Goal in ₹ · Cover image URL · End date.
- Tap Create. Status defaults to
live.raised: 0, supportersCount: 0.
Editing
The Edit modal lets you change everything except raised, supportersCount, and createdAt (those are immutable here — they're driven by the contribution flow). Status can be flipped between live / paused / completed. Cover image: upload directly or paste a URL.
Posting an update
Tap Post update. Add a short message + optional photo. This appears under the initiative on the public side and gets a push notification to supporters.
Close
Click Close on a live initiative. Sets status: 'completed', closedAt: now, closedBy: yourUid. The contribution ledger is preserved.
Delete
Permanent. Use only for accidental creations / test data. The contributions ledger is also lost. Confirm dialog warns about this.
Auto-rollover
The rolloverInitiatives CF runs daily at 00:30 IST. Any initiative past its endsAt gets auto-closed with status: 'completed'. No manual action needed.
17 Contribution ledger
Per-initiative list of every contribution recorded: contributor name, amount, anonymous flag, timestamp, optional note.
How contributions are recorded today
The app's payment integration is not yet wired. Contributions are coordinated manually by the Finance & Fundraising lead (currently Dr Shakeel on +91 96634 14714). When money is received:
- Open the Initiative ledger
- (Future feature) Tap Record contribution → fill amount + contributor UID + anonymous flag
- The
recordContributionCF writes a subcollection doc and incrementsraised+supportersCountatomically
18 Batches
The batches collection is auto-seeded — SSLC batches 1985 to current year for Faraan High School + Lilly Rose Primary. A scheduled CF (addYearlySSLCBatch) runs every Jan 1 at 00:05 IST to add the new year's batches automatically.
What you can do
- Create a manual batch — useful for one-off cohorts (e.g. a special programme not covered by SSLC/PUC)
- Edit a batch — change lead name, notes
- Delete a batch (only empty ones — Firestore won't auto-cascade members)
Member count maintenance
The onUserBatchChange Firestore trigger fires on any user-doc write. It diffs the user's SSLC/PUC year/institution before vs after, then updates the matching batch's membersCount. PUC batches are created lazily on first pick.
membersCount manually
Let the trigger maintain it. If it drifts, run npm run migrate-batch-schema to recount from the user collection.
19 Groups
Groups are community spaces — Batch reunion circles, Doctors Network, Gulf Alumni Circle, etc.
Creating a group
Tap + New group. Fields: Name · Emblem (1–4 chars) · Color (gradient palette pick) · Description · Cover image.
Editing / deleting
Same modal pattern. Delete is hard-delete (cascades members subcollection — Firestore doesn't auto-cascade, so the CF does it explicitly).
Member management
Tap a group row → Members to see who joined and when. Remove individual members via the row's danger button.
requiresApproval flag + pending-joins queue + approve action) is pending — it's Task 6 of the SOP v2.1 implementation plan. Until then, manage admission outside the app via WhatsApp + remove non-members manually.
20 Volunteers
Anyone who tapped the "Volunteer" pill in the app fills a short form (city + WhatsApp + interests). Each submission lands in volunteers/{uid}.
What the table shows
- Resolved name + photo + batch (clicking through to the alumnus is on the roadmap)
- City · WhatsApp · Interests (e.g. Education, Healthcare, Outreach)
- Last-updated timestamp
- Actions: WhatsApp them (opens wa.me link) · Remove (deletes the volunteer doc; their user account is untouched)
Coordination tips
- WhatsApp the new volunteer within 24h to keep momentum
- Match them to the Convener of an interest they ticked
- If they vanish for 3+ months, leave them — they might come back. Remove only on explicit unsubscribe.
21 Broadcasting announcements
Sends an in-app inbox notification + a push notification to every alumnus on the network. Use sparingly — too many broadcasts dull the signal.
Fields
- Title — short, ~50 chars. Becomes the push notification title.
- Body — ~140 chars. Becomes the push notification body.
- URL (optional) — where to open when tapped. Defaults to
/(home).
The send action
- Fill the fields. The character counters help you stay short.
- Tap Send broadcast.
- The CF creates an
announcements/{id}doc + sends FCM multicast to everyfcmTokensdoc across all users (batched 500 at a time). - Toast confirms: "Sent · reached X devices".
Recent announcements
Below the form: a history table of every broadcast sent, with Delete action. Deleting an announcement doesn't unsend the push — it only removes the inbox record going forward.
- Major news (verified launch, big event): max once a week
- Initiative updates: better to post inside the initiative (auto-pushes to supporters only)
- Personal greetings (Eid, Ramzan): yes, but craft them well
22 Site content editor
Every user-facing string in the app (hero title, onboarding copy, section labels, etc.) is editable here without redeploying. Changes hit Firestore content/strings and propagate within ~60 seconds via the live listener.
Organised by section
- Brand (short name, sub-tagline, splash text)
- Onboarding slides (title + body per slide)
- Sign-in screen
- Home hero + quick actions + section titles
- Community labels
- Memory wall labels
- Initiatives copy (transparency block, membership tiers, support modal)
- Profile labels
Save behaviour
Each section card has its own Save button. The CF validates that no key exceeds 500 chars + that the writer is admin/staff, then writes content/strings.{key}.
23 Super admins
Visible only to users with the superAdmin: true claim. Non-supers see "Access restricted".
Adding a super admin
- Confirm the person has signed in at least once with their phone OTP on the main app. If not, ask them to do that first.
- Open Super admins. Type their phone in strict E.164:
+91XXXXXXXXXX. No spaces, no dashes. - Tap Add super admin. Confirm the warning.
- Tell them to sign out and back in on their device to pick up the new claim.
Removing
Tap Remove on their row. Confirm. They keep alumni access but lose super-admin powers immediately on next token refresh.
Safety rails (server-enforced)
- You can't remove yourself — the button is disabled on your row
- You can't remove the last super admin — disabled when only 1 remains
- Non-super-admins who somehow call
adminAddSuperAdmindirectly are rejected byrequireSuper(req)in the CF
config/super_admins doc is the allowlist. If it ever gets deleted or empty, no one can sign in as super admin again. To recover: run npm run seed-admins -- +91XXXXXXXXXX from the dev machine with the service-account JSON.
24 Audit log
Every meaningful admin action writes a row here. The Dashboard tab also surfaces the latest 15.
What's logged
- Verification decisions (approve / reject / direct)
- Role changes (admin promotions / demotions)
- Content moderation (hide / delete / restore on memories + shoutouts)
- User deletions (soft / hard) + blocks / unblocks
- Account merges (by phone, by email, by admin SDK)
- Convener assignments + unassignments
- Cause edits
- Broadcasts sent
- Super-admin additions + removals
- Initiative creates / updates / closures / deletes
- Reports filed + resolved
Reading a row
| Column | What it means |
|---|---|
| When | UTC timestamp formatted for IST. |
| Action | Namespaced, e.g. user.verified_direct, cause.convener_set, account.merged_by_email. |
| By | First 10 chars of the actor's UID. Cross-reference in the Alumni tab if needed. |
| Detail | JSON blob of additional context — target UID, fields changed, reason notes, etc. |
audit_log collection has a write-once rule. If someone tries to tamper, the rule rejects.
25 Troubleshooting
Common failure modes + their fixes.
"User not found" when adding a super admin
The target phone doesn't have a Firebase Auth user yet. Ask them to sign in once with that phone (OTP flow) on the main app, then retry. The Auth user is created on first successful OTP.
"Not a super admin — signing out" when I try to enter /admin/
Your account has the admin role but not the superAdmin flag, OR your account isn't admin at all. Check config/super_admins.phones from the dev machine — your phone must be on the list. If you just got added, sign out and sign back in to refresh your token.
Email notifications aren't arriving
- Check the Gmail account (
zaheerdv@gmail.com) Spam folder. - Check Cloud Function logs:
firebase functions:log --only onUserDocCreate. If you see "Onboarding email failed: Invalid login", the Gmail app password has expired. Rotate it (see Key rotation). - Check throttle — only 30 onboarding emails go out per 24h. The 31st onwards is queued for the daily summary (09:00 IST).
Push notifications aren't delivering
Known limitation: the Firebase web vapidKey is empty in firebase-config.js. FCM token registration silently short-circuits. Browser push doesn't fire in production — only the in-app inbox works. Fix on roadmap. Native iOS / Android PWA installs also don't have a vapid path yet.
An alumnus' name shows as "Alumni" in the admin list
This used to happen for every phone-OTP signup (Firebase Auth provides no displayName for phone, and ensureUserDoc falls back to the literal string "Alumni"). It's fixed now — new signups are routed through batch onboarding which forces a real name. Existing "Alumni" rows get re-routed once on next sign-in. If a particular user is stuck, manually edit via Alumni → Edit.
Causes don't load in the Profile screen
Check that config/super_admins rules are deployed AND that /causes has the read rule. Run firebase deploy --only firestore:rules from dev. The signed-in alumnus needs the rule to read the collection.
"Permission denied" when an alumnus tries to post a memory
They're not verified. The Firestore rule for memories.create requires isVerified(). Verify them (Alumni → Verify) and ask them to sign out + back in to pick up the claim.
I see two profiles for the same person
Auto-merge should have caught this but if it didn't: the user can run the merge themselves via Edit Profile → Connect Google / Add phone — the conflict dialog will offer it. If they can't, do it manually (see Merging duplicate accounts).
A cause shows 0 members but I know people picked it
The members count is a live array-contains query, so it's never stale. If it's 0, double-check the cause key matches what's in users.causes[]. If they drift (rare), run npm run migrate-batch-schema as a one-off recount — it covers causes too.
26 Key rotation
Rotate these every 90 days. Calendar reminder recommended.
Gmail app password
- Sign in to myaccount.google.com/apppasswords as
zaheerdv@gmail.com. - Delete the existing "Faraan Alumni" app password.
- Create a new one named "Faraan Alumni" → copy the 16-char password.
- Update Secret Manager:
gcloud secrets versions add GOOGLE_APP_PASS --data-file=-then paste → Ctrl+D. Or via the Cloud Console UI. - Disable the previous version of the secret.
- Cloud Functions pick up the new password on their next cold start (~10 min) or redeploy:
npm run deploy:fns.
Firebase Admin service account JSON
This is the faraan-alumni-network-firebase-adminsdk-*.json file used by the dev-machine scripts.
- Open Cloud Console → IAM → Service Accounts.
- Find
firebase-adminsdk-*@faraan-alumni-network.iam.gserviceaccount.com→ Keys tab. - Delete the old key. Generate a new JSON key.
- Save it to the project root with the same filename pattern (it's gitignored + hosting-ignored).
- Distribute to teammates who run scripts.
App Check (reCAPTCHA Enterprise)
App Check secrets are managed by Google — they auto-rotate. No action needed. To verify enforcement: npm run enable-appcheck -- --enforce from dev. Currently App Check is configured but not yet enforced on the Firebase APIs.
audit_log for any unauthorised admin actions in the window between leak and revocation.
27 Backups & exports
No automatic backups today. Manual procedures + recommended schedule.
Recommended schedule
| Cadence | What | Where |
|---|---|---|
| Weekly | Firestore export of users, memories, initiatives, causes | GCS bucket |
| Monthly | Full Firestore export | GCS bucket → cold storage |
| Quarterly | Auth user export (UIDs + phone/email mappings) | Encrypted local archive |
How to do a one-off Firestore export
gcloud firestore export gs://faraan-alumni-backups/$(date +%Y%m%d) \
--project=faraan-alumni-network
Requires the faraan-alumni-backups GCS bucket to exist (one-time setup) + the service account to have the Cloud Datastore Import Export Admin role.
Restore
gcloud firestore import gs://faraan-alumni-backups/20260601 \
--project=faraan-alumni-network
User export (GDPR-style)
For a single user requesting their data: pull their users/{uid} doc, their memories (where authorId = uid), their contributions (collectionGroup), and their notifications inbox. Email as a JSON file. There's no UI for this yet — use the dev SDK directly.
28 Glossary
- UID
- Unique ID assigned by Firebase Auth on first sign-in. Immutable. Used everywhere as the canonical reference to a user.
- Custom claim
- A key/value pair Firebase Auth carries inside the user's ID token. Server-enforced.
role,superAdmin,verifiedare ours. - OTP
- One-Time Password — the 6-digit SMS code Firebase sends during phone sign-in.
- FCM
- Firebase Cloud Messaging — Google's push-notification pipe. Each device that grants permission registers a token; we send pushes to those tokens.
- App Check
- Verifies that requests to Firebase APIs come from your real app, not bots. Backed by reCAPTCHA Enterprise SCORE. Configured but not yet enforced.
- PWA
- Progressive Web App. The site can be "installed" on a phone home screen and behaves like a native app (service worker, offline cache, push). Faraan Alumni is a PWA.
- Service worker
- A background JS file (sw.js) the browser runs to cache assets + intercept fetches. Bumping
VERSIONin sw.js forces installed apps to pick up new code on next load. - Convener
- Operational lead for one of the 12 causes. Firestore-only, NOT a custom claim.
- E.164
- International phone-number format:
+followed by country code and number, no spaces.+919876543210. Required everywhere phones are stored. - SSLC / PUC
- Indian school levels — SSLC = Class 10 / Secondary School Leaving Certificate. PUC = Class 11–12 / Pre-University Course. The app distinguishes them as separate batch types.
Found something not covered here? Ask the Charity Head (currently Dr Shakeel — +91 96634 14714) or open a question with the dev team.