# Design: Production Gap Closure

## Date: 2026-02-15
## Author: Claude Code (Opus 4.6)
## Status: Approved

## Problem

LLMHosts.com has 27K+ lines of working code across Phases 1-4, but 10 gaps prevent customer onboarding:

1. No authentication on proxy endpoints (anyone on network can use it)
2. No rate limiting (abuse vector if exposed)
3. CORS allows all origins (CSRF risk)
4. No database migrations (schema changes break existing users)
5. No error tracking (blind to production failures)
6. No deployment documentation (customers can't self-serve install)
7. No multi-tenant isolation (SaaS dashboard is single-user)
8. Stripe webhook lifecycle incomplete
9. No E2E tests
10. GitHub infrastructure blocked (external dependency)

## Requirements

- All gaps closed in one cohesive implementation
- Zero breaking changes to existing API contracts
- Default config preserves current behavior (local-only, no auth required)
- Production mode auto-hardens when tunnel is active
- Tests for every new module

## Design

### New Config Section: `[security]`

```toml
[security]
require_auth = false      # true when tunneled/exposed
api_keys = []             # proxy access keys (separate from BYOK cloud keys)
rate_limit_rpm = 60       # requests per minute per key
rate_limit_burst = 10     # burst allowance
cors_restrict = false     # true = use cors_origins from [server], false = allow all
```

```toml
[observability]
sentry_dsn = ""           # empty = disabled
log_format = "text"       # "text" | "json"
```

### Module: `llmhosts/proxy/auth.py` — API Key Authentication

Middleware that:
- Extracts `Authorization: Bearer <key>` from request
- Validates against proxy API key store (`~/.llmhosts/proxy_keys.json`)
- Sets `request.state.authenticated = True` and `request.state.api_key_id`
- Skips: `/health`, `/docs`, `/openapi.json`, `/redoc`
- Returns 401 with `WWW-Authenticate: Bearer` on failure
- When `require_auth = false`: passes all requests through (sets anonymous identity)

### Module: `llmhosts/proxy/ratelimit.py` — Token Bucket Rate Limiter

- In-memory token bucket per API key (or per-IP when auth disabled)
- Config-driven: `rate_limit_rpm` and `rate_limit_burst`
- Returns 429 with `Retry-After` and `X-RateLimit-*` headers
- Exempt paths: `/health`
- Resets buckets periodically (no memory leak)

### CORS Hardening (modify `llmhosts/proxy/app.py`)

- When `cors_restrict = true`: use `server.cors_origins` list
- When `cors_restrict = false`: use `["*"]` (current behavior)
- `create_app()` reads config from `app.state.llmhost_config`

### Module: `llmhosts/migrations/` — Alembic Database Migrations

- Alembic env configured for async SQLite
- Initial migration captures current audit schema
- CLI: `llmhosts db upgrade` / `llmhosts db downgrade`
- Auto-runs on `llmhosts serve` startup (like Django)

### Module: `llmhosts/observability.py` — Error Tracking & Structured Logging

- Optional Sentry SDK (guarded import like smart/full tiers)
- `init_observability(config)` called at server startup
- JSON log formatter when `log_format = "json"`
- Captures unhandled exceptions + slow requests (>5s)
- Added to `[project.optional-dependencies]` as `monitoring` tier

### Multi-Tenant Isolation (extend `llmhosts/rbac/`)

- Auth middleware resolves tenant context from API key
- `request.state.tenant_id` set on authenticated requests
- Audit queries filtered by tenant
- Dashboard API scoped to authenticated user's data
- Anonymous mode (no auth) = single-tenant (current behavior)

### Stripe Webhook Completion (`frontend/`)

- `POST /api/webhooks/stripe` handles: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`
- Idempotent: tracks processed event IDs
- Updates user subscription status in Prisma DB

### Deployment Documentation

- `docs/deployment/bare-metal.md` — pip install + systemd
- `docs/deployment/docker-production.md` — compose with reverse proxy
- `docs/deployment/vps-guide.md` — Hetzner/OVH specific
- `docker-compose.prod.yml` — production-ready compose file

### E2E Test Suite (`tests/test_e2e.py`)

- Uses ASGI transport (consistent with existing test infra)
- Flows: unauthenticated access denied → create key → authenticate → send request → verify response headers → check audit log
- Rate limit test: burst requests, verify 429
- CORS test: verify restricted origins

## Middleware Stack Order (post-implementation)

```
Request →
  1. RequestIdMiddleware (inject X-Request-ID)
  2. AuthMiddleware (validate Bearer token)
  3. RateLimitMiddleware (enforce rate limits)
  4. RequestLoggingMiddleware (log method/path/status/latency)
  5. CORSMiddleware (enforce origin policy)
→ Route handler
```

## File Inventory

### New files
- `llmhosts/proxy/auth.py`
- `llmhosts/proxy/ratelimit.py`
- `llmhosts/observability.py`
- `llmhosts/migrations/__init__.py`
- `llmhosts/migrations/env.py`
- `llmhosts/migrations/versions/001_initial_audit_schema.py`
- `tests/test_auth.py`
- `tests/test_ratelimit.py`
- `tests/test_e2e.py`
- `docs/deployment/bare-metal.md`
- `docs/deployment/docker-production.md`
- `docs/deployment/vps-guide.md`
- `docker-compose.prod.yml`

### Modified files
- `llmhosts/config.py` — add SecurityConfig, ObservabilityConfig
- `llmhosts/proxy/app.py` — wire auth, ratelimit, CORS from config
- `llmhosts/server.py` — call init_observability(), auto-migrate
- `llmhosts/cli.py` — add `llmhosts db` subcommand
- `pyproject.toml` — add monitoring optional deps
- `frontend/src/app/api/webhooks/stripe/route.ts` — complete handlers

## Trade-offs

- **In-memory rate limiting**: Simpler than Redis, but doesn't persist across restarts. Acceptable for single-process `llmhosts serve`. Would need Redis for multi-worker.
- **Alembic over raw SQL**: Adds dependency but provides safe schema evolution.
- **Sentry as optional**: Won't break core tier. Users who want it install `llmhost[monitoring]`.
