JupiterAPI has three domains:
- Read API — proxies ESN Jupiter (upstream SaaS) with normalization, caching, and section-level statistics.
- Write proxy (Hercules) — JWT-gated board operations persisted to Firestore (events, meetings, offices, trainings, fidelity, attendance, board approvals, audit logs).
- Auth — Cognito-backed login/refresh for the board frontend plus JWKS-based token verification.
┌────────────┐
│ index.js │ bootstrap: cors, rate limit, api key, mounts
└─────┬──────┘
│ requires
┌────────────────┼──────────────────┐
▼ ▼ ▼
src/routes/* src/middleware/* src/config.js
(HTTP only) (cross-cutting) (sole env reader)
│ │ ▲
▼ │ │ all modules may read config
src/jupiter.js ◄───────┘ (only jupiter calls upstream)
│
▼
src/normalizers.js ──► src/formatting.js ──► src/nationality.js
│ │
├────────────────────┴──► src/fieldsOfStudy.js
└───────────────────────► src/eventLocation.js
src/routes/hercules.js ──► firebase-admin (Firestore) [only Firestore writer]
src/routes/auth.js ──► amazon-cognito-identity-js + middleware/jwtAuth (JWKS)
Rules encoded by the arrows:
routes/*never call upstream or Firestore directly excepthercules.js(Firestore) andjupiter.js(upstream).- Domain modules (
normalizers,formatting,nationality,fieldsOfStudy,eventLocation) depend only on each other and Node built-ins — never express, never jupiter.js. config.jsdepends on nothing but dotenv; everything else reads config from it.
client → apiKeyAuth → rateLimiter → router (src/routes/<domain>.js)
→ jupiter.js (Cognito token cached 55 min, response cached 5 min,
interceptors absorb "null" strings)
→ normalizers.js (users/events/subscriptions → stable JSON shape)
→ res.json
GET /stats/comprehensive keeps its own 15-minute cache and fans out to event subscriptions in batches of 5 (getEventParticipantUserIds) to compute inactive-member lists. All other stats endpoints are computed per request over cached user lists.
client → jwtAuth (Cognito ID token via JWKS) → requireBoardMember
(config.boardMemberIds, immutable sub IDs only)
→ validation.js (whitelists, enums, ranges)
→ Firestore write
Fidelity entries run in a transaction that also updates progress/{userId} counters (fidelityPointsTotal, fidelityEntriesCount, fidelityEventsCount); deletes reverse them. Progress updates go through a field whitelist (mass-assignment protection).
POST /auth/login|refreshuse the Cognito SDK directly.POST /auth/verifyandmiddleware/jwtAuthverify ID tokens against Cognito JWKS; public keys are cached for 1 hour with force-refresh retry on unknownkid(key rotation).
Upstream quirks are absorbed at the edge so routers see clean data:
- Jupiter sometimes returns the string
"null"or literalnull→jupiter.jsinterceptors convert to[]. - A user without a subscription makes Jupiter return 400 (not 404) →
getUserSubscriptionmaps both tonull. - City inference (domicile → profile → field-of-study campus map; venue/address/postal-code/fidelity-points parsing for events) lives in
eventLocation.js/fieldsOfStudy.js;normalizers.jsattachescitySource/cityInferred/cityUnknownso clients can show provenance.
- In-memory caches (jupiter 5 min, comprehensive stats 15 min, JWKS 1 h): assumes a single instance; horizontal scaling would need a shared cache.
- Academic year runs Sept–Aug and rolls over automatically on September 1 (
getCurrentAcademicYear()/isCurrentAcademicYear()insrc/formatting.js). No manual update is needed when the season changes: on Sep 1 the API starts reporting the new year (e.g.2026-2027) and/eventsbegins filtering to the new window. Test fixtures derive their timestamps from the live academic year, so the suite stays green across rollovers. - Board authorization by immutable
sub: usernames change; subs do not.BOARD_MEMBER_IDSis parsed once inconfig.js. require.cachetest mocking instead of DI: tests inject fakes forjupiter.js,firebase-admin, the Cognito SDK, and JWKS verification viatest/helpers/mockModule.js— no mocking framework.- Centralized fail-fast config: missing core env vars exit(1) at first require of
config.js, before any route can serve traffic.