# 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
```bash
cd web
npm install
npm run dev:all # Starts both Vite dev server and Express backend
```
- Frontend: http://localhost:3000
- Backend API: http://localhost:3001
## 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
```tsx
```
## 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
```typescript
{
'Authorization': 'Bearer ',
'x-profile-id': 'profile#',
'x-app-version': '8.8.3',
'x-device-os': 'ios',
'x-os-version': '18.6.2',
'x-transaction-id': '',
'x-event-analytics-id': ''
}
```
### 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
```bash
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/graphql` → `core.api.fldcore.com` (GraphQL backend)
- `/api/firebase` → `securetoken.googleapis.com` (auth tokens)
- `/api/images` → `res.cloudinary.com` (profile images)
- `/api/fldcdn` → `prod.fldcdn.com` (CDN assets)
**Local backend:**
- `/api/who-liked-you` → `localhost:3001`
- `/api/sent-pings` → `localhost:3001`
- `/api/disliked-profiles` → `localhost:3001`
- `/api/data` → `localhost:3001`
- `/api/auth` → `localhost:3001`
- `/api/health` → `localhost: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)
```bash
# 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.
```tsx
// 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: {safeText(profile.gender)}
// NOT: {profile.gender} // Can crash with "Objects are not valid as React child"
```
Also sanitize profile data before saving to backend cache with `safeStr()`:
```tsx
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/`.
```bash
# 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
- **API Documentation**: `API_DOCUMENTATION.md` (comprehensive reverse-engineered docs)
- **Captured Requests**: `proxyman_extracted/`, `proxyman_chat/`, `stream_extracted/`
- **Stream Chat Docs**: https://getstream.io/chat/docs/
- **Apollo Client Docs**: https://www.apollographql.com/docs/react/