Skip to content

Latest commit

 

History

History
458 lines (344 loc) · 11 KB

File metadata and controls

458 lines (344 loc) · 11 KB

OSA Deployment Architecture

This document explains the deployment architecture for OSA (Open Science Assistant) with Cloudflare integration for security and scalability.

Table of Contents


Overview

Backend: FastAPI + Docker on port 38528 Frontend: Cloudflare Pages (planned) API Proxy: Cloudflare Worker with Turnstile protection

Port Allocation:

  • HEDit prod: 38427
  • HEDit dev: 38428
  • OSA prod: 38528
  • OSA dev: 38529

Production URLs

Environment API URL Docker Image Tag Port
Production https://api.osc.earth/osa ghcr.io/openscience-collective/osa:latest 38528
Development https://api.osc.earth/osa-dev ghcr.io/openscience-collective/osa:dev 38529

Frontend:

  • Production: https://demo.osc.earth
  • Development: https://develop-demo.osc.earth

BYOK (Bring Your Own Key)

OSA supports BYOK, allowing users to provide their own LLM API keys instead of relying on server-configured keys.

How It Works

Users can pass their own API keys via HTTP headers:

Header Provider
X-OpenAI-API-Key OpenAI
X-Anthropic-API-Key Anthropic
X-OpenRouter-Key OpenRouter

Authentication Policy

  • With BYOK: Users providing any BYOK header bypass server API key requirement
  • Without BYOK: Users must provide server API key via X-API-Key header

Example Request with BYOK

curl -X POST https://api.osc.earth/osa-dev/hed/chat \
  -H "Content-Type: application/json" \
  -H "X-OpenRouter-Key: sk-or-your-key" \
  -d '{"message": "What is HED?", "stream": false}'

No X-API-Key required when using BYOK headers.

CLI Configuration for BYOK

# Set up your API key
osa init --api-key "sk-or-your-key"

# Or set it directly
osa config set --openrouter-key "sk-or-your-key"

# Ask a question (uses saved key via BYOK)
osa ask -a hed "What is HED?"

# Use against dev server
osa ask -a hed "What is HED?" --api-url https://api.osc.earth/osa-dev

Architecture Options

Option 1: Direct Connection (Development)

┌─────────────────────────────┐
│  Frontend                   │  ← Static Site / Local Dev
│  (localhost:3000)           │
└──────────────┬──────────────┘
               │ HTTP
               ▼
┌─────────────────────────────┐
│  OSA Backend                │  ← FastAPI
│  localhost:38528            │
│  (CORS validation)          │
└─────────────────────────────┘

Option 2: Cloudflare Worker Proxy (Production)

┌─────────────────────────────┐
│  Frontend                   │  ← Cloudflare Pages
│  (osa.pages.dev)            │
│  + Turnstile Challenge      │
└──────────────┬──────────────┘
               │ HTTPS + Turnstile Token
               ▼
┌─────────────────────────────┐
│  Cloudflare Worker          │  ← API Proxy
│  (api.osa.pages.dev)        │
│  - Validates Turnstile      │
│  - Rate limiting            │
│  - Adds API token           │
└──────────────┬──────────────┘
               │ HTTPS + API Token
               ▼
┌─────────────────────────────┐
│  Cloudflare Tunnel          │  ← Secure Tunnel
│  (cloudflared)              │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│  OSA Backend                │  ← Docker Container
│  127.0.0.1:38528            │
│  (Validates API Token)      │
└─────────────────────────────┘

Security Layers

Layer 1: Turnstile (Frontend → Worker)

Purpose: Bot protection at the edge Location: Cloudflare Worker validates Turnstile token

Turnstile is Cloudflare's CAPTCHA alternative:

  • Invisible challenge (no user interaction needed)
  • Blocks automated attacks
  • Free for unlimited verifications

Frontend Integration:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>

Worker Validation:

async function validateTurnstile(token, remoteIP, env) {
  const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      secret: env.TURNSTILE_SECRET_KEY,
      response: token,
      remoteip: remoteIP,
    }),
  });
  const result = await response.json();
  return result.success;
}

Layer 2: API Token (Worker → Backend)

Purpose: Authenticate Worker requests to backend Location: Worker adds token, backend validates

Worker adds token:

const backendRequest = new Request(backendUrl, {
  method: request.method,
  headers: {
    ...Object.fromEntries(request.headers),
    'X-API-Token': env.BACKEND_API_TOKEN,
  },
  body: request.body,
});

Backend validates:

# In FastAPI middleware
def validate_api_token(request: Request):
    token = request.headers.get("X-API-Token")
    expected = settings.api_key
    if expected and token != expected:
        raise HTTPException(status_code=401, detail="Invalid API token")

Layer 3: CORS (Backend)

Purpose: Ensure requests only from allowed origins Location: FastAPI CORS middleware

# In src/api/main.py
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://osa.pages.dev", "https://api.osa.pages.dev"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Setup Instructions

Docker Deployment

# Pull from GHCR
docker pull ghcr.io/openscience-collective/osa:latest

# Run container
docker run -d \
  --name osa \
  -p 38528:38528 \
  -e API_KEY=your-api-token \
  -e OPENROUTER_API_KEY=your-openrouter-key \
  ghcr.io/openscience-collective/osa:latest

# Verify health
curl http://localhost:38528/health

Cloudflare Tunnel Setup

# Install cloudflared
# macOS: brew install cloudflare/cloudflare/cloudflared
# Linux: wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64

# Login to Cloudflare
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create osa-backend

# Configure tunnel (config.yml)
cat > ~/.cloudflared/config.yml << EOF
tunnel: YOUR_TUNNEL_ID
credentials-file: /path/to/credentials.json

ingress:
  - hostname: api.osa.pages.dev
    service: http://localhost:38528
  - service: http_status:404
EOF

# Run tunnel
cloudflared tunnel run osa-backend

Cloudflare Worker Deployment

cd workers

# Install wrangler
npm install -g wrangler
wrangler login

# Set secrets
wrangler secret put TURNSTILE_SECRET_KEY
wrangler secret put BACKEND_API_TOKEN

# Deploy
wrangler deploy

Configuration

Environment Variables

Backend (.env):

# Server
PORT=38528
HOST=0.0.0.0

# Security
API_KEY=your-backend-api-token

# LLM Provider
OPENROUTER_API_KEY=your-openrouter-key

Worker (wrangler.toml secrets):

TURNSTILE_SECRET_KEY=your-turnstile-secret
BACKEND_API_TOKEN=your-backend-api-token

Port Configuration

The port is configurable via environment variable:

# Default: 38528
PORT=38528 docker run ...

Or via CLI:

osa serve --port 38528

Security Comparison

Layer Protects Against Location
Turnstile Bots, automated abuse Edge (Worker)
API Token Unauthorized backend access Worker → Backend
CORS Cross-origin attacks Backend
Rate Limiting DoS, abuse Worker (KV)
HTTPS Man-in-the-middle Cloudflare

Monitoring

Health Check

curl https://api.osa.pages.dev/health

Worker Logs

wrangler tail

Backend Logs

docker logs -f osa

Cost Estimation

Cloudflare (Free Tier)

  • Workers: 100,000 requests/day
  • Pages: Unlimited static sites
  • Turnstile: Unlimited verifications
  • Tunnel: Free

OpenRouter API

  • Varies by model (see .context/research.md)
  • Cerebras models: ~$0.0001/request

Estimated monthly cost for 10,000 requests: ~$1-5


Apache Reverse Proxy

For servers using Apache as a reverse proxy (alternative to Cloudflare Tunnel):

Configuration

# /etc/apache2/sites-available/apache-api.osc.earth.conf

<VirtualHost *:443>
    ServerName api.osc.earth

    # SSL configuration (managed by certbot or similar)
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem

    # PRODUCTION: OSA API Backend (port 38528)
    ProxyPass /osa/ http://localhost:38528/
    ProxyPassReverse /osa/ http://localhost:38528/

    # DEVELOPMENT: OSA Dev API Backend (port 38529)
    ProxyPass /osa-dev/ http://localhost:38529/
    ProxyPassReverse /osa-dev/ http://localhost:38529/
</VirtualHost>

Enable Required Modules

sudo a2enmod proxy proxy_http ssl
sudo systemctl reload apache2

CLI Usage

Installation

# From PyPI (lightweight, ~7 dependencies)
pip install open-science-assistant

# From source (with server dependencies)
git clone https://github.com/OpenScience-Collective/osa.git
cd osa
uv sync

Commands

# Setup (saves API key securely)
osa init

# Ask a question
osa ask -a hed "What is HED?"

# Interactive chat session
osa chat -a hed

# Override API URL per-command
osa ask -a hed "What is HED?" --api-url https://api.osc.earth/osa-dev

# Configuration
osa config show                           # Show current config
osa config set --openrouter-key "sk-..."  # Set LLM API key
osa config path                           # Show config file location

# Server management (requires pip install 'open-science-assistant[server]')
osa serve                                 # Start API server
osa serve --port 38529 --reload           # Development mode
osa health --url https://api.osc.earth/osa  # Check API health

The CLI defaults to connecting to the production API at https://api.osc.earth/osa. Use --api-url to override.


Last Updated: February 2026