17 KiB
Integration Audit: Backend ↔ Frontend
Generated: 2025-01-27
Scope: veza-backend-api (Go) ↔ apps/web (React/TypeScript)
Auditor: Cursor AI
Health Score: 4/10
Executive Snapshot
Overall Integration Health: 4/10
The integration surface between backend and frontend shows significant contract drift, authentication inconsistencies, and configuration mismatches. While core flows (login, basic CRUD) function in development, production deployment will expose critical failures due to CORS misconfiguration, missing CSRF protection, type mismatches, and environment variable inconsistencies.
What is working:
- Basic authentication flow (login/logout) works in dev
- API client interceptors handle token refresh
- Response unwrapping in
apiClientcorrectly handles{ success, data }format - Health check endpoints accessible
What is fragile:
- CORS configuration will break in production (empty origins = reject all)
- Multiple auth storage mechanisms cause token sync issues
- Type mismatches (User.id: string vs number) will cause runtime errors
- Environment variable naming inconsistencies (VITE_API_BASE_URL vs VITE_API_URL)
- No CSRF protection for state-changing operations
- Response format expectations mismatch between deprecated
ApiServiceandapiClient - Missing error correlation IDs in frontend error boundaries
Real Architecture
Development Setup
Frontend (apps/web):
- Vite dev server on
http://localhost:3000 - API calls via
apiClient(axios) toVITE_API_URL(default:http://127.0.0.1:8080/api/v1) - No proxy configuration in
vite.config.ts(direct CORS requests) - Token storage:
localStorage(access_token, refresh_token)
Backend (veza-backend-api):
- Gin server on
:8080 - Routes under
/api/v1/*(v1 group) - CORS middleware: allows
http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173(dev defaults) - Auth: JWT tokens in
Authorization: Bearer <token>header
Production Setup (Inferred)
Frontend:
- Static build served via nginx (see
apps/web/nginx.conf) - Nginx proxies
/api/*tobackend-api:8080 - Environment variables baked at build time (
VITE_*)
Backend:
- CORS origins: REQUIRED via
CORS_ALLOWED_ORIGINSenv var - If
CORS_ALLOWED_ORIGINSempty → rejects all origins (strict mode) - No proxy rewrite assumptions (routes expect
/api/v1prefix)
Critical Gap: Frontend expects /api/v1 in base URL, but nginx proxy may rewrite paths. No evidence of path rewriting in nginx config.
Contract Map
| Frontend Module/Hook | HTTP Method + Path | Backend Handler | Request Shape | Response Shape | Status |
|---|---|---|---|---|---|
authApi.login() |
POST /api/v1/auth/login |
handlers.Login() |
{ email, password, remember_me? } |
{ success: true, data: { user: {...}, token: {...} } } |
✅ Match |
authApi.register() |
POST /api/v1/auth/register |
handlers.Register() |
{ email, username, password } |
{ success: true, data: { user: {...} } } |
✅ Match |
authApi.refresh() |
POST /api/v1/auth/refresh |
handlers.Refresh() |
{ refresh_token } |
{ success: true, data: { access_token, refresh_token, expires_in } } |
✅ Match |
authApi.getMe() |
GET /api/v1/auth/me |
handlers.GetMe() |
(auth header) | { success: true, data: { id, email, role } } |
⚠️ Partial (missing user fields) |
apiClient.get('/tracks') |
GET /api/v1/tracks |
trackHandler.ListTracks() |
Query params | { success: true, data: [...] } |
✅ Match |
apiClient.get('/tracks/:id') |
GET /api/v1/tracks/:id |
trackHandler.GetTrack() |
(path param) | { success: true, data: {...} } |
✅ Match |
apiClient.post('/tracks') |
POST /api/v1/tracks |
trackHandler.UploadTrack() |
FormData | { success: true, data: {...} } |
✅ Match |
apiClient.get('/users/:id') |
GET /api/v1/users/:id |
profileHandler.GetProfile() |
(path param) | { success: true, data: { profile: {...} } } |
⚠️ Nested profile key |
apiClient.get('/playlists') |
GET /api/v1/playlists |
playlistHandler.GetPlaylists() |
Query params | { success: true, data: [...] } |
✅ Match |
apiClient.post('/chat/token') |
POST /api/v1/chat/token |
chatHandler.GetToken() |
{} |
{ success: true, data: { token } } |
✅ Match |
apiClient.get('/conversations') |
GET /api/v1/conversations |
roomHandler.GetUserRooms() |
(auth header) | { success: true, data: [...] } |
✅ Match |
apiClient.post('/sessions/logout') |
POST /api/v1/sessions/logout |
sessionHandler.Logout() |
(auth header) | { success: true, data: {...} } |
✅ Match |
apiClient.get('/health') |
GET /api/v1/health |
healthHandler.Check() |
- | { success: true, data: {...} } |
✅ Match |
apiClient.get('/upload/limits') |
GET /api/v1/upload/limits |
uploadHandler.GetUploadLimits() |
- | { success: true, data: {...} } |
✅ Match |
apiService.login() (deprecated) |
POST /api/v1/auth/login |
handlers.Login() |
{ email, password } |
Expects { user, token } in data |
❌ Mismatch (expects flat structure) |
apiService.getUser() |
GET /api/v1/users/:id |
profileHandler.GetProfile() |
(path param) | Expects profile or direct user |
⚠️ Handles nested but fragile |
2fa-service.ts |
GET /api/v1/2fa/status |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
2fa-service.ts |
POST /api/v1/2fa/setup |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.addCollaborator() |
POST /api/v1/playlists/:id/collaborators |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.removeCollaborator() |
DELETE /api/v1/playlists/:id/collaborators/:userId |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.updateCollaborator() |
PUT /api/v1/playlists/:id/collaborators/:userId |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.search() |
GET /api/v1/playlists/search |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.share() |
POST /api/v1/playlists/:id/share |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
playlistService.getRecommendations() |
GET /api/v1/playlists/recommendations |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
trackApi.getTrackDownload() |
GET /api/v1/tracks/:id/download |
trackHandler.DownloadTrack() |
(path param) | File stream | ✅ Match |
trackApi.likeTrack() |
POST /api/v1/tracks/:id/like |
trackHandler.LikeTrack() |
(path param) | { success: true, data: {...} } |
✅ Match |
trackApi.unlikeTrack() |
DELETE /api/v1/tracks/:id/like |
trackHandler.UnlikeTrack() |
(path param) | { success: true, data: {...} } |
✅ Match |
trackApi.deleteTrack() |
DELETE /api/v1/tracks/:id |
trackHandler.DeleteTrack() |
(path param) | { success: true, data: {...} } |
✅ Match |
hlsService.getHLSInfo() |
GET /api/v1/tracks/:id/hls/info |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
hlsService.getHLSStatus() |
GET /api/v1/tracks/:id/hls/status |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
roleService.assignRole() |
POST /api/v1/users/:userId/roles |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
roleService.removeRole() |
DELETE /api/v1/users/:userId/roles/:roleId |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
roleService.updateRole() |
PUT /api/v1/roles/:roleId |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
roleService.deleteRole() |
DELETE /api/v1/roles/:roleId |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
profileService.getProfile() |
GET /api/v1/users/:userId/profile |
❌ NOT FOUND (uses /users/:id instead) |
- | - | ⚠️ Path mismatch |
profileService.updateProfile() |
PUT /api/v1/users/:userId/profile |
❌ NOT FOUND (uses /users/:id instead) |
- | - | ⚠️ Path mismatch |
avatarService.deleteAvatar() |
DELETE /api/v1/users/:userId/avatar |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
settingsService.updateSettings() |
PUT /api/v1/users/:userId/settings |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
notificationsApi.markRead() |
POST /api/v1/notifications/:id/read |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
notificationsApi.markAllRead() |
POST /api/v1/notifications/read-all |
❌ NOT FOUND | - | - | ❌ Endpoint missing |
Summary: 18 endpoints called by frontend are missing or have path mismatches.
Top 5 Critical Issues (P0)
1. CORS Configuration Will Break Production
Severity: P0
Impact: All API requests will fail in production if CORS_ALLOWED_ORIGINS is not set.
Evidence:
veza-backend-api/internal/api/router.go:119-128: CORS middleware applied withconfig.CORSOriginsveza-backend-api/internal/config/config.go:638-664:getCORSOrigins()returns empty list[]in production ifCORS_ALLOWED_ORIGINSunsetveza-backend-api/internal/middleware/cors.go:34-37: Empty origins list →isAllowedOrigin()returnsfalse→ all CORS requests rejected
Root Cause: Production defaults to strict mode (reject all) but frontend expects CORS to work.
Fix Plan:
- Document
CORS_ALLOWED_ORIGINSas REQUIRED in production - Add startup validation: fail fast if
APP_ENV=productionandCORS_ALLOWED_ORIGINSempty - Update deployment docs with example:
CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com
Tests: E2E test with production-like CORS config.
2. Multiple Auth Storage Mechanisms Cause Token Sync Failures
Severity: P0
Impact: Tokens stored in multiple places (localStorage, Zustand persist, TokenStorage) can desync, causing auth failures.
Evidence:
apps/web/src/services/tokenStorage.ts:35-36: Stores inlocalStorage(keys:veza_access_token,veza_refresh_token)apps/web/src/services/api/client.ts:48-64: Falls back to parsingauth-storagefrom Zustand if TokenStorage emptyapps/web/src/stores/auth.ts: Zustand store also manages auth stateapps/web/src/utils/token-manager.ts:25: Also stores inlocalStoragewith different key (veza_access_token)
Root Cause: No single source of truth for token storage.
Fix Plan:
- Standardize on
TokenStorageclass as single source - Remove Zustand token storage (keep only
userandisAuthenticatedflags) - Remove
token-manager.tsor migrate toTokenStorage - Update all code to use
TokenStorage.getAccessToken()/TokenStorage.setTokens()
Tests: Test token persistence across page reloads, multiple tabs.
3. Type Mismatch: User.id (string vs number)
Severity: P0
Impact: Runtime errors when accessing user.id in TypeScript, type coercion bugs.
Evidence:
apps/web/src/types/api.ts:3:User.id: string✅apps/web/src/features/auth/types/index.ts:id: number❌ (inferred from usage)apps/web/src/services/api/auth.ts: Usesnumberin some places- Backend:
internal/dto/user_response.go:ID: uuid.UUID(serializes as string)
Root Cause: Inconsistent type definitions across frontend modules.
Fix Plan:
- Audit all
Usertype definitions, standardize onid: string - Update
apps/web/src/features/auth/types/index.tsto usestring - Add TypeScript strict mode checks
- Update Zod schemas to validate UUID format
Tests: TypeScript compilation, runtime validation.
4. Deprecated ApiService Expects Wrong Response Format
Severity: P0
Impact: Code using deprecated ApiService will break when backend response format changes.
Evidence:
apps/web/src/services/api.ts:238-239: Expects{ user, token }directly indata- Backend
handlers.Login(): Returns{ success: true, data: { user: {...}, token: {...} } }✅ apps/web/src/services/api.ts:74-80: Marked as@deprecatedbut still used in some places
Root Cause: Legacy code expects flat structure, new code expects nested.
Fix Plan:
- Complete migration from
ApiServicetoapiClient(seeMIGRATION_GUIDE.md) - Remove
ApiServiceclass entirely - Update all imports to use
apiClientfrom@/services/api/client
Tests: Verify all API calls use apiClient.
5. Missing CSRF Protection for State-Changing Operations
Severity: P0 (Security)
Impact: Vulnerable to CSRF attacks on POST/PUT/DELETE requests.
Evidence:
apps/web/src/services/csrf.ts:14-17:refreshCsrfToken()is a placeholder (not implemented)apps/web/src/services/secure-auth.ts:119: CallscsrfService.refreshCsrfToken()but it does nothing- Backend: No CSRF middleware found in
veza-backend-api veza-chat-server/src/security/csrf.rs: CSRF exists in chat server but not in backend API
Root Cause: CSRF protection not implemented for REST API.
Fix Plan:
- Implement CSRF token generation endpoint:
GET /api/v1/csrf-token - Backend: Add CSRF middleware for POST/PUT/DELETE (exclude GET, OPTIONS)
- Frontend: Fetch CSRF token on app init, include in
X-CSRF-Tokenheader - Update
apiClientinterceptor to add CSRF header
Tests: CSRF attack simulation, verify tokens validated.
Risk Register
Security Risks
| Risk | Severity | Evidence | Mitigation |
|---|---|---|---|
| CORS misconfiguration | High | Empty origins in prod → reject all | Document required env var, add validation |
| No CSRF protection | High | csrf.ts placeholder, no backend middleware |
Implement CSRF tokens (see P0 #5) |
| Tokens in localStorage | Medium | XSS can steal tokens | Consider httpOnly cookies (requires backend changes) |
| No SameSite cookie flags | Medium | Cookies not used, but if added later | Set SameSite=Lax for cookies |
| CORS allows credentials | Low | Access-Control-Allow-Credentials: true |
OK if origins whitelisted correctly |
Reliability Risks
| Risk | Severity | Evidence | Mitigation |
|---|---|---|---|
| Token storage desync | High | Multiple storage mechanisms | Standardize on TokenStorage (see P0 #2) |
| Type mismatches | High | User.id string vs number | Fix type definitions (see P0 #3) |
| Missing endpoints | Medium | 18 endpoints called but not implemented | Implement or remove frontend calls |
| Error parsing inconsistencies | Medium | Multiple error formats handled | Standardize on backend error format |
| No retry logic for 503/502 | Low | Handled in apiClient but no exponential backoff |
Add retry with backoff |
Drift Risks
| Risk | Severity | Evidence | Mitigation |
|---|---|---|---|
| Response format changes | High | Deprecated ApiService expects different format | Remove ApiService (see P0 #4) |
| Environment variable naming | Medium | VITE_API_BASE_URL vs VITE_API_URL |
Standardize on VITE_API_URL |
| Path mismatches | Medium | /users/:userId/profile vs /users/:id |
Align frontend paths with backend |
| Field name mismatches | Low | cover_art_path vs cover_art_url |
Document field mappings |
Recommended Fix Order (1-2 weeks)
Week 1: Critical Fixes
Day 1-2: CORS & Environment
- Add CORS validation on backend startup (fail if prod + empty origins)
- Document
CORS_ALLOWED_ORIGINSrequirement - Standardize env var names (
VITE_API_URLeverywhere) - Update deployment configs
Day 3-4: Auth Storage
- Audit all token storage usage
- Remove Zustand token storage, keep only flags
- Remove
token-manager.tsor migrate toTokenStorage - Test token persistence across reloads
Day 5: Type Safety
- Fix
User.idtype (string everywhere) - Update Zod schemas
- Run TypeScript strict checks
- Fix type errors
Week 2: API Contract & Security
Day 1-2: CSRF Protection
- Implement backend CSRF endpoint
- Add CSRF middleware
- Update frontend to fetch/include tokens
- Test CSRF protection
Day 3-4: Missing Endpoints
- Audit all frontend API calls
- Implement missing endpoints OR remove frontend calls
- Fix path mismatches (
/profilevs/users/:id) - Update API documentation
Day 5: Deprecated Code Removal
- Complete migration from
ApiServicetoapiClient - Remove
ApiServiceclass - Update all imports
- Verify no regressions
Appendix
Commands Executed
# Searched for route definitions
grep -r "router\.(GET|POST|PUT|DELETE)" veza-backend-api/internal/api/
# Searched for API calls
grep -r "apiClient\.(get|post|put|delete)" apps/web/src/
# Searched for environment variables
grep -r "VITE_" apps/web/
# Searched for CORS configuration
grep -r "CORS" veza-backend-api/internal/
Search Hits Summary
- Backend routes: 50+ endpoints found in
router.go - Frontend API calls: 100+ calls found across services
- Environment variables: 8
VITE_*vars found (inconsistent naming) - CORS config: 1 middleware, 1 config function
- Auth storage: 3 different mechanisms found
- Type definitions: 5+
Usertype definitions found
Logs Captured
No runtime logs captured (audit based on code analysis only).
End of Report