Technical Architecture

BlackOps Center is a multi-tenant SaaS platform built on modern web technologies. This document explains our architecture, design decisions, and how the system works under the hood.

High-Level Architecture

┌─────────────────────────────────────────────────────┐
│                  CLIENT LAYER                       │
│  ┌──────────┐  ┌──────────┐  ┌─────────────────┐  │
│  │  Web App │  │  Mobile  │  │ Browser         │  │
│  │  (Next.js)│  │  (PWA)  │  │ Extension       │  │
│  └─────┬────┘  └─────┬────┘  └────────┬────────┘  │
└────────┼─────────────┼────────────────┼───────────┘
         │             │                │
         └─────────────┼────────────────┘
                       │
         ┌─────────────▼────────────────┐
         │      API LAYER (Next.js)     │
         │  ┌────────────────────────┐  │
         │  │  REST API Routes       │  │
         │  │  /api/*                │  │
         │  └────────────────────────┘  │
         │  ┌────────────────────────┐  │
         │  │  Webhook Handlers      │  │
         │  │  /api/webhooks/*       │  │
         │  └────────────────────────┘  │
         └──────────┬──────────────────┘
                    │
         ┌──────────▼──────────────────┐
         │    DATA & SERVICES LAYER    │
         │  ┌───────┐  ┌────────────┐  │
         │  │Supabase│  │  External  │  │
         │  │(Postgres)│  │  Services │  │
         │  │  + RLS │  │  (AI, etc.)│  │
         │  └───────┘  └────────────┘  │
         └─────────────────────────────┘
                    │
         ┌──────────▼──────────────────┐
         │     INFRASTRUCTURE          │
         │  ┌────────┐  ┌───────────┐  │
         │  │ Vercel │  │   CDN     │  │
         │  │(Edge)  │  │(Images)   │  │
         │  └────────┘  └───────────┘  │
         └─────────────────────────────┘

Tech Stack

Frontend

  • Framework: Next.js 14 (App Router)
  • Language: TypeScript
  • Styling: Tailwind CSS
  • UI Components: Custom + Radix UI primitives
  • State Management: React Server Components + client state
  • Forms: React Hook Form + Zod validation

Backend

  • API: Next.js API Routes (serverless)
  • Database: PostgreSQL (via Supabase)
  • Auth: Supabase Auth (magic links, OAuth)
  • Storage: Supabase Storage (images, files)
  • Email: Resend (transactional + inbound)
  • Payments: Stripe (subscriptions + one-time)

AI & ML

  • LLM: OpenAI GPT-4 / Claude 3.5 Sonnet
  • Embeddings: OpenAI text-embedding-3-small
  • Vector Search: pgvector (Postgres extension)

Infrastructure

  • Hosting: Vercel (Next.js + Edge Functions)
  • Database: Supabase (managed Postgres)
  • CDN: Vercel Edge Network
  • Monitoring: Vercel Analytics + Sentry

Multi-Tenancy Architecture

Row-Level Security (RLS)

BlackOps Center uses PostgreSQL's Row-Level Security to enforce tenant isolation at the database level. Every table has a site_id column, and RLS policies automatically filter queries.

-- Example RLS policy
CREATE POLICY "Users see only their site's posts"
  ON posts
  FOR ALL
  USING (site_id = current_setting('app.current_site_id')::uuid);

-- Every query is automatically filtered
SELECT * FROM posts; 
-- Becomes: SELECT * FROM posts WHERE site_id = 'abc-123'

-- This prevents cross-tenant data leaks at DB level

Site Context

Site context flows through the request chain:

1. Request arrives at middleware
   → Domain/subdomain identifies site
   → Site ID added to request headers

2. API route extracts site context
   → Validates user has access to site
   → Sets Supabase RLS context

3. Database queries automatically filtered
   → All queries scoped to site_id
   → Cross-tenant access impossible

4. Response returned
   → Only site-specific data included

Data Isolation

Multi-tenant isolation strategy:

  • Shared Schema: All tenants use same tables (cost-efficient)
  • RLS Enforcement: Database-level security (bulletproof)
  • API Validation: Additional checks in application layer (defense in depth)
  • Tenant Context: Passed via headers, validated every request

🔒 Security Considerations

Multi-tenancy requires vigilance. We've audited 128+ API routes to ensure proper site_id filtering. Key principle: Never trust client-supplied site_id. Always derive from authenticated user context.

Database Schema

Core Tables

-- Sites (tenants)
sites
├─ id (uuid, PK)
├─ owner_id (uuid, FK → auth.users)
├─ slug (text, unique)
├─ domain (text, unique)
├─ subdomain (text, unique)
├─ settings (jsonb)
└─ created_at (timestamptz)

-- Site users (team members)
site_users
├─ id (uuid, PK)
├─ site_id (uuid, FK → sites)
├─ user_id (uuid, FK → auth.users)
├─ role (text: owner|admin|editor|viewer)
└─ created_at (timestamptz)

-- Content reservoirs
content_reservoirs
├─ id (uuid, PK)
├─ site_id (uuid, FK → sites) ← RLS
├─ name (text)
├─ description (text)
├─ email_address (text, unique)
├─ settings (jsonb)
└─ created_at (timestamptz)

-- Reservoir items
reservoir_items
├─ id (uuid, PK)
├─ reservoir_id (uuid, FK → content_reservoirs)
├─ site_id (uuid, FK → sites) ← RLS
├─ title (text)
├─ content (text)
├─ url (text)
├─ source_type (text)
├─ embedding (vector(1536)) ← pgvector
├─ tags (text[])
└─ created_at (timestamptz)

-- Blog posts
posts
├─ id (uuid, PK)
├─ site_id (uuid, FK → sites) ← RLS
├─ slug (text)
├─ title (text)
├─ content (text)
├─ status (text: draft|published)
├─ published_at (timestamptz)
└─ created_at (timestamptz)

-- Newsletters
newsletters
├─ id (uuid, PK)
├─ site_id (uuid, FK → sites) ← RLS
├─ name (text)
├─ from_email (text)
└─ created_at (timestamptz)

-- Newsletter subscribers
newsletter_subscribers
├─ id (uuid, PK)
├─ newsletter_id (uuid, FK → newsletters)
├─ site_id (uuid, FK → sites) ← RLS
├─ email (text)
├─ status (text: active|unsubscribed|bounced)
└─ subscribed_at (timestamptz)

-- Subscriptions (billing)
subscriptions
├─ id (uuid, PK)
├─ user_id (uuid, FK → auth.users)
├─ site_id (uuid, FK → sites, nullable)
├─ stripe_subscription_id (text)
├─ plan (text)
├─ sites_count (int)
├─ status (text)
└─ created_at (timestamptz)

Vector Search with pgvector

Reservoir items are embedded for semantic search:

-- Add pgvector extension
CREATE EXTENSION vector;

-- Embedding column
ALTER TABLE reservoir_items
  ADD COLUMN embedding vector(1536);

-- Cosine similarity search
SELECT 
  id, 
  title,
  1 - (embedding <=> query_embedding) AS similarity
FROM reservoir_items
WHERE site_id = 'abc-123'
ORDER BY embedding <=> query_embedding
LIMIT 10;

-- Index for performance
CREATE INDEX ON reservoir_items 
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

Authentication & Authorization

Authentication Flow

1. User visits /admin
   ↓
2. Middleware checks session
   ↓
3. If not authenticated:
   → Redirect to /signin
   ↓
4. User signs in (Supabase Auth):
   → Magic link (email)
   → OAuth (Google, GitHub)
   → Password (optional)
   ↓
5. Session created (JWT in cookie)
   ↓
6. Redirect back to /admin
   ↓
7. User sees their sites

Authorization Model

// Authorization check in API route
export async function POST(request: NextRequest) {
  // 1. Verify user is authenticated
  const user = await getUser(request)
  if (!user) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }
  
  // 2. Get site context
  const siteId = request.headers.get('x-site-id')
  
  // 3. Verify user has access to site
  const { data: siteAccess } = await supabase
    .from('site_users')
    .select('role')
    .eq('user_id', user.id)
    .eq('site_id', siteId)
    .single()
  
  if (!siteAccess) {
    return NextResponse.json(
      { error: 'Forbidden' },
      { status: 403 }
    )
  }
  
  // 4. Check role permissions
  if (siteAccess.role === 'viewer') {
    return NextResponse.json(
      { error: 'Insufficient permissions' },
      { status: 403 }
    )
  }
  
  // 5. Proceed with request
  // ...
}

Role Permissions

const PERMISSIONS = {
  owner: ['*'], // Full access
  
  admin: [
    'posts:read',
    'posts:write',
    'posts:publish',
    'reservoirs:read',
    'reservoirs:write',
    'team:manage',
    'settings:write'
  ],
  
  editor: [
    'posts:read',
    'posts:write',
    'posts:publish',
    'reservoirs:read',
    'reservoirs:write'
  ],
  
  viewer: [
    'posts:read',
    'reservoirs:read'
  ]
}

Content Generation Pipeline

AI Content Generation Flow

1. User triggers generation
   → "Generate blog post from reservoir"
   ↓
2. API route validates request
   → User has access to site?
   → Sufficient credits?
   ↓
3. Fetch relevant reservoir items
   → Vector search for semantic relevance
   → Filter by tags, date, score
   ↓
4. Construct AI prompt
   → System prompt (tone, style)
   → Reservoir context (research)
   → User instructions (topic, angle)
   ↓
5. Call LLM API (streaming)
   → OpenAI or Anthropic
   → Stream response to client
   ↓
6. Post-process output
   → Markdown formatting
   → Link insertion
   → SEO optimization
   ↓
7. Save as draft
   → Store in posts table
   → Deduct credits
   ↓
8. User reviews and publishes

Prompt Engineering

// System prompt (defines voice/style)
const systemPrompt = `
You are Ben Newton, a technical content creator who writes 
about web development, React, and content strategy. Your 
writing is:
- Direct and practical (no fluff)
- Technically accurate but accessible
- Conversational yet authoritative
- Heavy on examples and code snippets

Write a blog post based on the research provided.
`

// User prompt (specific request)
const userPrompt = `
Topic: React Server Components Performance

Research context:
${reservoirItems.map(item => `
  - ${item.title}: ${item.content}
`).join('\n')}

Structure:
1. Hook: Why RSC performance matters
2. Common performance mistakes
3. Optimization techniques (with code)
4. Real-world benchmark results
5. Best practices

Target: 2,500 words, intermediate developers
`

// Call LLM
const stream = await openai.chat.completions.create({
  model: 'gpt-4',
  messages: [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: userPrompt }
  ],
  stream: true
})

RSS & Content Monitoring

RSS Processing Pipeline

1. Cron job runs every hour
   ↓
2. Fetch active RSS feeds
   → WHERE is_active = true
   → WHERE last_fetched < now() - interval
   ↓
3. For each feed:
   a. Fetch feed XML
   b. Parse items
   c. Check for duplicates (by GUID)
   d. Filter by language preferences
   e. Extract metadata (title, content, image)
   ↓
4. Enrich items:
   a. Fetch missing images (OpenGraph)
   b. Calculate keyword relevance
   c. Analyze sentiment
   ↓
5. AI reservoir scoring (async):
   a. Generate embedding for item
   b. Compare to all reservoirs
   c. Calculate match scores
   d. Store scores in item metadata
   ↓
6. Save to database
   → INSERT INTO rss_feed_items
   → UPDATE feed last_fetched
   ↓
7. User sees scored items in feed reader

AI Reservoir Scoring

// Score content against reservoirs
async function scoreContent(item, reservoirs) {
  // 1. Generate embedding for item
  const embedding = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: `${item.title}\n\n${item.content}`
  })
  
  // 2. Vector search against each reservoir
  const scores = await Promise.all(
    reservoirs.map(async (reservoir) => {
      // Semantic similarity via vector search
      const { data } = await supabase.rpc('match_reservoir_items', {
        query_embedding: embedding.data[0].embedding,
        reservoir_id: reservoir.id,
        match_threshold: 0.7,
        match_count: 5
      })
      
      // Keyword matching
      const keywordScore = calculateKeywordMatch(
        item,
        reservoir.keywords
      )
      
      // Combined score (60% semantic, 40% keyword)
      const score = (data.avg_similarity * 0.6) + (keywordScore * 0.4)
      
      return {
        reservoirId: reservoir.id,
        score: score * 10, // Scale to 0-10
        reasoning: generateReasoning(data, keywordScore)
      }
    })
  )
  
  // 3. Return best match + all scores
  return {
    bestMatch: scores.sort((a, b) => b.score - a.score)[0],
    allMatches: scores
  }
}

Email-to-Reservoir

Inbound Email Flow

1. Email sent to reservoir address
   → react-tips@blackopscenter.com
   ↓
2. Resend receives email
   → Validates inbound domain
   ↓
3. Resend webhook fires
   → POST /api/webhooks/resend/inbound
   → Payload: email metadata
   ↓
4. Webhook handler:
   a. Verify signature (Svix)
   b. Rate limit check (10/min per reservoir)
   c. Validate payload size (<2MB)
   d. Find reservoir by email address
   ↓
5. Fetch full email content
   → GET from Resend API
   → Extract text/HTML
   ↓
6. Process email:
   a. Sanitize HTML (prevent XSS)
   b. Extract metadata (from, subject)
   c. Check for duplicates (prevent re-import)
   ↓
7. Create reservoir item
   → INSERT INTO reservoir_items
   → Link to reservoir
   → Track in reservoir_inbound_emails
   ↓
8. Return 200 OK (acknowledge receipt)
   ↓
9. User sees item in reservoir

Billing & Subscriptions

Subscription Model

Base Subscription: $297/month
├─ 1 site included
├─ Core features
└─ Monthly credits

Additional Sites: $100/site/month
├─ Billed as separate line items in Stripe
├─ Added/removed dynamically
└─ Prorated billing

Credit Packs (One-Time Purchase):
├─ Starter: $29 → 3,000 credits
├─ Standard: $49 → 6,000 credits
└─ Pro: $99 → 15,000 credits

Stripe Webhook Flow

subscription.created:
1. Parse subscription from Stripe
2. Calculate site count (base + add-ons)
3. Create subscription record in DB
4. Initialize monthly credits
5. Send welcome notification

subscription.updated:
1. Detect changes (site count, tier, period)
2. Update subscription record
3. If billing period renewed → reset credits
4. If tier changed → adjust credit limits

subscription.deleted:
1. Mark subscription as canceled
2. Send cancellation notification
3. Sites go read-only (data retained)

checkout.session.completed:
1. Identify purchase type (subscription vs. credit pack)
2. If credit pack → add credits to user
3. Record transaction
4. Send receipt

Performance & Scaling

Caching Strategy

  • Static Pages: Blog posts cached at CDN (Vercel)
  • API Responses: Cached with stale-while-revalidate
  • Database Queries: React Server Components cache
  • Images: Optimized and cached by Vercel Image Optimization

Database Optimization

-- Indexes for performance
CREATE INDEX idx_posts_site_slug ON posts(site_id, slug);
CREATE INDEX idx_reservoir_items_site ON reservoir_items(site_id);
CREATE INDEX idx_rss_items_published ON rss_feed_items(published_at DESC);
CREATE INDEX idx_rss_items_score ON rss_feed_items(ai_reservoir_score DESC);

-- Vector index for similarity search
CREATE INDEX ON reservoir_items 
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

Scaling Considerations

  • Serverless Functions: Auto-scale with traffic (Vercel)
  • Database Connection Pooling: Supabase handles pooling
  • Rate Limiting: Per-API-key and per-endpoint limits
  • Background Jobs: Offload heavy work (RSS, AI) to async queues
  • CDN: Static assets served from edge

Monitoring & Observability

Logging

// Structured logging
console.log('[API]', {
  method: 'POST',
  path: '/api/admin/reservoirs',
  userId: user.id,
  siteId: siteId,
  duration: 152,
  status: 200
})

// Error logging with context
console.error('[ERROR]', {
  error: error.message,
  stack: error.stack,
  context: {
    userId: user.id,
    siteId: siteId,
    endpoint: '/api/admin/posts'
  }
})

Metrics & Alerts

  • Vercel Analytics: Performance, Core Web Vitals
  • Sentry: Error tracking, performance monitoring
  • Supabase Dashboard: DB performance, query times
  • Stripe Dashboard: Payment success rate, MRR

Security Measures

Defense in Depth

  1. RLS Policies: Database-level tenant isolation
  2. API Authorization: Every route validates site access
  3. Input Validation: Zod schemas for all inputs
  4. Rate Limiting: Prevent abuse and DDoS
  5. CSRF Protection: Built into Next.js
  6. Content Sanitization: HTML sanitized before storage
  7. Webhook Signatures: Verify all webhooks (Stripe, Resend)

Data Encryption

  • In Transit: TLS 1.3 for all connections
  • At Rest: Database encrypted by Supabase
  • Secrets: Environment variables (not in code)
  • API Keys: Hashed before storage

Disaster Recovery

Backup Strategy

  • Database Backups: Daily automated backups (Supabase)
  • Point-in-Time Recovery: 7-day retention
  • Storage Backups: Images replicated to multiple regions

Incident Response

  1. Alert triggers (error rate spike, latency increase)
  2. On-call engineer notified
  3. Assess impact and severity
  4. Mitigate (rollback, scale up, disable feature)
  5. Post-incident review
  6. Update runbooks

Future Architecture

Planned Improvements

  • Edge Compute: Move more logic to edge for lower latency
  • Real-Time Collaboration: Websockets for live editing
  • Background Job Queue: Dedicated queue system (BullMQ)
  • Read Replicas: DB replicas for read-heavy queries
  • GraphQL API: Alternative to REST for flexible querying

Developer Resources

Good architecture is invisible. It just works—reliably, securely, and at scale.

Technical Architecture - BlackOps Center