Pathfinder is a business operations platform for leads, school applications, dashboards, notifications, archive, personnel, and timesheets.
- last updated: 2026-04-18
- this revision aligns docs to the current hardened baseline and portfolio sanitization pass (local-only media assets, generic project endpoints, and removed school website/source links from seed data).
- Next.js 16 App Router (
src/app) - React + TypeScript
- Firebase client SDK (Auth, Firestore, Storage)
- Firebase Admin SDK (server routes)
- Cloudflare Turnstile (login verification)
- Playwright (smoke E2E)
- Cloud Run deployment via Cloud Build
- Public login route:
src/app/(public)/login/page.tsx - Protected route group:
src/app/(protected) - Protected redirect:
src/app/(protected)/page.tsx->/navigation - Protected shell route:
src/app/(protected)/[...slug]/page.tsx - App startup chain:
src/app/client-only.tsxsrc/app/app-root.tsxsrc/components/app/App.tsx
The app is a protected SPA shell; client-side view switching is handled by useAppUiState + AppView.
Image sources are centralized so branding/assets can be replaced from one source:
src/config/imageLinks.ts- global branding assets (logo, favicon, background, mascot) served from local
public/assets/branding/* - shared UI imagery (headers, transfer icon, status runner, podium medals) served from local
public/assets/ui/*
- global branding assets (logo, favicon, background, mascot) served from local
src/config/schoolImageLinks.ts- school-logo placeholder registry (local-only; no external logo host dependency)
src/components/common/components/SchoolLogo.tsx- resolves school logos from centralized registry first, then initials fallback (no external Clearbit fallback)
- Dashboard
- Profiles (Leads)
- School Applications
- Application Detail
- Education Providers
- Timesheet
- Personnel
- Notifications
- Archive
- Profile
Role helpers are centralized in src/utils/roles.ts.
Primary roles:
DeveloperOperationsBranch ManagerEducation ConsultantAdministrative StaffSatellite Office Staff
High-level behavior:
Developer/Operations: global data visibility.Branch Manager/Administrative Staff: branch-scoped visibility for leads/submissions/applications.Education Consultant: assigned-only visibility for lead/application actions where required.Satellite Office Staff: restricted navigation (no applications, no education providers, no personnel), branch-scoped where applicable.- Archive access policy:
- Archive page visibility (
canViewArchiveRole):Developer,Operations,Branch Manager,Marketing,Administrative Staff, andEducation Consultant. - Yearly archive execution (
isArchiveViewerRole/canRunYearlyArchiveRole):Developer,Operations, andBranch Manageronly.
- Archive page visibility (
Authoritative enforcement:
- Frontend scope/query logic:
src/components/app/hooks/useFirestoreData.tsand feature-level access helpers. - Firestore rules:
firestore.rules(must remain aligned with role logic).
- Leads + Student Modal
- Admin/consultation actions
- Notes/logs
- Endorsement + assignment flows
- Application creation from lead context
- School Applications
- Status timeline/history
- Milestone notifications
- Role-aware edit permissions
- Dashboard
- Role-based widget layouts
- Application Funnel filters (Branch, Month, Quarter, Staff) on default dashboards
- Application Funnel filters (Month, Quarter) on Education Consultant dashboards
- Month/Quarter mutual normalization and quarter-scoped month options
- Milestone-safe metrics (current status + status history)
- Filter-scoped widgets: Target vs Actual, Leads by Branch, Top Country, Preferred Course, Top Lead Sources
- Global ranking widgets: Top Visa Grant Counsellors and Top Staff Referrers remain unscoped
- Heatmap origin filter:
Leads Origin(default): plots locations from scoped leadsApplication Origin: plots locations for leads that have applications in the active funnel scope- location source is lead submission
currentLocation(fallbackreferredStaffBranch)
- Education Consultant widget invariants:
My Leadsshows assigned leads for the current month (Leads Assigned To You This Month)My Visa Pipelinebars follow selected month/quarter, whileVisa Grant Ratestays up-to-date from overall assigned visa outcomes
- Heatmap geocoding endpoint
- PDF/Excel report exports
- AI-generated insights for exported reports
- Archive
- View access is broader for operations workflows (see role model above)
- Yearly rollover archives completed records
- Yearly rollover execution remains restricted to
Developer,Operations, andBranch Manager - Keeps in-progress/unfinished applications active
- Manual + guarded automatic trigger flow
- Timesheet
- Time in/out, lunch, leave, offset
- Approval workflows
- Auto-plot from approved requests
- Offset-use duration excludes lunch overlap (
12:00-13:00) from consumed offset credits - Offset-use start-time dropdown excludes
12:00 - Approved offset-use requests append standardized remark lines and apply boundary checkpoints only when crossed:
09:00-> autotimeIn12:00-> auto lunch-out (lunchStart)13:00-> auto lunch-in (lunchEnd)17:00-> autotimeOut
- Auto-plot logic does not overwrite existing manual punches
- Leave policy runtime logic:
- accrual:
2leave credits/month - annual cap:
24 - carryover cap into new year:
5
- accrual:
- Offset policy runtime logic:
- offset-use request minimum is
1hour (whole-hour usage path) - offset balances reset at new-year boundary in Manila timezone
- offset-use request minimum is
admin_ph@example.comgets a Timesheet Download tab that exports one workbook with one sheet per staff member
- Notifications
- Persistent Firestore-backed notifications
- Action/event-key formatting and approval flows
POST /api/turnstile/verify- Verifies Cloudflare Turnstile token.
POST /api/personnel/create- Admin SDK endpoint; role-gated personnel + auth creation.
POST /api/personnel/delete- Admin SDK endpoint; role-gated personnel + auth deletion.
POST /api/personnel/sync-balances- Admin SDK-backed balance reconciliation endpoint for leave/offset fields.
- bearer-token protected with route-level rate limiting.
POST /api/personnel/force-password-reset- Admin SDK-backed forced-password-reset completion endpoint.
- clears
passwordNeedsResetonly after server-side password update. - bearer-token protected with route-level rate limiting.
POST /api/notifications/create- Admin SDK-backed cross-user notification dispatch endpoint.
- bearer-token protected with role checks + route-level rate limiting.
POST /api/geocode/locations- Philippines-focused geocoding with fallback aliases for dashboard heatmap.
- bearer-token protected with route-level rate limiting and bounded cache guards.
GET /api/dashboard/top-visa-grant-counsellors- Aggregates visa grants (non-archived applications).
GET /api/dashboard/top-staff-referrers- Aggregates staff referral counts from leads.
GET /api/dashboard/global-visa-approval-trend- Returns global visa approval trend data.
POST /api/dashboard/ai-report- Generates structured dashboard insights from snapshot data via OpenAI.
POST /api/archive/yearly-rollover- Archives prior-year completed records (manual token-based trigger or scheduler key).
POST /api/studynavi/sso- Generates StudyNavi SSO redirect URL using Firebase custom token.
- bearer-token protected with route-level rate limiting.
Required:
NEXT_PUBLIC_FIREBASE_API_KEYNEXT_PUBLIC_FIREBASE_AUTH_DOMAINNEXT_PUBLIC_FIREBASE_PROJECT_IDNEXT_PUBLIC_FIREBASE_STORAGE_BUCKETNEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_IDNEXT_PUBLIC_FIREBASE_APP_IDNEXT_PUBLIC_FIREBASE_MEASUREMENT_IDNEXT_PUBLIC_TURNSTILE_SITE_KEYTURNSTILE_SECRET_KEYFIREBASE_ADMIN_SDK_JSON
Feature-dependent:
OPENAI_API_KEY(required for/api/dashboard/ai-report)OPENAI_REPORT_MODEL(optional, defaultgpt-4.1-mini)NEXT_PUBLIC_FIREBASE_APPCHECK_SITE_KEY(required when App Check is enforced for Firebase Authentication /identitytoolkit)NEXT_PUBLIC_FIREBASE_APPCHECK_DEBUG_TOKEN(optional; local-only App Check debug token, blocked from production public-runtime exposure)STUDYNAVI_URL(required for/api/studynavi/sso)STUDYNAVI_ALLOWED_HOSTS(optional comma-separated hostname allowlist for/api/studynavi/sso)TURNSTILE_EXPECTED_ACTION(optional; defaults tologinand must match Cloudflare Turnstile widget action)TURNSTILE_ALLOWED_HOSTNAMES(optional comma-separated host allowlist for Turnstile server verification)ARCHIVE_JOB_KEY(optional scheduler secret for/api/archive/yearly-rollover)GEOCODING_PROVIDER(optional;googleornominatim)GOOGLE_GEOCODING_API_KEY(optional; required when forcingGEOCODING_PROVIDER=google)NEXT_PUBLIC_GOOGLE_MAPS_API_KEY(required for dashboard leads heatmap map)- can be provided as direct Cloud Run env value or secret-backed runtime binding (
pathfinder-google-maps-api-key)
- can be provided as direct Cloud Run env value or secret-backed runtime binding (
NEXT_ALLOWED_DEV_ORIGINS(optional comma-separated local dev origins for Next.js LAN access)
Reference: .env.example
- Install dependencies:
npm install - Create
.env.localfrom.env.example - Run dev server:
npm run dev
npm run check:firestore-indexesnpm run check:firebase-confignpm run check:firestore-rules-contractnpm run check:java(preflight for emulator-based rules tests)npm run typechecknpm run typecheck:api:strictnpm run lintnpm run check:max-linesnpm run check:secretsnpm run check:unused-exportsnpm run check:required-docsnpm run check:env-examplenpm run testnpm run test:rulesnpm run buildnpm run verifynpm run test:e2e:smokenpm run verify:smokecd functions && npm run lintnpm run postdeploy:checknpm run ops:check:deploy-driftnpm run ops:ensure:alertingnpm run ops:enforce:app-check -- --mode=UNENFORCED(dry-run example)npm run doctornpm run hooks:setup
Notes:
npm run test:rulesintentionally fails fast when Java is missing and prints installation guidance.npm run check:javanow enforces Java17-21(Temurin/OpenJDK), with Temurin 21 recommended.- CI
Quality Gate / rules-semanticrunsnpm run test:ruleson a Java-enabled runner (actions/setup-java, Temurin 21). - Rollout-only drift-check overrides are available when needed:
npm run ops:check:deploy-drift -- --allow-app-check-unenforcednpm run ops:check:deploy-drift -- --allow-ttl-creating
Entrypoints:
scripts/cleanup-preferred-courses.cjsscripts/import-archives-from-excel.cjsscripts/backfill-archive-lead-sync.cjsscripts/cleanup-lead-sources.cjsscripts/backfill-personnel-approval-fields.cjs
Personnel approval-field rollout:
npm run migrate:personnel-approval-fieldsruns dry-run and prints proposed updates.npm run migrate:personnel-approval-fields:verifyprints migration coverage metrics (no writes).npm run migrate:personnel-approval-fields:verify:strictexits non-zero when coverage is incomplete.npm run migrate:personnel-approval-fields:applywrites normalized approval fields.- During rollout, approver lookup intentionally merges indexed
approvalBranchKeymatches with legacy branch fallback results so partial backfill does not hide valid approvers.
Current layout:
- Keep root files as thin wrappers.
- Keep implementation in
scripts/lib/<feature>/using small focused modules (parseArgs, normalization/mapping helpers, runner). - Shared Firebase Admin bootstrap is in
scripts/config/firebase-admin-utils.cjs.
.github/workflows/quality-gate.yml- blocking gate on
mainpush/PR - runs
npm audit --omit=dev,npm run verify, semantic emulator rules tests (npm run test:rules),npm run test:e2e:smoke, andfunctionslint verification
- blocking gate on
.github/workflows/security-gate.yml- security workflow (secret scan, dependency review, npm audit artifact, unused-exports report)
- dependency/audit policy aligned to fail on
moderate+severity
.github/workflows/codeql.yml- CodeQL SAST scan for JavaScript/TypeScript on
mainpush/PR and weekly schedule
- CodeQL SAST scan for JavaScript/TypeScript on
.github/workflows/postdeploy-uptime-check.yml- scheduled/manual production uptime checks against
https://your-app.example.com/
- scheduled/manual production uptime checks against
.github/workflows/ops-drift-verification.yml- scheduled/manual production drift assertion for Cloud Run env/secrets/IAM, Firestore TTL, App Check service enforcement, and monitoring baseline integrity
- requires GitHub repository secrets:
GCP_WIF_PROVIDER,GCP_WIF_SERVICE_ACCOUNT - optional repo vars:
PATHFINDER_GCP_PROJECT,PATHFINDER_GCP_REGION,PATHFINDER_CLOUD_RUN_SERVICE,PATHFINDER_BASE_URL
.github/workflows/dependabot-triage.yml- auto-labels Dependabot PRs for triage (
dependencies,automated,needs-triage, plusci/regressionwhen applicable)
- auto-labels Dependabot PRs for triage (
.github/workflows/label-sync.yml- syncs repository labels from
.github/labels.yml
- syncs repository labels from
.github/workflows/stale.yml- marks/closes stale issues and pull requests for hygiene
- all third-party GitHub Actions in workflow files are commit-SHA pinned.
Current dependency posture (2026-04-08):
- app runtime pinned to
next@16.1.7/eslint-config-next@16.1.7 - dependency hardening includes overrides for
node-forge,fast-xml-parser,dompurify,brace-expansion,flatted,picomatch,yaml, andlodash npm audit --omit=devresidual risk is low-severity transitive Firebase/Google chain (@tootallnate/once)- full
npm audit(including dev) is now reduced to low-severity transitive Firebase/Google chain advisories only. - unused-export guardrail uses an actionable budget ceiling (
UNUSED_EXPORTS_MAX, default71) to prevent regression growth
- Rules file:
firestore.rules - Indexes file:
firestore.indexes.json - Storage rules file:
storage.rules - Firebase deployment config:
firebase.json(explicitfirestore.rules+storage.rulesbindings) - Index drift helper:
npm run check:firestore-indexes - Rules contract helper:
npm run check:firestore-rules-contract - Semantic rules helper:
npm run test:rules - Deploy drift guardrail:
npm run ops:check:deploy-drift- strict defaults verify:
- Firestore TTL state
ACTIVEon__rateLimits.expiresAt - App Check enforcement state for
firestore.googleapis.com,firebasestorage.googleapis.com,identitytoolkit.googleapis.com - uptime check host/path integrity (
[Pathfinder] prod uptimeon/login) - required alert policies enabled and wired with notification channels
- Firestore TTL state
- rollout escape hatches:
--allow-app-check-unenforced--allow-ttl-creating
- strict defaults verify:
Production controls aligned to this repo:
- Firestore TTL:
- collection group:
__rateLimits - field:
expiresAt - state target:
ACTIVE
- collection group:
- App Check enforcement:
- service targets:
firestore.googleapis.com,firebasestorage.googleapis.com,identitytoolkit.googleapis.com - enforce with:
npm run ops:enforce:app-check -- --apply - safe validation-only mode:
npm run ops:enforce:app-check
- service targets:
- Monitoring baseline:
- ensure alerting + uptime checks with
npm run ops:ensure:alerting - drift assertion with
npm run ops:check:deploy-drift
- ensure alerting + uptime checks with
When queries evolve, update and deploy indexes/rules together to avoid runtime collectionGroup index errors and permission mismatches.
Cloud Build config: cloudbuild.yaml
Canonical production flow (single source of truth):
- Build an image with explicit
NEXT_PUBLIC_*substitutions. - Deploy that image to existing service
pathfinderinasia-southeast1. - Reapply runtime env + secrets on deploy.
App Check prerequisite when identitytoolkit.googleapis.com is ENFORCED:
- Ensure
NEXT_PUBLIC_FIREBASE_APPCHECK_SITE_KEYis set on Cloud Run. - Ensure Firebase App Check web app config has a valid enterprise site key:
projects/<firebase-project-number>/apps/<firebase-web-app-id>/recaptchaEnterpriseConfig.siteKey
- If either is missing, Firebase Auth login can fail with
401 Firebase App Check token is invalid. - Repeat check for each web app sharing Firebase auth in production (Pathfinder, StudyNavi, Assessment).
Turnstile network timeout note:
- If
/api/turnstile/verifyreturns503with messageCaptcha verification is temporarily unavailable. Please retry., treat as transient upstream timeout/network issue (ETIMEDOUT/connection reset). Retry login and verify egress/network health before changing app logic.
Why this is required:
NEXT_PUBLIC_*values are compiled into client bundles at build-time.- Runtime-only env updates do not repair already-built client bundles.
- Runtime fallback (
window.__PATHFINDER_PUBLIC_ENV__) is a safety net, not a replacement for correct build-time config.
PowerShell example:
$IMAGE_TAG = Get-Date -Format "yyyyMMdd-HHmmss"
$IMAGE = "asia-southeast1-docker.pkg.dev/your-gcp-project/cloud-run-source-deploy/pathfinder:$IMAGE_TAG"
gcloud builds submit . --config cloudbuild.yaml --project your-gcp-project --substitutions `
"_IMAGE=$IMAGE,_NEXT_PUBLIC_FIREBASE_API_KEY=<api_key>,_NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<auth_domain>,_NEXT_PUBLIC_FIREBASE_PROJECT_ID=<project_id>,_NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<storage_bucket>,_NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=<messaging_sender_id>,_NEXT_PUBLIC_FIREBASE_APP_ID=<app_id>,_NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=<measurement_id>,_NEXT_PUBLIC_FIREBASE_APPCHECK_SITE_KEY=<appcheck_site_key>,_NEXT_PUBLIC_TURNSTILE_SITE_KEY=<turnstile_site_key>,_NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=<google_maps_api_key>,_NEXT_PUBLIC_STUDYNAVI_URL=<studynavi_url>"
gcloud run deploy pathfinder --image $IMAGE --region asia-southeast1 --project your-gcp-project `
--update-secrets "FIREBASE_ADMIN_SDK_JSON=firebase-admin-sdk:latest,TURNSTILE_SECRET_KEY=turnstile-secret-key:latest,OPENAI_API_KEY=openai-api-key:latest" `
--update-env-vars "NEXT_PUBLIC_FIREBASE_API_KEY=<api_key>,NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<auth_domain>,NEXT_PUBLIC_FIREBASE_PROJECT_ID=<project_id>,NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<storage_bucket>,NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=<messaging_sender_id>,NEXT_PUBLIC_FIREBASE_APP_ID=<app_id>,NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=<measurement_id>,NEXT_PUBLIC_FIREBASE_APPCHECK_SITE_KEY=<appcheck_site_key>,NEXT_PUBLIC_TURNSTILE_SITE_KEY=<turnstile_site_key>,NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=<google_maps_api_key>,STUDYNAVI_URL=<studynavi_url>,NEXT_PUBLIC_STUDYNAVI_URL=<studynavi_url>,OPENAI_REPORT_MODEL=<model>"Post-deploy required checks:
npm run postdeploy:check- Login page does not show
Firebase failed to initialize - Browser console has no
[firebase] Missing env varswarning - Login runtime payload includes
NEXT_PUBLIC_FIREBASE_APPCHECK_SITE_KEY
One-command production deploy (recommended):
npm run deploy:prod- pulls current Cloud Run env/secrets
- builds image with required
NEXT_PUBLIC_*substitutions and present optionalNEXT_PUBLIC_*values - resolves optional
NEXT_PUBLIC_*values from secret-backed runtime env bindings when needed (for exampleNEXT_PUBLIC_GOOGLE_MAPS_API_KEY) - deploys same service with env/secrets reapplied
- runs postdeploy checks plus login runtime env validation
- Known rough edges:
- Heatmap location quality depends on free-text lead location values plus alias normalization; unexpected new location formats may require alias updates.
- Dashboard mixes scoped and intentionally global widgets; filter behavior must stay aligned with
docs/LOGIC_CONTRACT.mdto avoid regressions. - School seed data intentionally omits external school website links after portfolio scrub; provider website values now depend on runtime/source-of-truth data entry.
- Deferred refactors:
- Dashboard metric/filter logic is distributed across multiple hooks/utilities (
funnelMetrics,funnelFilters,targetVsActualMetrics, widget-specific transforms); centralization is still a future maintainability improvement. - Heatmap geocoding utilities and alias mappings are split for line-length policy and maintainability, but still rely on static alias inventory curation.
- Dashboard metric/filter logic is distributed across multiple hooks/utilities (
- Risky areas to touch carefully:
- Role scope changes must stay aligned across
src/utils/roles.ts, client query scope, andfirestore.rules. - Archive rollover logic is year + milestone sensitive; adjust only with matching logic-contract/test updates.
- Build-time vs runtime env behavior (
NEXT_PUBLIC_*) remains a deployment-sensitive area. - StudyNavi integration depends on external hostname reachability (
STUDYNAVI_URL/NEXT_PUBLIC_STUDYNAVI_URL); DNS failures on the configured StudyNavi host can break SSO even when Pathfinder is healthy.
- Role scope changes must stay aligned across
- Future product/analytics extensions:
- Heatmap can be extended with stronger geocoding normalization and country-level drilldowns once source data quality constraints are addressed.
- Additional dashboard widgets should explicitly declare whether they are funnel-scoped or globally ranked before implementation.
docs/BLUEPRINT.mddocs/SMOKE_TEST.mddocs/LOGIC_CONTRACT.mddocs/RELEASE_RUNBOOK.mddocs/INCIDENT_RUNBOOK.mddocs/SECURITY.mddocs/SUPPORT.mddocs/OPERATIONS_HANDOVER.mddocs/GITHUB_BRANCH_PROTECTION.mddocs/adr/0001-testing-split-smoke-regression.md