Imported from SuhanVerse/EdumentX (
AGENTS.md). Install upstream withnpx skills add SuhanVerse/EdumentX. Copyright stays with the author.
EdumentX System Directives
You are an expert React Native + NativeWind engineer building EdumentX.
Architectural Rules
- Use standard React Native primitives (
View,Text,Pressable,TextInput,ScrollView) for layout — apply styling through NativeWindclassNameprops, not inlinestyle={{}}objects. - Never use Tamagui. All
@tamagui/*packages andtamagui.config.tshave been removed. Do not reintroduce them. - Never hardcode hex colors. Always use design tokens defined in
tailwind.config.js(e.g.,bg-night,text-amber,border-border). The narrow exceptions are SVG illustrations (src/components/illustrations/*) wherereact-native-svgprimitives need raw hex — those consumesrc/constants/colors.ts. Enforced by five custom ESLint rules (eslint-rules/design-tokens.js, registered ineslint.config.js, regression-tested bynpm run test:lint-rules):no-raw-hex-placeholderrejects raw hex inplaceholderTextColor;no-raw-hex-color-proprejects raw hex incolor="#…"props (Ionicons, ActivityIndicator, …);no-raw-hex-inline-colorrejects raw hex in inline-stylebackgroundColor/border*Color(pure black#000000scrims are the one exempt convention — there's no black token andshadowColoris likewise out of scope);no-non-token-radiusrejects anyclassNameradius that isn't a token —rounded-xs|sm|md|card|lg|xl|hero|pill(with-t/-b/-l/-rpartials). Tailwind defaults (rounded-2xl,rounded-full,rounded-t-3xl) and arbitraryrounded-[…]values are violations becausetheme.extendleaves them off the documented scale; andno-non-token-color-classrejects anyclassNamethat uses a Tailwind default palette shade (text-slate-300,bg-blue-500,border-t-slate-300, …) — those resolve to framework defaults, not the palette intailwind.config.js. Dark surfaces must usetext-glass-secondary/text-glass-muted(the light-modetext-text-secondary/text-text-mutedmid-grays fail contrast onnight); the rule's className checker also inspects string literals nested inside template expressions (ternaries inclassName={`…`}), so classes hidden in conditionals can't slip past. Every hex value must map tosrc/constants/colors.ts(e.g.#2F5D50→colors.brand.primary,#E5A03B→colors.brand.accent,#3F8A5A→colors.brand.verification,#4A7FA5→colors.brand.ai,#C1503D→colors.semantic.danger,#6B7280→colors.text.muted,#0F172A→colors.text.primary,#FFFFFF→colors.text.inverse). - Reference Sandbox: The folder
Documentation/98-Reference-BasoBas/contains a React web app. You may study its UX logic, component composition, and layout structures, but you MUST translate those concepts into pure React Native + NativeWind code before writing anything to oursrc/app/orsrc/components/directories. - Auth is native Firebase Auth. Use
@react-native-firebase/auth(getAuth(getApp()),createUserWithEmailAndPassword,signInWithEmailAndPassword,signInWithCredential, etc.) and@react-native-google-signin/google-signinfor Google Sign-In. The unified entry point issrc/screens/auth/EmailSignUp.tsx— it hosts both the Email + Password form and the "Continue with Google" button. Do NOT reintroduce Clerk; a one-day pivot to Clerk (June 20) was reverted the next day (Clerk'sintegration_firebasetemplate is discontinued for new accounts). The Clerk-pivot history is archived underDocumentation/99-Archive/2026-06-21-clerk-revert/. - No-cost at demo scale (Blaze, Aug 2026). Billing is now enabled (Google Cloud linked, card on file, prepaid balance), but the rule is that real usage must stay inside free quotas so the bill stays $0: Firestore 50K reads / 20K writes / 20K deletes per day, Cloud Functions 2M invocations/mo, Cloud Storage 5 GB, Google Maps Platform 10K free calls/mo per SKU. A Google Cloud budget alert (~$5) and spend caps on Cloud Functions guard the project. Never introduce OpenAI / Anthropic / Cohere (paid keys only), Mapbox, Algolia, or live merchant credentials — eSewa stays sandbox-only (
EPAYTEST); a real merchant key requires a separate, documented decision + registered business. Map tiles come from nativeexpo-maps(Google on Android viaEXPO_PUBLIC_GOOGLE_MAPS_API_KEY, Apple on iOS) — NOT OSMUrlTile. Geocoding defaults to Nominatim (keyless); Google Geocoding/Places is optional within its 10K/mo free quota with a restricted key. Object storage stays in Supabase Storage (1 GB free; Firebase Storage's 5 GB is available but not migrated). Distance / KNN / matching math runs client-side. Serverless is split: Supabase Edge Functions (chat RAG, eSewa order signing, verify-identity) + Firebase Cloud Functions (Blaze no-cost quota;functions/scaffolded for Firestore-centric work — scheduled sweeps, verification triggers, custom claims). The RAG chatbot uses Groq or HuggingFace Serverless Inference (free dev tier). Before adding any new dependency or paid-tier service, updateDocumentation/01-Architecture/ARCHITECTURE.md§0 with a row justifying it with quota math + spend-cap notes. If it can't be justified at $0 for demo traffic, replace the feature or remove it.
Translation Protocol
When referencing the BasoBas project (found in Documentation/98-Reference-BasoBas/):
- DO NOT copy BasoBas CSS, Tailwind classes, or standard React Native components wholesale — study the ideas, not the markup.
- ONLY study their UX flow, navigation logic, and layout composition.
- ALWAYS rewrite their UI logic into our EdumentX design system using React Native primitives + NativeWind classes (
bg-night,text-amber,rounded-card,p-4). - Read
Documentation/98-Reference-BasoBas/ANALYSIS.mdfirst for the per-file translation map.
Current State
Living snapshot — each block below carries its own date. The auth narrative is the June 21, 2026 snapshot (the Clerk-revert pivot); the dashboard / enrollments / testing blocks are Aug 2026.
Phase 2 (Native Firebase Auth) — Complete (June 21, 2026 snapshot)
Identity lives in native Firebase Auth. Two free methods are wired up:
Email + Password (via createUserWithEmailAndPassword +
sendEmailVerification) and Google Sign-In (via
@react-native-google-signin/google-signin + signInWithCredential).
There is no Clerk, no SMS, and no phone provider — Firebase's SMS OTP
requires the paid Blaze plan, and a one-day Clerk pivot (June 20) was
reverted after Clerk discontinued their integration_firebase template
for new accounts.
The Clerk-pivot history is archived under
Documentation/99-Archive/2026-06-21-clerk-revert/.
Auth flow (Email + Password / Google):
src/screens/auth/EmailSignUp.tsx— single auth entry screen with a "Sign up" / "Log in" toggle. On signup, callssignUpWithEmail(...)and flips to a "check your inbox" pending panel that hosts the "I've verified — continue" button. That button is the only place the app callsauth.currentUser.reload()— without it, the cachedUserobject'semailVerifiedflag stays stale and the layout guard refuses to advance. This was Bug #4 in the June 21 audit.src/screens/auth/EmailSignUp.tsx— also hosts the "Continue with Google" button, which callssignInWithGoogle(). Google users are auto-verified by Google and skip the inbox step entirely.
Auth-flow routing ("Source of Truth"):
src/app/_layout.tsx runs a 5-step redirect tree on every render where
user / role / segments change:
- Wait for the root navigator to mount (
useRootNavigationState()). !user→/email-signup.user && !emailVerified && password-provider→/email-signup(the "check your inbox" panel; user can also sit on the screen freely).user && verified && !role→/role-selection(first-time signup, no doc yet).user && verified && role→ matching dashboard.
Step 5 was Bug #1 ("Amnesia Login Loop") — an existing user with a
populated users/{uid}.role was being sent back to
/role-selection on login. The fix: read the role inside the
onAuthStateChanged callback (not in a separate effect) and write
it to the Zustand store before the redirect effect runs, so the
guard sees the populated role on its first pass.
Phase flow (after auth):
email-signup ← single auth surface (signup or login)
↓ (verified, role still null)
role-selection ← pick student or tutor (writes role to Firestore)
↓ (role set)
profile-student ← collect username, phone, grade, subjects, location
or profile-tutor ← collect username, phone, headline, bio, monthly rate
↓ (profile subcollection written)
student-home ← live dashboard (reads users/{uid} + profile subdoc)
or tutor-home
Files at play in this flow:
- ✅
src/app/_layout.tsx(Source-of-Truth routing + onAuthStateChanged) - ✅
src/screens/auth/EmailSignUp.tsx(signup + login + Google + "I've verified — continue") - ✅
src/screens/auth/RoleSelection.tsx(writes AUTH METADATA ONLY — role is deliberately NOT written here; it lands atomically inside the profile-screen batches:StudentProfileScreen/TutorProfileScreen. Routes to /profile-* for new users or straight to the dashboard on re-login.) - ✅
src/screens/auth/StudentProfileScreen.tsx(writesusers/{uid}/studentProfile/default) - ✅
src/screens/auth/TutorProfileScreen.tsx(writesusers/{uid}/tutorProfile/default, usesmonthlyRateNpr) - ✅
src/screens/student/student_home.tsx(liveonSnapshotreads, shows realfullName+locationLabel) - ✅
src/screens/tutor/tutor_home.tsx(liveonSnapshotreads, shows realfullName+ verified flag) - ✅
src/services/firebase/authService.ts(modular RNFirebase API, Google Sign-In, no OTP) - ✅
src/components/forms/LocationField.tsx(MIN_CITY_LENGTH = 3, not 2 — the location-field bug from the June 21 audit) - ✅
src/lib/registration.ts(no more Clerk-pivot type-level placeholders)
Files removed in this pivot:
- ❌
src/screens/auth/PhoneEntryScreen.tsx - ❌
src/screens/auth/OtpVerify.tsx - ❌
src/screens/auth/Password.tsx - ❌
src/app/phone-entry.tsx - ❌
src/app/otpverify.tsx - ❌
src/app/create_password.tsx - ❌
src/components/ClerkFirebaseBridge.tsx - ❌
@clerk/clerk-expo,expo-crypto,expo-secure-store,expo-web-browser,expo-application - ❌
Documentation/04-Firebase/Clerk_Integration.md(moved to archive)
Build pipeline (expo SDK 54, NativeWind 4.2.x, native Firebase) — June 21 snapshot, still current as of Aug 2026:
babel.config.jsusesbabel-preset-expowithjsxImportSource: 'nativewind'+nativewind/babel. No reanimated/Tamagui plugins.metro.config.jsusesgetDefaultConfig(__dirname, { isCSSEnabled: true })only. TheDocumentation/98-Reference-BasoBas/folder is excluded viablockList.src/app/_layout.tsxmounts<GestureHandlerRootView>→<SafeAreaProvider>→<Stack>. The Stack always renders (no conditional tree returns — that breaks expo-router child tracking). The loading overlay sits on top viapointerEvents="none".users/{uid}is the only Firestore root document the auth flow touches. Role + verified flag drive the redirect guard.useRootNavigationState()gate preventsrouter.replace()from firing before the navigator mounts.
Tutor dashboard + enrollments (Aug 2026):
src/screens/tutor/TutorHome.tsxis fully live: capacity + active-students + monthly rate come fromusers/{uid}/tutorProfile/defaultviaonSnapshot; **Avg rating- Reviews** are derived from the
reviews/{tutorUid}/reviewscollection (subscribeReviews); Response rate = share of responded requests across the full request history; Monthly revenue = live roster ×monthlyRateNpr(zero-budget earnings proxy — no session billing). Pending requests come fromenrollmentRequests/{tutorUid}/requestsfiltered topending; "today's sessions" are derived from the live roster (enrollments/{tutorUid}/roster) — there is NOsessionscollection (deriveTodaySessionsinservices/enrollments/derived.tsmatches active enrollments whoseslotKeyday == today's Asia/Kathmandu weekday AND whose[startDate, endDate]window contains today).
- Reviews** are derived from the
src/screens/tutor/TutorInbox.tsxis live via the sharedEnrollmentRequestCard; accept opens a slot-picker sheet (AvailabilityTimeListfed bysubscribeAvailability+subscribeEnrollments+subscribeBatches) and callsacceptRequest(requires a concreteslotKey); decline callsdeclineRequest(hard-deletes the request).- slotKey format is
"<day>:<slot>"(e.g.mon:5-7) — built byslotKey()/ parsed byparseSlotKeyinservices/enrollments/types.ts. A malformed key (e.g.mon-5-7) returnsnulland is silently excluded from derived views, so writers (tests, seeds, scripts) MUST use the colon format.derived.tslogs a warn-once (warnBadSlotKey) for bad keys. - Group Batches is live —
services/batches/(BatchesRepositoryinterface + Firebase/Mock impls +dataSourceselector gated byEXPO_PUBLIC_USE_MOCK_DATA).BatchCreation.tsxfetches live batches + per-batch members, its student picker reads the live active roster (enrollments/{tutorUid}/roster), and the 3-step wizard creates batches via the existingcreateBatchtransaction. Member add/remove +endBatchuse direct paths (no collectionGroup scans). Rules verified intest:rules. - Platform Statistics is live — the old
PlatformStatistics.tsxwas merged intoAdminHome.tsx(Aug 15): the admin Home screen shows a live KPI grid viagetCountFromServer(users, tutors, approved tutors, pending enrollment requests) with loading/error/retry states; the fabricatedMOCK_ADMIN_STATSmodule is deleted. The pending-requests KPI needs therequests.statusCOLLECTION_GROUP index (deployed + READY, verified Aug 16). - In-app messaging is live —
services/messages/(messages domain:MessagesRepositoryinterface + Firebase/Mock impls +dataSourceselector). Schema:conversations/{conversationId}with amessagessubcollection;conversationIdis the sorted participant pair joined by__(conversationKey), so either side addresses the same thread without a lookup. Each side self-writes its own displaymeta(name/avatar) so the hub needs no cross-user reads. Routes:/chat(paramspeerId, optionalpeerName/peerAvatar) +/messageshub. Entries: the student enrollment card's "Message" CTA, Messages header buttons on both dashboards, and TutorHome's quick actions. Rules caveat: list-membership ops (in/hasAny) and bareauthfail the local emulator with "Null value error" — the conversation rules use scalarparticipantA/participantB==request.auth.uidchecks instead (see the comment infirebase/firestore.rules). The hub inbox query therefore filters on the scalars with a compositeor()(NOTarray-containsonparticipants— that shape is unprovable against the scalar rules and 403'd on every render until the Aug 2026 fix;messagesRulesTest.mjs§1b locks it in). - Saved tutors is live —
services/savedTutors/domain. Storage: asavedTutorsuid-key map onusers/{uid}/studentProfile/default; the heart onTutorDetailsScreenand the/saved-tutorslist (TutorCard wide cards from the live tutor feed) both subscribe to it, andtoggleSavedTutorflips a single key withdeleteField()in onesetDoc(merge). No rules change was needed — the owner subcollection wildcard (match /users/{userId}/{subcollection}/ {document=**}) already covers it. - Help & support is live —
/help-support(FAQ + mailto). TutorDetailsScreen share uses the nativeShare.share; the group batch pricing card routes to/chat; session CTAs open the enroll sheet. All "Coming soon" alerts in student/tutor profiles are gone. The payouts + payment-methods feature was removed (Aug 15) —services/paymentMethods/,PaymentMethodForm,/payouts, and/payment-methodswere deleted; the "Monthly revenue" dashboard metric (roster × rate) went with it.
Search-visibility flag (Aug 2026):
- The tutor dashboard's "Available for new students / Hidden from
search" toggle writes
tutors/{uid}.isAvailableForNewStudents(viasetTutorAvailabilityinsrc/lib/tutor/firestoreTutorService.ts). Discovery (subscribeTutors— backs both StudentHome and MapSearch) filterswhere("isAvailableForNewStudents", "==", true). - Aug 25 — availability gate: discovery ALSO filters
where("hasAvailability", "==", true). The flag is mirrored bysaveAvailability(any enabled slot ⇒ true), defaultsfalseon admin approval, is owner-writable (bool-only carve-out in rules), and slot-less tutors are hidden from students entirely. Backfill:npm run backfill:has-availability -- --write. tutors/{uid}is otherwise admin-write-only; the rules carve-out (firebase/firestore.rules) lets the OWNER update only that flag (+updatedAt) viarequest.resource.data.diff(resource.data).affectedKeys().hasOnly(...).- Live ratings on the cards — the discovery docs only mirror
rating/reviewCountat approval time (and are admin-write-only, so students can't bump them), soFirebaseTutorRepositorysubscribeTutorsoverlays live aggregates from a singlecollectionGroup("reviews")subscription (status == "active" docs, grouped bytutorUid) onto every listing. One listener covers all tutors; no index needed (no filters on the query).TutorDetailsScreendoes the same per-tutor: it subscribes toreviews/{uid}/reviewsand merges rating, count, star breakdown, category averages and the review LIST over the fetched profile (mergeLiveReviewsin the screen), so the Reviews & Ratings section shows real reviews instead of the profile doc's mirrored aggregates (which never included the list).Reviewcarries an optionalcategoryRatings(mapped from the raw doc) to feed the category averages. Note: the local emulator'srunQuerycan't docollectionGroupId(400) — thereviewsListRulesTest.mjssuite verifies the underlyinglistrule via the document-parent query form instead. NOTE:differsOnlyFromis NOT a real rules function — it fails closed; don't reintroduce it. - Newly-approved tutors default to visible (the admin approval mirror
writes
isAvailableForNewStudents: true); the backfill script (npm run backfill:tutor-availability) defaults legacy docs totrueso the strict filter doesn't hide them.
Aug 25 session (security overhaul + prod-rule divergences + UX batch):
- Rules authority model changed.
isAdmin()= Firebase Auth custom claimadmin:true(minted byseedAdmin.ts, which also keeps the auditableadmins/{uid}doc). The oldusers.role=="admin"clause was a live privilege-escalation (owner could patch own role). Legacy admins MUST re-runnpm run seed:adminonce, then sign out/in. - users/{uid} owner writes: forbid-list model via the
unchanged(k)rules helper (presence-parity + value-equality; dynamicdata[k]indexing). Immutable to owners:status,deletedAt,subscriptionTier,subscriptionExpiresAt; role transitions limited to student/tutor; uid immutable-if-touched; uid-less maintenance patches legal. - Pro tier is server-granted only. Tier keys are pinned on BOTH
users/{uid}/tutorProfile/default(+ wildcard subdoc path) ANDtutors/{uid}. Grant =create-esewa-orderedge fn service-role REST write AFTER verification (grant-before-COMPLETE ordering). ClientapplyProGrantwas deleted; demo grant is mock-mode-only (applyDemoProGrant). - ⚠️ Emulator-vs-production rules divergences (cost us hours):
diff()evaluates as no-change on some masked/unmasked patches in the emulator → diff-based guards are UNPROVABLE locally. Useunchanged()instead.exists()on an absent doc THROWS locally (fine in prod); a throwing statement poisons sibling statements.- Reading
resource.dataduring CREATE throws in PRODUCTION but the emulator treats it as{}— this denied every FIRST-TIME subdoc save on real devices (StudentProfileScreen). Verb-split any write rule that must cover creates. - Unmasked REST PATCH = full REPLACE (wipes untouched fields);
RN SDK always sends masks. Test harnesses MUST send
updateMask.fieldPaths. request.auth.token.get('admin', false)— plain indexing of an absent claim throws; always.get(key, default). Live probe tool:node scripts/auditLiveReads.mjs(zero-write).
- Signed document URLs. PII scans are ONLY viewable via 10-min
signed URLs from the
verification-doc-urledge fn (owner-or-admin;demokind any-signed-in).getVerificationDocPublicUrlreturns null for PII kinds; UI usesuseSignedDocUrl(sep-aware cache-bust — never raw-append?t=after a signed URL's?token=). Demo videos moved to PUBLIC buckettutor-demo-videos. Migrationsupabase/migrations/016_*.sql(v2) flips the bucket private; storage policies are BUCKET-scoped anon writes because Supabaseauth.uid()is always NULL under Firebase auth. - Edge functions deploy with
--no-verify-jwt— the project uses asymmetric JWT signing keys, so the gateway 401s Firebase tokens otherwise (this silently broke chat→mock fallback + AI pre-screen). Storage signing needs secretSTORAGE_SERVICE_JWT(legacy JWT key) because the platform-injectedSUPABASE_SERVICE_ROLE_KEYmay be the opaquesb_secret_…format that storage rejects ("Invalid Compact JWS"). - Groq re-rank goes through the
groq-proxyedge fn (JWT + per-user rate limit). Client Groq/HF keys are dead — never addEXPO_PUBLIC_*LLM keys back. - Push on new message: Gen1 Cloud Function
onNewChatMessage(Node 20, asia-south1 — gen1 rejects node24; gen2 needed Eventarc IAM we skipped) readsusers/{uid}.pushTokensand POSTs to the Expo push API (free, no FCM SDK). firebase.json predeploy lint removed (ESLint8/9 --ext conflict); root lint covers functions/src. - Chat hub identity:
resolveOwnIdentityno longer negative- caches empty names; hub enriches TUTOR peers via publictutors/{uid}reads (emit-then-enrich, local-only). - Student pending-tab requests feed now backfills tutor
name/avatar (
EnrollmentRequest.tutorName/tutorAvatar), and the inbox accept-sheet preselectspickedSlotKeys[0]. - UI: profile-setup screens are light variant; credentials copy
tightened; PendingReview checklist pruned (no phase jargon);
TutorHome profile-completion computed client-side (doc field was
never written); chat KAV behavior=undefined on Android +
softwareKeyboardLayoutMode:"resize".
Aug 16, 2026 session (rules deploy + Phase 1–5 polish):
- Deployed to
edumentx-dev(firebase deploy --only firestore:rules,firestore:indexes, verified by all five live smoke suites): thereviewsaggregate carve-out (a reviewer may update ONLY the tutor'srating/reviewCount/categoryRatings/reviewBreakdown/updatedAt—submitReview's transaction writes those in the same tx as the review doc), the roster update rule keyed on the PATH owner (legacy rows without atutorUidfield expire cleanly), and therequests.statusCOLLECTION_GROUP index (AdminHome pending-requests KPI). sweepExpiredEnrollmentsroot cause: the STUDENT screens (TutorDetailsScreen,EnrollmentFormScreen) subscribed to a tutor's roster and the sweep ran AS THE STUDENT. Those call sites now pass{ runSweep: false }— the sweep only runs on the tutor's own subscription.- Batch "404" fixed: the student detail screen now reads the
batch by DIRECT PATH (
subscribeBatch— status-agnostic) instead of the active-only marketplace feed, so ended batches opened from My Enrollments render instead of "not found". - Live student avatars: the tutor-facing roster/request feeds
backfill blank names/avatars from
users/{uid}/studentProfile/default(emit-then-backfill, same pattern as the student view's tutor identity). - UI: chat list + composer sit in one
KeyboardAvoidingView; TutorDetails CTA flips to a disabled "Already enrolled" state; admin screens converted to the light theme; MyEnrollments tabs got 24px gutters; BrowseBatches keys by${tutorUid}-${batchId}(duplicateseed-1crash); smoke scripts use timestamped ids +try/finallycleanup (andsmokeTestTutorDetailsnowrecursiveDeletes — its parent-only cleanup was reseedingseed-1every run; the 8 legacy leftovers were scrubbed from live).
Test commands (Aug 2026):
npm run test:derived— 64 unit tests over every pure helper inservices/enrollments/derived.ts+types.ts(KTM date helpers, slotKey parsing, today-sessions derivation, booked-map, capacity counts + availability-draft helpers, malformed-slotKey guard).npm run test:lint-rules— node:test suite for the five design-token ESLint rules above, so a regression ineslint-rules/design-tokens.jsfails CI instead of slipping through (21 tests).npm run test:rules— TWO stages: (1)test:rules:deployedfetches the latest released ruleset from the Firebase Rules API and fails on drift vs localfirebase/firestore.rules(needsGOOGLE_APPLICATION_CREDENTIALS); (2) boots the Firestore emulator with the LOCAL rules and exercises the security rules via the REST API across SEVEN suites: thetutors/{uid}availability carve-out, the batches collections, the acceptRequest transaction paths (incl. the legacy no-counters profile carve-out + the student fast-forwardendDatecarve-out + the §7 legacy-roster-row sweep the auto-expiry sweep needs), the conversations/messages participant gates (incl. the hubor()scalar list query + the message read-receiptstatus/readAtcarve-out), the reviews list rules (incl. the §4b reviewer-aggregate-write carve-out), the removeEnrollment cascade, and the tutorDetails direct-path reads.
CI (.github/workflows/ci.yml, green on develop): check
(typecheck, lint incl. the five design-token rules, test:derived,
test:lint-rules) → rules (seven emulator suites, Java 21) →
rules-deployed (deployed-vs-local drift + FIVE live smoke
suites — enrollments, reviews, batches-browse, messages,
tutor-details — that act as real signed-in clients against the
deployed rules, gated on the GCP_SA_KEY + FIREBASE_WEB_API_KEY
secrets). Trigger on main pushes + manual dispatch; the
credentialed job intentionally skips PRs (rules-changing PRs
would show drift until deployed).
Pending deliverables (as of Aug 16, 2026):
- ✅ Firestore rules + indexes deployed (see the Aug 16 block
above — the review/sweep rule fixes and the
requests.statusCOLLECTION_GROUP index are LIVE and verified by all five smoke suites). - Rebuild the EAS dev client with the updated native deps (Clerk
packages removed,
@react-native-google-signin/google-signinrestored) — the only remaining item.
