Skip to content

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

  1. Architecture Overview
  2. API Classification
  3. Communication Patterns
  4. Project Structure
  5. Deployment Guide
  6. Environment Configuration
  7. 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

# Deploy only functions with changes since last deploy
deno task deploy:changed

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

# Via CLI
vercel logs

# Via Dashboard:
# Vercel Dashboard → Deployments → Functions → View 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}` },
});