Files
Trey t 6005146e75 Airline integration work: AirlineLoadService updates, docs, JSX scripts
- AirlineLoadService: pass airport DB for timezone-aware date strings,
  add browser-shaped headers for United, expand JetBlue/Alaska/Emirates
  signatures to take origin, log/parse fixes for Korean Air.
- FlightsApp: build AirlineLoadService with the airport DB and inject it.
- JSX: continued WebView-based fetcher work plus updated JSX_NOTES.
- Docs: add AIRLINE_INTEGRATION_GUIDE.md, drop the old AIRLINE_API_SPEC.md,
  add api_docs/ (StaffTraveler reverse-engineering captures + findings).
- Scripts: jsx_cdp_probe, jsx_live_monitor, jsx_swift_smoke for JSX
  protocol exploration.
- .gitignore: exclude airlines/ (local-only APK/IPA reverse-engineering).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:21:30 -05:00

9.4 KiB

StaffTraveler Firestore schema (live-extracted)

Captured 2026-04-22 via Playwright MCP on https://app.stafftraveler.com while logged in as stafftraveler@treymail.com (uid 3NNPesQMiMRNYnPmuQzh6w2YKyh1, airline st_SWA). All queries/docs below are copied from the web client's firestore.googleapis.com/.../Listen/channel addTarget bodies — not guessed.

Why iOS mitm didn't show this

The iOS Firebase SDK and Android SDK both use the gRPC transport to firestore.googleapis.com (HTTP/2 with protobuf frames). Many mitm setups decode HTTPS but not gRPC framing, so the requests appear as opaque binary. The web client uses the VER=8 TYPE=xmlhttp long-poll fallback — plain HTTP POST with URL-encoded JSON bodies — which is fully readable. That's why we only saw this via the browser.

Required headers for REST access

Direct REST (/v1/projects/.../documents:runQuery or /documents/...) works with:

Authorization: Bearer <firebase id token>
X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0
X-Goog-Api-Client: gl-js/ fire/12.12.0
?key=AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc      (URL query param)

Note: web API key (AIzaSyD82...), not the Android key from strings.xml. Omitting ?key=... or the GMPID header causes 403 PERMISSION_DENIED — the rules consult the key for the project consumer context before evaluating allow read.

Single-document subscriptions

These are fetched as addTarget.documents.documents:

Path What
__system/maintenance Server maintenance flags
__system/version Required client versions (public)
users/{uid} Profile: airlineStCode, airlineId, firstName, gender, …
userHints/{uid} UI hints the user has dismissed
userRequestCounters/{uid} Regular-request quota state
userPriorityRequestCounters/{uid} Priority-request quota state
userCredits/{uid} Credit balance
userAchievements/{uid} Unlocked achievements
userMetrics/{uid} Usage telemetry
userFlightSearchHistory/{uid} Recent searches
airportsByIata/{IATA} Public airport lookup (DFW, LAS, …)
airlinesBySt/{stCode} Airline lookup by StaffTraveler code (st_AAL, st_SWA, …)
airlineNotesBySt/{stCode} Non-rev agreement notes per airline
flightEquipment/{iataOrId} Aircraft type info (32Q, 321, 738, …)
conversations/{flightId_v3} Flight-level comment thread
derivedLoadsReports/{reportId} Individual load report doc — see below
trackedFlights/{flightId_v3} User's tracked flight state

Collection queries

derivedLoadsReports — THE LOADS DATA

{
  "from": [{"collectionId": "derivedLoadsReports"}],
  "where": {
    "fieldFilter": {
      "field": {"fieldPath": "flightId"},
      "op": "EQUAL",
      "value": {"stringValue": "AA_2178_DFW_2026_04_22"}
    }
  },
  "orderBy": [
    {"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
    {"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
  ]
}

This is the query you want for the Flights integration. Pass the flight id_v3 (e.g. AA_2178_DFW_2026_04_22 — format is {airlineId}_{flightNumber}_{departureIata}_{YYYY}_{MM}_{DD}).

Access control (verified 2026-04-22): this query returns 403 PERMISSION_DENIED unless the user has an active trackedFlights/{flightId_v3} doc, which is created only by POST /v1/commands/user/createLoadsRequests. That call spends a credit on flights the user has never requested; replay on an already-requested flight is idempotent (no charge). So:

  • Freely re-read derivedLoadsReports for any flight you've already requested — no cost, no rate limit observed.
  • You cannot peek at loads without first requesting — the "instant results" in the app for flights with recent community reports rely on the user having previously paid the credit.
  • On the 15 DFW-LAS flights tested, only AA_2178_DFW_2026_04_22 (the one with an open request) returned 200; the other 14 all returned 403. Same for 29 LAX-JFK flights the user hadn't requested — all 403.

Example doc (captured AA2178 2026-04-22 12:42 PM CT):

{
  "flightId": "AA_2178_DFW_2026_04_22",
  "openSeats": {
    "first":   {"count": 3},
    "eco":     {"count": 9}
  },
  "staffListing": {
    "type": "available",
    "count": 6,
    "countByClass": {}
  },
  "responseTimeSeconds": 196,
  "numberOfCreditsRewarded": 1,
  "isFlagged": false,
  "isFlaggedConfirmed": false,
  "seatsAvailabilityScore": 0.79,
  "hasClosedRequest": true,
  "isJackpotHit": false,
  "isPriorityRequest": false,
  "createdAt": "2026-04-22T18:42:51.313Z",
  "expireAt":  "2026-07-21T21:45:00Z",
  "creatorName":      "M",
  "creatorImageUrl":  "https://images.stafftraveler.com/avatars/happysuitcase.png",
  "creatorImagePath": "avatars/happysuitcase.png"
}

Field notes:

  • openSeats.<class>.countexactly what the app shows as "OPEN SEATS". Classes seen: first, business, premiumEconomy, eco. Absent class = 0 or unknown.
  • staffListing.countthe "LISTED NON-REV PASSENGERS" number.
  • staffListing.type"available" means loads were given; other types likely include "upgrade" variants (see checkForLoads heuristic in bundle.hasm).
  • seatsAvailabilityScore — 0-1, app uses this to color-code the flight.
  • creatorName is the first letter only (privacy).
  • expireAt — ~90 days out, so load reports stay queryable.

trackedFlights — your open requests

{
  "from": [{"collectionId": "trackedFlights"}],
  "where": {
    "compositeFilter": {"op": "AND", "filters": [
      {"fieldFilter": {"field": {"fieldPath": "status"}, "op": "EQUAL", "value": {"stringValue": "open"}}},
      {"fieldFilter": {"field": {"fieldPath": "hasDeparted"}, "op": "EQUAL", "value": {"booleanValue": false}}},
      {"fieldFilter": {"field": {"fieldPath": "scheduledFlight.airlineStCode"}, "op": "EQUAL", "value": {"stringValue": "st_SWA"}}}
    ]}
  },
  "orderBy": [
    {"field": {"fieldPath": "priority"}, "direction": "DESCENDING"},
    {"field": {"fieldPath": "scheduledFlight.departureTimeUtc"}, "direction": "ASCENDING"},
    {"field": {"fieldPath": "__name__"}, "direction": "ASCENDING"}
  ],
  "limit": 25
}

Returns the logged-in user's open requests filtered by their airline. Doc ID = flight id_v3. scheduledFlight is the embedded full flight object (matches searchFlightsByRoute item shape).

trackedFlights/{flightId}/statusUpdates

{
  "from": [{"collectionId": "statusUpdates"}],
  "orderBy": [
    {"field": {"fieldPath": "createdAt"}, "direction": "DESCENDING"},
    {"field": {"fieldPath": "__name__"}, "direction": "DESCENDING"}
  ],
  "parent": "projects/stafftraveler-prod/databases/(default)/documents/trackedFlights/AA_2178_DFW_2026_04_22"
}

Delay / gate / equipment changes for that flight over time.

users/{uid}/pinnedFlights, users/{uid}/connectingFlights

Subcollections under the user doc. Empty for this account.

autoRequestRecords where subscribedUserIds ARRAY_CONTAINS {uid} AND isActive

Auto-request subscriptions — flights the user has set up to auto-request loads for on a recurring schedule.

trackedFlights where subscribedUsers.{uid} == true AND isDeleted == false

Other users' requests that THIS user has subscribed to (so they also see the loads when answered).

Idempotent REST access pattern (integration recipe)

# 1. sign in (you already do this)
TOKEN=$(curl -sX POST "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=$WEB_KEY" \
  -d "{\"email\":\"$EMAIL\",\"password\":\"$PW\",\"returnSecureToken\":true}" | jq -r .idToken)

# 2. read loads for any flight
BASE='https://firestore.googleapis.com/v1/projects/stafftraveler-prod/databases/(default)/documents'
KEY='AIzaSyD82Hiqx-jAbHF8faMGSveqLw8CdX8a-Uc'
curl -sX POST "$BASE:runQuery?key=$KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Firebase-GMPID: 1:628258099825:web:35b20eaab4d441894041d0" \
  -H "Content-Type: application/json" \
  -d '{"structuredQuery":{"from":[{"collectionId":"derivedLoadsReports"}],"where":{"fieldFilter":{"field":{"fieldPath":"flightId"},"op":"EQUAL","value":{"stringValue":"AA_2178_DFW_2026_04_22"}}},"orderBy":[{"field":{"fieldPath":"createdAt"},"direction":"DESCENDING"}]}}'

Returns empty array if no report exists yet (meaning you'd need createLoadsRequests first to ask the crew). If a report exists, you get it without spending a credit.

Integration plan for the iOS Flights app (final)

  1. Secondary FirebaseApp configured against stafftraveler-prod with the web API key + GMPID above.
  2. Auth.auth(app: stApp).signIn(withEmail:password:) with user's StaffTraveler credentials, refresh token stored in Keychain.
  3. Use Firestore.firestore(app: stApp) and wire:
    • collection("derivedLoadsReports").whereField("flightId", isEqualTo: flightIdV3).addSnapshotListener { ... } — live load updates per flight
    • collection("trackedFlights").whereField("status", isEqualTo: "open")....whereField("scheduledFlight.airlineStCode", isEqualTo: userStCode).limit(to: 25).addSnapshotListener — user's open requests list
    • document("airlinesBySt/\(stCode)") — airline metadata
    • document("airportsByIata/\(iata)") — airport metadata
  4. For writes (request loads, submit report, etc.), keep using the HTTP commands API at https://api.stafftraveler.com/v1/commands/user/<cmd>.

No App Check enforcement observed from the web client — plain Firebase ID token + API key is sufficient.