This document explains the deployment architecture for OSA (Open Science Assistant) with Cloudflare integration for security and scalability.
- Overview
- Production URLs
- BYOK (Bring Your Own Key)
- Architecture Options
- Security Layers
- Setup Instructions
- Apache Reverse Proxy
- Configuration
- CLI Usage
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
| 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
OSA supports BYOK, allowing users to provide their own LLM API keys instead of relying on server-configured keys.
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 |
- With BYOK: Users providing any BYOK header bypass server API key requirement
- Without BYOK: Users must provide server API key via
X-API-Keyheader
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.
# 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┌─────────────────────────────┐
│ Frontend │ ← Static Site / Local Dev
│ (localhost:3000) │
└──────────────┬──────────────┘
│ HTTP
▼
┌─────────────────────────────┐
│ OSA Backend │ ← FastAPI
│ localhost:38528 │
│ (CORS validation) │
└─────────────────────────────┘
┌─────────────────────────────┐
│ 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) │
└─────────────────────────────┘
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;
}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")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=["*"],
)# 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# 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-backendcd 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 deployBackend (.env):
# Server
PORT=38528
HOST=0.0.0.0
# Security
API_KEY=your-backend-api-token
# LLM Provider
OPENROUTER_API_KEY=your-openrouter-keyWorker (wrangler.toml secrets):
TURNSTILE_SECRET_KEY=your-turnstile-secret
BACKEND_API_TOKEN=your-backend-api-tokenThe port is configurable via environment variable:
# Default: 38528
PORT=38528 docker run ...Or via CLI:
osa serve --port 38528| 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 |
curl https://api.osa.pages.dev/healthwrangler taildocker logs -f osa- Workers: 100,000 requests/day
- Pages: Unlimited static sites
- Turnstile: Unlimited verifications
- Tunnel: Free
- Varies by model (see .context/research.md)
- Cerebras models: ~$0.0001/request
Estimated monthly cost for 10,000 requests: ~$1-5
For servers using Apache as a reverse proxy (alternative to Cloudflare Tunnel):
# /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>sudo a2enmod proxy proxy_http ssl
sudo systemctl reload apache2# 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# 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 healthThe CLI defaults to connecting to the production API at https://api.osc.earth/osa. Use --api-url to override.
Last Updated: February 2026