Hybrid Architecture Guide¶
Domain APIs on Supabase Edge + Experience APIs on Vercel
This guide documents the hybrid architecture where database-intensive Domain APIs run on Supabase Edge Functions (close to the database), while Experience/BFF APIs run on Vercel (close to the UI).
Table of Contents¶
- Architecture Overview
- API Classification
- Communication Patterns
- Project Structure
- Deployment Guide
- Environment Configuration
- Monitoring & Troubleshooting
Architecture Overview¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER CLIENTS │
│ Web (Vercel) │ Android (Future) │ iOS (Future) │
└─────────────────┬─────────────────────┬────────────────────┬────────────────┘
│ │ │
▼ │ │
┌─────────────────────────────────────────────────────────────────────────────┐
│ VERCEL (Experience Layer / BFF) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Next.js App (apps/vercel-api) │ │
│ │ └── app/api/ │ │
│ │ ├── discovery/domains/ → Proxies to Edge: /discovery/domains │ │
│ │ ├── discovery/browse/ → Orchestrates multiple Edge calls │ │
│ │ └── metadata/ → Static response (no DB) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ Runtime: Node.js 18+ │ Cold Start: ~250-500ms │ Best for: orchestration │
└─────────────────────────────┬───────────────────────────────────────────────┘
│ HTTPS calls to Edge Functions
│ Latency: ~50-100ms per call
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ SUPABASE EDGE (Domain Layer) │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Edge Functions (supabase/functions/*) │ │
│ │ ├── discovery/ Public content browsing │ │
│ │ ├── metadata/ Static enumerations │ │
│ │ ├── search/ Full-text search (tsvector) │ │
│ │ ├── graph/ DAG traversal, learning paths │ │
│ │ ├── user/ User profile, progress, events │ │
│ │ ├── snapshots/ Content versioning │ │
│ │ └── browse/ (Deprecated - use discovery) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ Runtime: Deno (V8) │ Cold Start: ~50ms │ Best for: heavy DB queries │
│ │ │
│ ┌───────────────────────────▼───────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ - Latency from Edge: ~1ms │ │
│ │ - Full-text search (tsvector) │ │
│ │ - RLS policies for security │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ (Mobile clients call Domain APIs directly)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ MOBILE CLIENTS (Future - Direct to Domain APIs) │
│ Android/iOS apps call: https://<project>.supabase.co/functions/v1/* │
│ - No BFF needed for mobile (they have their own UX logic) │
│ - Lower latency (skip Vercel hop) │
└─────────────────────────────────────────────────────────────────────────────┘
Why This Split?¶
| Layer | Location | Reasoning |
|---|---|---|
| Domain APIs | Supabase Edge | ~1ms DB latency, 50ms cold starts, efficient connection pooling |
| Experience APIs | Vercel | Unified deployment with UI, PR previews, Edge caching |
API Classification¶
Domain APIs (Supabase Edge Functions)¶
These APIs perform direct database operations and benefit from proximity to PostgreSQL:
| Endpoint | Auth | Purpose | DB Operations |
|---|---|---|---|
/discovery |
Public | Browse domains, trails, concepts, sparks | Multiple table queries with joins |
/metadata |
Public | Static enumerations | None (cached) |
/search |
JWT | Full-text search | tsvector queries (CPU-intensive) |
/graph |
JWT | DAG traversal, prerequisites | Recursive graph queries (10-50+ calls) |
/user |
JWT | Profile, stats, progress | User tables with RLS |
/snapshots |
JWT | Content versioning | Metadata queries |
Base URL: https://<project-ref>.supabase.co/functions/v1
Experience APIs (Vercel)¶
These APIs orchestrate Domain API calls and add UI-specific transformations:
| Endpoint | Auth | Purpose | Calls |
|---|---|---|---|
/api/discovery/domains |
Optional | List domains with caching | Edge: /discovery/domains |
/api/discovery/browse |
Optional | Aggregated browse view | Edge: /discovery/domains + /discovery/trails |
/api/metadata |
Public | Static enums | None (static response) |
Base URL: https://<your-domain>.vercel.app or custom domain
Communication Patterns¶
Pattern 1: Web App → Experience API → Domain API¶
┌──────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌────────┐
│ Web UI │────▶│ Vercel BFF │────▶│ Supabase Edge │────▶│ DB │
│ │ │ /api/discovery │ │ /discovery │ │ │
└──────────┘ └─────────────────┘ └──────────────────┘ └────────┘
Browser ~250ms ~50ms ~1ms
Use for: Discovery, browse, metadata (aggregated views)
Pattern 2: Web App → Domain API (Direct)¶
┌──────────┐ ┌──────────────────┐ ┌────────┐
│ Web UI │────▶│ Supabase Edge │────▶│ DB │
│ │ │ /search, /graph │ │ │
└──────────┘ └──────────────────┘ └────────┘
Browser ~50ms ~1ms
Use for: Search, graph (performance-critical, direct DB access)
Pattern 3: Mobile App → Domain API (Direct)¶
┌──────────┐ ┌──────────────────┐ ┌────────┐
│ Mobile │────▶│ Supabase Edge │────▶│ DB │
│ App │ │ /user, /search │ │ │
└──────────┘ └──────────────────┘ └────────┘
Native ~50ms ~1ms
Use for: All mobile API calls (no BFF layer needed)
Request Flow Example: Discovery Browse¶
// 1. User visits browse page
// 2. Web app calls Vercel Experience API
const response = await fetch('/api/discovery/browse?domain=ml');
// 3. Vercel BFF orchestrates parallel calls to Edge
const [domains, trails] = await Promise.all([
domainClient.listDomains({ snapshot_id }),
domainClient.listTrails({ domain: 'ml', snapshot_id }),
]);
// 4. BFF aggregates and returns
return {
domains: domains.data,
trails: trails.data,
meta: { ...trails.meta, domains_count: domains.meta.total },
};
Project Structure¶
core/
├── packages/ # Monorepo shared packages (Node.js)
│ ├── shared/ # Schemas, types, utilities
│ │ ├── src/
│ │ │ ├── schemas.ts # Zod schemas
│ │ │ ├── types/ # TypeScript types
│ │ │ └── utils/ # Shared utilities
│ │ └── package.json
│ │
│ ├── domain-apis/ # Domain API logic (for reference)
│ │ ├── src/
│ │ │ ├── routes/ # Route definitions
│ │ │ └── services/ # Business logic
│ │ └── package.json
│ │
│ └── experience-apis/ # Experience/BFF layer
│ ├── src/
│ │ ├── routes/ # BFF route logic
│ │ └── clients/ # Domain API HTTP client
│ └── package.json
│
├── apps/
│ └── vercel-api/ # Vercel Next.js deployment
│ ├── src/app/api/ # API routes
│ │ ├── discovery/
│ │ │ ├── domains/route.ts
│ │ │ └── browse/route.ts
│ │ └── metadata/route.ts
│ ├── next.config.mjs
│ └── package.json
│
├── src/ # Deno source code (Edge Functions)
│ ├── app.ts # Main Hono application
│ ├── routes/ # API route handlers
│ ├── services/ # Business logic
│ ├── schemas/ # Zod schemas
│ └── utils/ # Utilities
│
├── supabase/
│ └── functions/ # Edge Function entry points
│ ├── discovery/index.ts
│ ├── search/index.ts
│ ├── graph/index.ts
│ ├── user/index.ts
│ ├── snapshots/index.ts
│ ├── metadata/index.ts
│ └── _shared/ # Shared Edge Function code
│
├── pnpm-workspace.yaml # Monorepo workspace config
└── deno.json # Deno configuration
Key Distinction: Deno vs Node.js¶
| Component | Runtime | Package Source |
|---|---|---|
Edge Functions (supabase/functions/) |
Deno | src/ directory |
Vercel API (apps/vercel-api/) |
Node.js | packages/* (monorepo) |
Shared packages (packages/) |
Node.js | npm workspaces |
Important: Edge Functions use the src/ Deno code directly. The packages/ directory is for Node.js consumers (Vercel, future services).
Deployment Guide¶
Deploying Supabase Edge Functions¶
Prerequisites¶
# Install Supabase CLI
brew install supabase/tap/supabase
# Login to Supabase
supabase login
# Link to your project
supabase link --project-ref <your-project-ref>
Deploy All Functions¶
# Build SDK first (required for shared code)
cd sdk && npm run build && cd ..
# Deploy all functions
deno task deploy:all
Deploy Individual Functions¶
# Deploy specific function
deno task deploy:discovery
deno task deploy:search
deno task deploy:graph
deno task deploy:user
deno task deploy:snapshots
deno task deploy:metadata
Deploy Changed Functions Only¶
Verify Deployment¶
# Health check all functions
deno task check:functions
# Expected output:
# ✅ discovery deployed healthy
# ✅ metadata deployed healthy
# ❌ search deployed error: HTTP 401 (expected - requires auth)
# ❌ graph deployed error: HTTP 401 (expected - requires auth)
# ❌ user deployed error: HTTP 401 (expected - requires auth)
# ❌ snapshots deployed error: HTTP 401 (expected - requires auth)
Deploying Vercel Experience APIs¶
Prerequisites¶
# Install Vercel CLI
npm i -g vercel
# Login to Vercel
vercel login
# Link to your project (run from apps/vercel-api)
cd apps/vercel-api
vercel link
Environment Variables¶
Set these in Vercel Dashboard → Project → Settings → Environment Variables:
| Variable | Description | Required |
|---|---|---|
SUPABASE_URL |
Supabase project URL | Yes |
SUPABASE_ANON_KEY |
Supabase anonymous key | Yes |
Deploy to Preview¶
# From apps/vercel-api directory
vercel
# Or from root with pnpm
pnpm --filter @microlearn/vercel-api run deploy
Deploy to Production¶
# From apps/vercel-api directory
vercel --prod
# Or via git push (if connected to GitHub)
git push origin main
Automatic Deployments¶
When connected to GitHub:
- Preview: Every PR gets a preview deployment
- Production: Merges to main trigger production deployment
Deployment Checklist¶
Before Deploying Edge Functions¶
- Run type checks:
deno task check - Run linting:
deno task lint - Build SDK if schema changes:
cd sdk && npm run build - Test locally:
deno task dev
Before Deploying Vercel APIs¶
- Build packages:
pnpm run build(from root) - Test locally:
cd apps/vercel-api && npm run dev - Verify environment variables are set in Vercel Dashboard
Rollback Procedures¶
Rollback Edge Functions¶
# List function versions
supabase functions list
# Edge functions don't have built-in rollback
# Redeploy previous version from git:
git checkout <previous-commit>
deno task deploy:<function-name>
git checkout main
Rollback Vercel¶
# Via CLI
vercel rollback
# Or via Dashboard:
# Vercel Dashboard → Deployments → Select previous → Promote to Production
Environment Configuration¶
Edge Functions (Supabase)¶
Environment variables are set via Supabase Dashboard or CLI:
# Set secret via CLI
supabase secrets set MY_SECRET=value
# Or via Dashboard:
# Supabase Dashboard → Edge Functions → Secrets
Required Variables:
- SUPABASE_URL (auto-injected)
- SUPABASE_ANON_KEY (auto-injected)
- SUPABASE_SERVICE_ROLE_KEY (for admin operations)
Vercel APIs¶
Set via Vercel Dashboard → Project → Settings → Environment Variables
Required Variables:
| Variable | Environment | Description |
|---|---|---|
SUPABASE_URL |
All | Supabase project URL |
SUPABASE_ANON_KEY |
All | Anonymous API key |
Optional Variables:
| Variable | Environment | Description |
|---|---|---|
LOG_LEVEL |
All | debug, info, warn, error |
CACHE_TTL |
Production | Response cache TTL in seconds |
Local Development¶
Create .env files for local development:
# .env (Edge Functions)
SUPABASE_URL=https://<project>.supabase.co
SUPABASE_ANON_KEY=<anon-key>
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
LOG_LEVEL=debug
LOG_FORMAT=pretty
# apps/vercel-api/.env.local (Vercel APIs)
SUPABASE_URL=https://<project>.supabase.co
SUPABASE_ANON_KEY=<anon-key>
Monitoring & Troubleshooting¶
Viewing Logs¶
Edge Function Logs¶
# Via CLI (live tail)
supabase functions logs <function-name> --tail
# Via Dashboard:
# Supabase Dashboard → Edge Functions → Logs
Vercel API Logs¶
Common Issues¶
1. Edge Function 401 Unauthorized¶
Cause: Missing or invalid JWT token
Solution:
# Get a test token
deno task api:token
# Include in request
curl -H "Authorization: Bearer <token>" \
https://<project>.supabase.co/functions/v1/user/me
2. Vercel API 500 Internal Error¶
Cause: Missing environment variables
Solution:
- Check Vercel Dashboard → Settings → Environment Variables
- Ensure SUPABASE_URL and SUPABASE_ANON_KEY are set
- Redeploy after adding variables
3. Edge Function Cold Start Timeout¶
Cause: Function taking too long on first request
Solution: - Keep functions warm with periodic health checks - Optimize initialization code - Use connection pooling
4. Vercel Build Fails with 404/500 Errors¶
Cause: pnpm monorepo compatibility with Next.js static generation
Solution:
- These are cosmetic warnings for error pages
- API routes still work correctly
- The next.config.mjs has eslint.ignoreDuringBuilds: true
Health Check Endpoints¶
| Endpoint | Expected Response | Notes |
|---|---|---|
GET /functions/v1/discovery/domains |
200 + data | Public endpoint |
GET /functions/v1/metadata |
200 + enums | Public endpoint |
GET /functions/v1/search |
401 | Requires auth |
GET /api/metadata (Vercel) |
200 + enums | Public endpoint |
GET /api/discovery/domains (Vercel) |
200 + data | Public endpoint |
Performance Benchmarks¶
| Endpoint | Cold Start | Warm Response | DB Queries |
|---|---|---|---|
Edge: /discovery/domains |
~50ms | ~20ms | 1 |
Edge: /search |
~60ms | ~30ms | 1 (tsvector) |
Edge: /graph/sparks/{slug} |
~80ms | ~40ms | 10-50+ |
Vercel: /api/discovery/browse |
~250ms | ~100ms | 2 (via Edge) |
Quick Reference¶
URLs¶
| Service | URL Pattern |
|---|---|
| Edge Functions | https://<project-ref>.supabase.co/functions/v1/<function> |
| Vercel APIs | https://<domain>.vercel.app/api/<path> |
| Vercel Preview | https://<deployment-id>.vercel.app/api/<path> |
Commands¶
# Edge Functions
deno task check # Type check
deno task dev # Local development
deno task deploy:all # Deploy all functions
deno task check:functions # Health check
# Vercel APIs
cd apps/vercel-api
npm run dev # Local development
npm run build # Build
vercel # Deploy preview
vercel --prod # Deploy production
# Monorepo
pnpm install # Install all dependencies
pnpm run build # Build all packages
API Routing Strategy (Web App)¶
// Experience APIs - go through Vercel BFF (same origin, caching)
const discovery = await fetch('/api/discovery/domains');
const browse = await fetch('/api/browse?domain=ml');
const metadata = await fetch('/api/metadata');
// Domain APIs - go direct to Edge (performance-critical)
const search = await fetch(`${SUPABASE_URL}/functions/v1/search?q=react`, {
headers: { Authorization: `Bearer ${token}` },
});
const graph = await fetch(`${SUPABASE_URL}/functions/v1/graph/sparks/xyz`, {
headers: { Authorization: `Bearer ${token}` },
});