API Architecture Guide
Comprehensive guide to the API architecture, caching strategy, and best practices for the AI Agentification Platform.
Table of Contents
- Architecture Overview
- API Flow
- Caching Strategy
- Security Architecture
- Best Practices
- Performance Optimization
Architecture Overview
No Direct Client Calls Pattern
This application follows a strict pattern: all database operations go through the backend API, not through direct Supabase client calls from the frontend.
Frontend (React)
↓
API Calls (fetch)
↓
Backend API (Flask/Python)
↓
Supabase Service
↓
Supabase Database
Why This Architecture?
Security Benefits
- No Exposed Database Logic - Business logic stays on the server
- Token Validation - Backend verifies auth tokens before database access
- Centralized Access Control - All permissions checked in one place
- Rate Limiting - Can add rate limiting at API level
- Audit Logging - All database operations logged server-side
- SQL Injection Prevention - Backend properly sanitizes inputs
Performance Benefits
- Server-Side Caching - Cache at API level for all users
- Request Batching - Combine multiple queries into one endpoint
- Query Optimization - Optimize queries without frontend changes
- CDN Caching - Cache API responses at edge locations
Maintainability Benefits
- Single Source of Truth - All business logic in backend
- Easy to Update - Change logic without frontend updates
- Testing - Test business logic separately from UI
- Versioning - API versioning for backward compatibility
API Flow
File Structure
Backend API Endpoints
src/routes/
├── enrollments.py # Enrollment operations
├── waitlist.py # Waitlist operations
├── content.py # Content management
├── user.py # User management
└── notifications.py # Notification operations
Frontend Services (API Clients)
src/services/
├── enrollments.js # Enrollment API calls
├── waitlist.js # Waitlist API calls
├── novaTracks.js # Track data API calls
├── homepageContent.js # Content API calls
├── footerContent.js # Footer content API calls
├── marketingPages.js # Marketing pages API calls
└── legalContent.js # Legal content API calls
Exception: Authentication
The only place with direct Supabase calls is src/context/AuthContext.jsx:
- Why? Supabase Auth SDK handles sessions, OAuth, token refresh
- What's allowed: Login, signup, logout, session management
- What's not: Direct database queries (those still go through API)
Authentication Flow
1. User logs in via AuthContext
↓
2. Supabase Auth creates session
↓
3. Session token stored in browser
↓
4. Frontend gets token from session
↓
5. Frontend includes token in API requests
↓
6. Backend verifies token with Supabase Auth
↓
7. Backend performs database operations
↓
8. Backend returns data to frontend
Code Examples
Frontend (Getting Token)
import { supabase } from '../lib/supabaseClient';
const getAuthToken = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('Not authenticated');
}
return session.access_token;
};
Backend (Verifying Token)
auth_header = request.headers.get('Authorization')
token = auth_header.replace('Bearer ', '')
user_response = supabase_service.client.auth.get_user(token)
user_id = user_response.user.id
# Now use user_id for database operations
Caching Strategy
We use TanStack Query (React Query) for intelligent data caching and state management.
Cache Configuration
Global Defaults (src/main.jsx)
{
staleTime: 5 * 60 * 1000, // Data fresh for 5 minutes
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
refetchOnWindowFocus: false, // Don't refetch on tab focus
refetchOnReconnect: false, // Don't refetch on reconnect
retry: 1, // Only retry once on failure
}
Data-Specific Cache Times
| Data Type | Fresh Time | Cache Time | Rationale |
|---|---|---|---|
| Tracks/Courses | 5 min | 30 min | Content rarely changes, admins update manually |
| Lessons | 10 min | 1 hour | Curriculum is static once published |
| Dashboard | 2 min | 10 min | Progress updates frequently during learning |
| Course Progress | 1 min | 5 min | Updates as users watch videos |
Available Query Hooks
1. Tracks/Courses (src/hooks/queries/useTracks.js)
import { useTracks, useTrack } from '../src/hooks/queries/useTracks';
// Get all tracks (cached for 30 minutes)
const { data: tracks, isLoading, error } = useTracks(userEmail);
// Get single track by ID
const { data: track } = useTrack(trackId);
2. Dashboard Data (src/hooks/queries/useEnrollments.js)
import {
useDashboard,
useCourseProgress,
useInvalidateProgress
} from '../src/hooks/queries/useEnrollments';
// Get all dashboard data (enrollments, lessons, progress)
const { data: dashboardData, isLoading } = useDashboard(userId);
// Get progress for specific course
const { data: progress } = useCourseProgress(userId, trackId);
// Invalidate progress cache after video watching
const invalidateProgress = useInvalidateProgress();
invalidateProgress.mutate({ userId, trackId });
3. Lessons (src/hooks/queries/useLessons.js)
import { useLessons } from '../src/hooks/queries/useLessons';
// Get lessons for a track (cached for 1 hour)
const { data: lessons, isLoading } = useLessons(trackId);
Benefits
Performance
- Instant page loads - Cached data displays immediately
- Reduced API calls - 80-90% fewer database queries
- Background updates - Data refreshes silently in the background
Cost Savings
- Lower server load - Fewer requests to Supabase
- Reduced bandwidth - Only fetch when data is stale
- Database efficiency - Fewer read operations
Better UX
- No loading spinners - Stale data shows while fetching fresh data
- Smooth navigation - Instant transitions between pages
- Offline resilience - Cached data available even with poor connection
Cache Invalidation
Automatic Invalidation
TanStack Query automatically invalidates based on:
staleTimeexpiry- Query key changes (e.g., different user/course)
Manual Invalidation
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['tracks'] });
// Invalidate all dashboard queries
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
// Refetch immediately
queryClient.refetchQueries({ queryKey: ['courseProgress', userId, trackId] });
Development Tools
React Query Devtools
The devtools are automatically included in development mode. Click the TanStack Query icon in the bottom-right corner to:
- View all cached queries
- See query status (fresh, stale, fetching)
- Inspect query data
- Manually invalidate queries
- Monitor network requests
Security Architecture
API Endpoint Patterns
What to Avoid: Direct Supabase Client Calls
// ❌ BAD - DON'T DO THIS
import { supabase } from '../lib/supabaseClient';
const fetchData = async () => {
const { data } = await supabase
.from('course_enrollments')
.select('*')
.eq('user_id', userId);
return data;
};
What to Use: API Endpoint Calls
// ✅ GOOD - DO THIS INSTEAD
const fetchData = async () => {
const token = await getAuthToken();
const response = await fetch('/api/enrollments/dashboard', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
const data = await response.json();
return data;
};
Authentication Pattern
All endpoints (except public content) require authentication via Bearer token:
# Backend token verification pattern
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Authorization required'}), 401
token = auth_header.replace('Bearer ', '')
try:
user_response = supabase_service.client.auth.get_user(token)
if not user_response or not user_response.user:
return jsonify({'error': 'Invalid or expired token'}), 401
user_id = user_response.user.id
except Exception as e:
return jsonify({'error': 'Authentication failed'}), 401
Service Role Key Usage
The backend uses Supabase Service Role Key for database operations:
# In SupabaseService.__init__
# Service role key bypasses RLS (use carefully!)
self.client = create_client(self.url, self.service_key)
Security Notes:
- Service Role Key is ONLY used in backend
- NEVER expose Service Role Key to frontend
- Frontend uses Anon Key only for authentication
- Backend validates all auth tokens before database access
Row Level Security (RLS)
Even though backend uses Service Role Key, implement RLS policies as defense in depth:
-- Example: Users can only see their own enrollments
CREATE POLICY "Users can view own enrollments"
ON course_enrollments FOR SELECT
USING (auth.uid() = user_id);
-- Example: Only admins can insert content
CREATE POLICY "Admins can insert content"
ON content FOR INSERT
WITH CHECK (
auth.uid() IN (
SELECT id FROM auth.users
WHERE raw_user_meta_data->>'role' = 'admin'
)
);
Best Practices
1. Use Specific Query Keys
// Good - specific keys
['tracks', userEmail]
['courseProgress', userId, trackId]
// Bad - generic keys
['data']
['api']
2. Set Appropriate Stale Times
// Static data - long stale time
staleTime: 30 * 60 * 1000 // 30 minutes
// Dynamic data - short stale time
staleTime: 1 * 60 * 1000 // 1 minute
3. Handle Loading States
const { data, isLoading, error } = useTracks();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <TracksList tracks={data} />;
4. Provide Default Values
// Prevent undefined errors
const { data: tracks = [] } = useTracks();
const { data: progress = {} } = useCourseProgress();
5. Implement Error Boundaries
// Wrap components in error boundaries
<ErrorBoundary fallback={<ErrorFallback />}>
<DashboardContent />
</ErrorBoundary>
6. Validate Input on Backend
from src.utils.validation import validate_enrollment_data
is_valid, error_msg, sanitized_data = validate_enrollment_data(data)
if not is_valid:
return jsonify({'error': error_msg}), 400
# Use sanitized data (prevents injection attacks)
track_id = sanitized_data['trackId']
Performance Optimization
1. Request Batching
Combine multiple requests into single endpoints:
# Instead of separate endpoints for enrollments and resources
# Create a combined dashboard endpoint
@blueprint.route('/dashboard', methods=['GET'])
def get_dashboard():
enrollments = get_user_enrollments(user_id)
resources = get_course_resources([e['track_id'] for e in enrollments])
return jsonify({
'enrollments': enrollments,
'resources': resources
})
2. Selective Data Loading
Only fetch what you need:
# Don't fetch all columns if you only need a few
response = client.table('content').select('id, title, slug').execute()
# Use joins to avoid N+1 queries
response = client.table('enrollments').select(
'id, status, track:track_id(id, title, description)'
).execute()
3. Database Indexes
Add indexes for frequently queried columns:
-- Index on user_id for faster enrollment queries
CREATE INDEX idx_enrollments_user_id ON course_enrollments(user_id);
-- Index on track_id for faster resource queries
CREATE INDEX idx_resources_track_id ON course_resources(track_id);
-- Composite index for common query patterns
CREATE INDEX idx_enrollments_user_track
ON course_enrollments(user_id, track_id);
4. Response Compression
Enable compression in Flask:
from flask_compress import Compress
app = Flask(__name__)
Compress(app) # Automatically compresses responses > 500 bytes
5. CDN Caching
Configure Vercel to cache API responses:
// In API route, set cache headers
export const config = {
runtime: 'edge',
regions: ['iad1'], // or your preferred region
};
// In response
headers: {
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600'
}
Migration Status
Migrated to TanStack Query
- CoursesPage -
/courses - DashboardPage -
/dashboard
TODO
- CourseDetailPage -
/courses/:slug - CourseViewPage -
/courses/:id/view - WaitlistPage -
/waitlist
Implementation Checklist
For Each New Feature:
Backend (Python/Flask)
- Create route in
/src/routes/[feature].py - Add authentication check (verify token)
- Use
SupabaseServicefor database operations - Add proper error handling
- Register blueprint in
main.py
Frontend (React/JavaScript)
- Create service file in
/src/services/[feature].js - Use
fetch()to call backend API - Include auth token in headers
- Handle errors properly
- NO direct
supabase.from()calls
Verification Checklist
- All
/src/services/*.jsfiles usefetch()notsupabase.from() - Only
AuthContext.jsxusessupabase.auth.* - All database operations have backend API endpoints
- All API calls include
Authorizationheader - Backend verifies tokens before database access
- No RLS policies bypassed inappropriately
- Error handling implemented end-to-end
Testing
Audit for Direct Calls
# Find all direct Supabase client imports
grep -r "from.*supabaseClient" src/
# Find direct database calls
grep -r "supabase\.from(" src/
# Find direct RPC calls
grep -r "supabase\.rpc(" src/
Expected Results
- ✅
src/context/AuthContext.jsx- Auth operations only - ✅
src/lib/supabaseClient.js- Client initialization only - ❌ No other files should import or use supabase client
Test API Endpoints
# Get auth token
TOKEN="your_token_here"
# Test dashboard
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:5001/api/enrollments/dashboard
# Test enrollment
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trackId":"track-id","pricingTier":"free"}' \
http://localhost:5001/api/enrollments
Additional Resources
- Backend Developer Guide - Python/Flask patterns
- Component Library - Frontend components
- TanStack Query Docs
- Supabase Python Client Docs
Summary
Architecture Benefits:
| Aspect | Direct Supabase | Through API |
|---|---|---|
| Security | ❌ Exposed logic | ✅ Hidden logic |
| Scalability | ❌ Hard to change | ✅ Easy to modify |
| Monitoring | ❌ Client-side only | ✅ Server logging |
| Rate Limiting | ❌ Limited | ✅ Full control |
| Caching | ❌ Browser only | ✅ Server & CDN |
| Validation | ❌ Client-side | ✅ Server-side |
Last Updated: December 2025 Architecture Status: ✅ All database calls through API Caching Status: ✅ TanStack Query implemented