Files
Feeld/claude.md
2026-03-20 18:49:48 -05:00

16 KiB

Feeld Web Client

A React/TypeScript web application that provides browser-based access to the Feeld dating platform. Connects to the same GraphQL backend as the mobile app using Firebase authentication.

Quick Start

cd web
npm install
npm run dev:all    # Starts both Vite dev server and Express backend

Project Structure

/Feeld
├── web/                          # Main React application
│   ├── src/
│   │   ├── api/                  # API layer
│   │   │   ├── auth.ts          # Firebase token management (AuthManager class)
│   │   │   ├── client.ts        # Apollo GraphQL client setup
│   │   │   ├── dataSync.ts      # localStorage + server sync service
│   │   │   └── operations/      # GraphQL queries and mutations
│   │   │       ├── queries.ts
│   │   │       ├── mutations.ts
│   │   │       └── experimental.ts
│   │   ├── components/
│   │   │   ├── ui/              # Shared UI primitives
│   │   │   │   ├── Avatar.tsx       # Profile avatar with fallback
│   │   │   │   ├── Badge.tsx        # Status/tag badges
│   │   │   │   ├── Button.tsx       # Styled button component
│   │   │   │   ├── Card.tsx         # Card container
│   │   │   │   ├── Loading.tsx      # Loading spinner/skeleton
│   │   │   │   └── ProxiedImage.tsx # Image component with proxy URL handling
│   │   │   ├── layout/          # Layout and Navigation
│   │   │   │   ├── Layout.tsx       # Main layout wrapper with nav
│   │   │   │   └── Navigation.tsx   # Side rail (desktop) / bottom bar (mobile)
│   │   │   ├── profile/         # Profile display components
│   │   │   │   ├── ProfileCard.tsx        # Profile card (used in Likes, Discover, etc.)
│   │   │   │   ├── ProfileDetailModal.tsx # Full profile modal with partner nav
│   │   │   │   └── PingModal.tsx          # Send ping modal with message
│   │   │   ├── chat/            # Chat components
│   │   │   │   └── ChatListItem.tsx
│   │   │   └── LoginPage.tsx
│   │   ├── pages/               # Route pages
│   │   │   ├── Discover.tsx     # Profile discovery feed with scanner
│   │   │   ├── Likes.tsx        # Likes/Pings/You Liked/Passed (4 tabs)
│   │   │   ├── SentPings.tsx    # Outbound pings tracker
│   │   │   ├── Messages.tsx     # Chat list
│   │   │   ├── Chat.tsx         # Individual conversation
│   │   │   ├── Profile.tsx      # Own profile view
│   │   │   ├── Settings.tsx     # App settings
│   │   │   └── ApiExplorer.tsx  # Debug tool for testing GraphQL
│   │   ├── hooks/               # Custom React hooks
│   │   │   ├── useAuth.tsx      # Authentication context/provider
│   │   │   ├── useLocation.tsx  # Location context with geocoding
│   │   │   ├── useLikedProfiles.ts
│   │   │   ├── useDislikedProfiles.ts  # Track passed/disliked profiles
│   │   │   └── useSentPings.ts         # Track outbound pings
│   │   ├── context/
│   │   │   └── StreamChatContext.tsx  # Stream Chat SDK provider
│   │   ├── config/
│   │   │   └── constants.ts     # API URLs, headers, credentials
│   │   ├── utils/
│   │   │   └── images.ts        # Image URL proxying
│   │   ├── App.tsx              # Root component with providers
│   │   └── main.tsx             # Entry point
│   ├── server/
│   │   └── index.js             # Express backend for data persistence
│   ├── vite.config.ts           # Vite bundler config with API proxies
│   ├── package.json
│   ├── docker-compose.yml       # Docker stack for Unraid production
│   └── docker-compose.local.yml # Docker stack for local development
├── proxyman_extracted/          # Captured API requests/responses
├── proxyman_chat/               # Captured Stream Chat API data
├── stream_extracted/            # Stream Chat SDK captures
└── API_DOCUMENTATION.md         # Comprehensive API docs (4780 lines)

Architecture

Stack

  • React 19 with TypeScript
  • Apollo Client 4 for GraphQL
  • Stream Chat SDK for real-time messaging
  • Tailwind CSS 4 for styling
  • Vite 7 for bundling
  • Express 5 for local backend

State Management

  • React Context for global state (Auth, StreamChat, Location)
  • Apollo Client cache for GraphQL data
  • localStorage for offline persistence with server sync

Provider Hierarchy

<AuthProvider>
  <ApolloProvider>
    <StreamChatProvider>
      <LocationProvider>
        <BrowserRouter>
          <Routes />
        </BrowserRouter>
      </LocationProvider>
    </StreamChatProvider>
  </ApolloProvider>
</AuthProvider>

Key Files

File Purpose
web/src/App.tsx Root component with all providers
web/src/api/client.ts Apollo GraphQL client with auth headers
web/src/api/auth.ts Firebase token refresh (AuthManager singleton)
web/src/api/dataSync.ts Dual storage: localStorage + backend server
web/src/config/constants.ts API endpoints, credentials, headers
web/src/hooks/useAuth.tsx Auth context and login/logout
web/src/context/StreamChatContext.tsx Stream Chat initialization
web/src/components/profile/ProfileCard.tsx Profile card with safeText() rendering
web/src/pages/Likes.tsx Likes page with enrichment, scanner, auto-refresh
web/server/index.js Express backend for data persistence
API_DOCUMENTATION.md Full API reference from reverse engineering

API Integration

GraphQL Backend

  • Endpoint: https://core.api.fldcore.com/graphql (proxied via Vite)
  • Client: Apollo Client with auth link middleware
  • Key queries: ProfileQuery, DiscoverProfiles, WhoLikesMe, ListSummaries
  • Key mutations: ProfileLike, ProfileDislike, DeviceLocationUpdate, SearchSettingsUpdate

Required Headers

{
  'Authorization': 'Bearer <firebase_jwt>',
  'x-profile-id': 'profile#<uuid>',
  'x-app-version': '8.8.3',
  'x-device-os': 'ios',
  'x-os-version': '18.6.2',
  'x-transaction-id': '<uuid>',
  'x-event-analytics-id': '<uuid>'
}

Firebase Authentication

  • Token refresh via https://securetoken.googleapis.com/v1/token
  • AuthManager class handles automatic refresh with 1-minute buffer
  • Tokens stored in memory, refresh token in localStorage

Stream Chat

  • Real-time messaging via Stream Chat SDK
  • Credentials fetched via StreamCredentialsQuery GraphQL
  • API Key: y4tp4akjeb49

Local Backend Endpoints

GET/PUT  /api/data/:userId             # Full user data
GET/PUT  /api/data/:userId/:key        # Specific key
DELETE   /api/data/:userId/:key        # Delete key
POST     /api/data/:userId/liked-profiles
DELETE   /api/data/:userId/liked-profiles/:profileId
GET/POST /api/disliked-profiles        # Passed/disliked profiles cache
DELETE   /api/disliked-profiles/:id    # Remove from passed
GET/POST /api/who-liked-you            # Cache profiles that liked user
GET/POST /api/sent-pings               # Track outbound pings
PUT      /api/sent-pings/:targetProfileId  # Update ping status
DELETE   /api/sent-pings/:targetProfileId  # Remove sent ping
POST     /api/auth/login
GET      /api/auth/verify
POST     /api/auth/logout
GET      /api/health

Data Persistence

Hybrid localStorage + server approach in dataSync.ts:

  1. Write: localStorage immediately → sync to server
  2. Read: Try server first → fallback to localStorage
  3. Health check: Server ping every 30 seconds

Keys synced: liked_profiles, disliked_profiles, who_liked_you_profiles, locations, credentials, current_location, analytics_id

Routes

Path Page Description
/discover DiscoverPage Profile discovery feed with scanner
/likes LikesPage Likes / Pings / You Liked / Passed (4 tabs)
/sent-pings SentPingsPage Outbound pings tracker
/messages MessagesPage Chat conversation list
/chat/:channelId ChatPage Individual chat
/profile ProfilePage Own profile view
/settings SettingsPage Location, search settings, credentials
/api-explorer ApiExplorerPage Debug tool for testing GraphQL queries

Development

Scripts

npm run dev              # Vite dev server only
npm run server           # Express backend only
npm run dev:all          # Both in parallel (recommended)
npm run build            # Production build
npm run lint             # ESLint check
npm run docker:local     # Start local Docker stack
npm run docker:local:down   # Stop local Docker stack
npm run docker:local:logs   # View local Docker logs

Vite Proxy Configuration

The Vite dev server proxies requests to bypass CORS:

External APIs:

  • /api/graphqlcore.api.fldcore.com (GraphQL backend)
  • /api/firebasesecuretoken.googleapis.com (auth tokens)
  • /api/imagesres.cloudinary.com (profile images)
  • /api/fldcdnprod.fldcdn.com (CDN assets)

Local backend:

  • /api/who-liked-youlocalhost:3001
  • /api/sent-pingslocalhost:3001
  • /api/disliked-profileslocalhost:3001
  • /api/datalocalhost:3001
  • /api/authlocalhost:3001
  • /api/healthlocalhost:3001

Mobile app User-Agent is injected on external proxies to bypass hotlink protection.

Docker

Two compose files:

  • docker-compose.yml - Production config for Unraid server (absolute paths to /mnt/user/appdata/FeeldWeb)
  • docker-compose.local.yml - Local dev config (relative paths)
# Local development
docker compose -f docker-compose.local.yml up -d --build

# Production (on Unraid server)
cd /mnt/user/appdata/FeeldWeb && docker compose up -d --build
  • Frontend: port 3000 (Vite dev server)
  • Backend: port 3001 (Express)
  • Nginx: port 7743 (reverse proxy)

Testing

No automated tests currently. Manual testing via:

  • /api-explorer route for GraphQL query testing
  • Console logging throughout codebase
  • /api/health endpoint for backend status

Credentials & Configuration

Credentials are stored in constants.ts and localStorage:

  • feeld_profile_id - Current profile UUID
  • feeld_refresh_token - Firebase refresh token
  • feeld_auth_token - Current session token
  • feeld_analytics_id - Event tracking ID

Features

Likes Page (4 Tabs)

  • Likes: Profiles that have liked you, enriched with cached data from /api/who-liked-you
  • Pings: Profiles that pinged you (liked with a message)
  • You Liked: Profiles you've liked
  • Passed: Profiles you've disliked/passed on, stored via /api/disliked-profiles

Enrichment: WhoLikesMe API returns limited data (null photos for non-Majestic users). Cached profile data from the scanner (Discover API) is merged in via useMemo. Older likes (further down the list) get matched first to preserve chronological accuracy when multiple people share the same name.

Auto-refresh: On page load, profiles with missing/expired photos are automatically refreshed via individual ProfileQuery calls. Uses useRef + sessionStorage to prevent crash-remount loops (React error unmounts/remounts the component, resetting useState but not useRef/sessionStorage).

Scanner: Scans multiple saved locations via the Discover API to find real profile data. "Fuck It" mode scans all saved locations. Profiles that have liked you are auto-saved to /api/who-liked-you.

Profile Detail Modal

  • View full profile details including photos, bio, desires, interests
  • Partner navigation: Click on linked partner profiles to view their details
  • Back button to return to previous profile when viewing partners
  • Like/Dislike actions with API mutations
  • Ping modal for sending messages with likes

Discover Page

  • Profile browsing with like/dislike/ping actions
  • Auto-saves profiles that have liked you (detected via interactionStatus.theirs === 'LIKED')
  • Profiles cached to /api/who-liked-you for enriching Likes page data

Sent Pings Page

  • Tracks outbound pings (likes with messages) sent to other profiles
  • Shows ping message, target profile, and status
  • Stored via /api/sent-pings backend endpoint

Gotchas

GraphQL __typename Objects

The Feeld GraphQL API can return profile fields (gender, sexuality, location, goals, distance) as objects with {__typename: "..."} instead of plain strings. Always use safeText() or type checks when rendering any profile field. Never render profile.gender or similar fields directly in JSX.

// ProfileCard.tsx has a safeText() helper:
const safeText = (v: any): string => {
  if (v == null) return '';
  if (typeof v === 'string') return v;
  if (typeof v === 'number' || typeof v === 'boolean') return String(v);
  return '';
};

// Usage: <span>{safeText(profile.gender)}</span>
// NOT:   <span>{profile.gender}</span>  // Can crash with "Objects are not valid as React child"

Also sanitize profile data before saving to backend cache with safeStr():

const safeStr = (v: any) => (typeof v === 'string' ? v : '');

Signed Image URLs

Cloudinary/fldcdn signed image URLs expire after a few hours. Cached profiles need periodic refresh to get new URLs. The auto-refresh on the Likes page handles this.

API Version Enforcement

Feeld API enforces x-app-version — old versions get UNSUPPORTED_APP_VERSION 400 errors. Check the App Store for the current version and update constants.ts and vite.config.ts headers.

WhoLikesMe Photo Limitations

WhoLikesMe API returns null photo URLs for non-Majestic (non-paying) users. The scanner (Discover API) returns real photos, which is why we cache scanner results and enrich WhoLikesMe data.

Render-time Side Effects

Never call functions with side effects during React render. Use useMemo for data transformation and separate useEffect for side effects like API calls or cache updates. Calling side-effect functions during render causes infinite re-render loops.

Common Tasks

Add a new page

  1. Create component in web/src/pages/
  2. Add route in web/src/App.tsx
  3. Add navigation link in web/src/components/layout/Navigation.tsx

Add a new GraphQL query/mutation

  1. Define in web/src/api/operations/queries.ts or mutations.ts
  2. Use fragments from existing definitions for consistency
  3. Call via Apollo's useQuery/useMutation hooks

Add a new context/hook

  1. Create in web/src/hooks/ or web/src/context/
  2. Export provider and hook
  3. Add provider to hierarchy in App.tsx

Debug API issues

  1. Use /api-explorer route to test queries directly
  2. Check Network tab for request/response
  3. Verify headers in constants.ts
  4. Check API_DOCUMENTATION.md for endpoint details

Remote Deployment (Unraid)

Production runs on an Unraid server at 10.3.3.11. Code lives at /mnt/user/appdata/FeeldWeb/, persistent data at /mnt/user/downloads/feeldWeb/.

# SSH in
sshpass -p 'Intel22' ssh -o StrictHostKeyChecking=no root@10.3.3.11

# Sync a file
sshpass -p 'Intel22' scp -o StrictHostKeyChecking=no \
  "web/FILE_PATH" "root@10.3.3.11:/mnt/user/appdata/FeeldWeb/FILE_PATH"

# Restart containers
sshpass -p 'Intel22' ssh -o StrictHostKeyChecking=no root@10.3.3.11 \
  'docker restart feeld-web-frontend feeld-web-backend feeld-web-nginx'

External URL: https://feeld.treytartt.com (proxied via Nginx Proxy Manager on port 7743)

External Resources