Skip to main content

API Architecture Guide

Comprehensive guide to the API architecture, caching strategy, and best practices for the AI Agentification Platform.

Table of Contents

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

  1. No Exposed Database Logic - Business logic stays on the server
  2. Token Validation - Backend verifies auth tokens before database access
  3. Centralized Access Control - All permissions checked in one place
  4. Rate Limiting - Can add rate limiting at API level
  5. Audit Logging - All database operations logged server-side
  6. SQL Injection Prevention - Backend properly sanitizes inputs

Performance Benefits

  1. Server-Side Caching - Cache at API level for all users
  2. Request Batching - Combine multiple queries into one endpoint
  3. Query Optimization - Optimize queries without frontend changes
  4. CDN Caching - Cache API responses at edge locations

Maintainability Benefits

  1. Single Source of Truth - All business logic in backend
  2. Easy to Update - Change logic without frontend updates
  3. Testing - Test business logic separately from UI
  4. 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 TypeFresh TimeCache TimeRationale
Tracks/Courses5 min30 minContent rarely changes, admins update manually
Lessons10 min1 hourCurriculum is static once published
Dashboard2 min10 minProgress updates frequently during learning
Course Progress1 min5 minUpdates 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:

  • staleTime expiry
  • 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 SupabaseService for 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/*.js files use fetch() not supabase.from()
  • Only AuthContext.jsx uses supabase.auth.*
  • All database operations have backend API endpoints
  • All API calls include Authorization header
  • 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

Summary

Architecture Benefits:

AspectDirect SupabaseThrough 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