# dd0c/route — Dual-Mode Deployment Architecture Addendum **Date:** March 1, 2026 **Scope:** Self-hosted deployment mode alongside existing cloud-managed architecture --- ## 1. Deployment Modes dd0c/route supports two deployment modes. The core business logic (proxy engine, router brain, analytics pipeline) is identical in both. Only the infrastructure and auth layers differ. | Aspect | Cloud-Managed | Self-Hosted | |--------|--------------|-------------| | Deployment | ECS Fargate + CDK | Docker Compose | | Auth | GitHub OAuth + Cognito JWT | Local auth (bcrypt + JWT) | | Database | RDS PostgreSQL + ElastiCache Redis | PostgreSQL + Redis containers | | Telemetry DB | TimescaleDB on RDS | TimescaleDB container | | CDN | CloudFront + S3 | Caddy reverse proxy (auto-TLS) | | Billing | Stripe Checkout | License key or honor system | | Analytics | PostHog Cloud | PostHog self-hosted (optional) | | Updates | Automatic (ECS rolling) | `docker compose pull && docker compose up -d` | | Monitoring | CloudWatch + PagerDuty | Grafana + Prometheus (bundled) | ## 2. Docker Compose (Self-Hosted) ```yaml # docker-compose.yml — dd0c/route self-hosted version: "3.8" services: proxy: image: ghcr.io/dd0c/route-proxy:latest ports: - "8080:8080" # Proxy endpoint environment: - DATABASE_URL=postgres://dd0c:${DB_PASSWORD}@postgres:5432/dd0c - REDIS_URL=redis://redis:6379 - TIMESCALE_URL=postgres://dd0c:${DB_PASSWORD}@timescaledb:5432/dd0c_telemetry - AUTH_MODE=local # local | oauth - GOVERNANCE_MODE=audit # strict | audit depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped api: image: ghcr.io/dd0c/route-api:latest ports: - "3000:3000" # Dashboard API environment: - DATABASE_URL=postgres://dd0c:${DB_PASSWORD}@postgres:5432/dd0c - REDIS_URL=redis://redis:6379 - TIMESCALE_URL=postgres://dd0c:${DB_PASSWORD}@timescaledb:5432/dd0c_telemetry - AUTH_MODE=local - JWT_SECRET=${JWT_SECRET} depends_on: postgres: condition: service_healthy worker: image: ghcr.io/dd0c/route-worker:latest environment: - DATABASE_URL=postgres://dd0c:${DB_PASSWORD}@postgres:5432/dd0c - TIMESCALE_URL=postgres://dd0c:${DB_PASSWORD}@timescaledb:5432/dd0c_telemetry - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-} - SMTP_URL=${SMTP_URL:-} depends_on: postgres: condition: service_healthy dashboard: image: ghcr.io/dd0c/route-dashboard:latest ports: - "3001:80" # Static SPA restart: unless-stopped postgres: image: postgres:16-alpine volumes: - pg_data:/var/lib/postgresql/data - ./migrations:/docker-entrypoint-initdb.d environment: - POSTGRES_USER=dd0c - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=dd0c healthcheck: test: ["CMD-SHELL", "pg_isready -U dd0c"] interval: 5s timeout: 3s retries: 5 timescaledb: image: timescale/timescaledb:latest-pg16 volumes: - ts_data:/var/lib/postgresql/data - ./migrations/timescale:/docker-entrypoint-initdb.d environment: - POSTGRES_USER=dd0c - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=dd0c_telemetry healthcheck: test: ["CMD-SHELL", "pg_isready -U dd0c"] interval: 5s timeout: 3s retries: 5 redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 caddy: image: caddy:2-alpine ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data depends_on: - proxy - api - dashboard volumes: pg_data: ts_data: redis_data: caddy_data: ``` ## 3. Install Script ```bash #!/usr/bin/env bash # install.sh — dd0c/route self-hosted installer set -euo pipefail echo "🚀 dd0c/route — Self-Hosted Installer" echo "======================================" # Check prerequisites command -v docker >/dev/null 2>&1 || { echo "❌ Docker required. Install: https://docs.docker.com/get-docker/"; exit 1; } command -v docker compose >/dev/null 2>&1 || { echo "❌ Docker Compose V2 required."; exit 1; } # Create directory DD0C_DIR="${DD0C_DIR:-$HOME/.dd0c}" mkdir -p "$DD0C_DIR" cd "$DD0C_DIR" # Generate secrets DB_PASSWORD=$(openssl rand -hex 16) JWT_SECRET=$(openssl rand -hex 32) # Write .env cat > .env << ENVEOF DB_PASSWORD=$DB_PASSWORD JWT_SECRET=$JWT_SECRET # Optional: Slack webhook for alerts # SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... # Optional: SMTP for email digests # SMTP_URL=smtp://user:pass@smtp.example.com:587 ENVEOF # Download compose file curl -sSL https://raw.githubusercontent.com/dd0c/route/main/docker-compose.yml -o docker-compose.yml curl -sSL https://raw.githubusercontent.com/dd0c/route/main/Caddyfile -o Caddyfile # Pull and start docker compose pull docker compose up -d echo "" echo "✅ dd0c/route is running!" echo "" echo " Proxy: http://localhost:8080/v1/chat/completions" echo " Dashboard: http://localhost:3001" echo " API: http://localhost:3000" echo "" echo " Create your first API key:" echo " curl -X POST http://localhost:3000/api/auth/local/signup \\" echo " -H 'Content-Type: application/json' \\" echo " -d '{\"email\":\"admin@localhost\",\"password\":\"changeme\"}'" echo "" echo " Data stored in: $DD0C_DIR" echo " Upgrade: cd $DD0C_DIR && docker compose pull && docker compose up -d" ``` ## 4. Auth Abstraction Layer The API uses an `AuthProvider` trait/interface that switches based on `AUTH_MODE`: ```rust // src/auth/mod.rs pub enum AuthMode { Local, // bcrypt passwords + local JWT OAuth, // GitHub OAuth + Cognito JWT } pub trait AuthProvider: Send + Sync { async fn authenticate(&self, req: &Request) -> Result; async fn create_user(&self, email: &str, password: Option<&str>) -> Result; } pub struct LocalAuthProvider { /* PostgreSQL-backed */ } pub struct OAuthProvider { /* GitHub + Cognito */ } impl AuthProvider for LocalAuthProvider { async fn authenticate(&self, req: &Request) -> Result { // Extract JWT from Authorization header // Verify with local JWT_SECRET (HS256) // Return AuthContext with org_id, role } } // Factory pub fn create_auth_provider(mode: AuthMode, config: &Config) -> Box { match mode { AuthMode::Local => Box::new(LocalAuthProvider::new(config)), AuthMode::OAuth => Box::new(OAuthProvider::new(config)), } } ``` ## 5. Data Layer Abstraction All 6 dd0c products must abstract the data layer so self-hosted mode uses PostgreSQL everywhere (no DynamoDB, no Cognito, no SQS): | Cloud Service | Self-Hosted Replacement | |--------------|----------------------| | DynamoDB | PostgreSQL (same schema, different driver) | | SQS FIFO | PostgreSQL LISTEN/NOTIFY + pgmq | | Cognito | Local JWT (HS256) | | EventBridge | Cron + webhook polling | | S3 | Local filesystem or MinIO | | CloudFront | Caddy reverse proxy | | SES | SMTP relay | | KMS | Local AES-256-GCM with key file | ```rust // src/data/mod.rs pub trait EventQueue: Send + Sync { async fn publish(&self, event: Event) -> Result<()>; async fn consume(&self, batch_size: usize) -> Result>; } pub struct SqsFifoQueue { /* AWS SQS */ } pub struct PgmqQueue { /* PostgreSQL pgmq */ } pub trait ObjectStore: Send + Sync { async fn put(&self, key: &str, data: &[u8]) -> Result<()>; async fn get(&self, key: &str) -> Result>; } pub struct S3Store { /* AWS S3 */ } pub struct LocalFsStore { /* Local filesystem */ } ``` ## 6. Upgrade Path Self-hosted users need a safe upgrade mechanism: ```bash # Upgrade script (runs as part of `docker compose pull`) # 1. Pull new images docker compose pull # 2. Run migrations (idempotent) docker compose run --rm api migrate # 3. Rolling restart docker compose up -d --remove-orphans # 4. Health check curl -sf http://localhost:8080/health || echo "⚠️ Proxy unhealthy after upgrade" curl -sf http://localhost:3000/health || echo "⚠️ API unhealthy after upgrade" ``` ## 7. Self-Hosted BDD Acceptance Specs ```gherkin Feature: Self-Hosted Installation Scenario: Fresh install via install script Given a Linux host with Docker and Docker Compose installed When the user runs curl -sSL install.dd0c.dev | bash Then docker-compose.yml is downloaded to ~/.dd0c And .env is generated with random DB_PASSWORD and JWT_SECRET And all containers start and pass health checks within 60 seconds And the proxy responds to GET /health with 200 Scenario: Local auth signup and API key generation Given dd0c/route is running in self-hosted mode (AUTH_MODE=local) When the user POSTs /api/auth/local/signup with email and password Then a user account is created with bcrypt-hashed password And a JWT is returned (HS256, signed with JWT_SECRET) And the user can create an API key via /api/orgs/{id}/api-keys Scenario: Upgrade preserves data Given dd0c/route is running with existing routing rules and telemetry When the user runs docker compose pull && docker compose up -d Then all routing rules are preserved And all telemetry data is preserved And the proxy resumes routing within 10 seconds Scenario: Self-hosted works without internet after initial pull Given all Docker images are cached locally When the host loses internet connectivity Then the proxy continues routing requests And the dashboard continues serving And cost tables use the last cached version Scenario: Caddy auto-TLS with custom domain Given the Caddyfile is configured with domain "route.example.com" And DNS points to the host When Caddy starts Then a Let's Encrypt TLS certificate is automatically provisioned And HTTPS is served on port 443 Scenario: PostgreSQL data persists across restarts Given routing rules and telemetry exist in PostgreSQL When docker compose down && docker compose up -d is run Then all data is preserved via named volumes ``` ## 8. Impact on Existing Epics | Epic | Change Required | Effort | |------|----------------|--------| | 1 (Proxy) | None — pure Rust, no AWS deps | 0 | | 2 (Router) | None — in-memory, no AWS deps | 0 | | 3 (Analytics) | Add pgmq as alternative to SQS | 2 pts | | 4 (Dashboard API) | Add LocalAuthProvider, abstract KMS | 3 pts | | 5 (Dashboard UI) | Add local login form (email/password) | 2 pts | | 6 (Shadow CLI) | None — already runs locally | 0 | | 7 (Slack/Email) | SMTP fallback for SES | 1 pt | | 8 (Infrastructure) | New: docker-compose.yml + install.sh + Caddyfile | 5 pts | | 9 (Onboarding) | New: local signup flow, remove Stripe requirement | 3 pts | | 10 (TF Tenets) | None — tenets are code-level, not infra-level | 0 | | **Total** | | **16 pts** | --- *This addendum applies to dd0c/route. The same pattern (AuthProvider trait, data layer abstraction, docker-compose, install script) replicates across all 6 dd0c products with product-specific service containers.*