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
- 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
<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
StreamCredentialsQueryGraphQL - 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:
- Write: localStorage immediately → sync to server
- Read: Try server first → fallback to localStorage
- 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/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)
# 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-explorerroute for GraphQL query testing- Console logging throughout codebase
/api/healthendpoint for backend status
Credentials & Configuration
Credentials are stored in constants.ts and localStorage:
feeld_profile_id- Current profile UUIDfeeld_refresh_token- Firebase refresh tokenfeeld_auth_token- Current session tokenfeeld_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-youfor 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-pingsbackend 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
- Create component in
web/src/pages/ - Add route in
web/src/App.tsx - Add navigation link in
web/src/components/layout/Navigation.tsx
Add a new GraphQL query/mutation
- Define in
web/src/api/operations/queries.tsormutations.ts - Use fragments from existing definitions for consistency
- Call via Apollo's
useQuery/useMutationhooks
Add a new context/hook
- Create in
web/src/hooks/orweb/src/context/ - Export provider and hook
- Add provider to hierarchy in
App.tsx
Debug API issues
- Use
/api-explorerroute to test queries directly - Check Network tab for request/response
- Verify headers in
constants.ts - Check
API_DOCUMENTATION.mdfor 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
- 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/