Spaces:
Running
Running
File size: 7,665 Bytes
b17baae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | # MicProtocol – Deployment & Scaling Strategy
## Deployment Architecture
```mermaid
graph LR
subgraph "Vercel Edge"
WEB[Next.js Web App]
PORTAL[Guest Portal]
end
subgraph "AWS ECS/Fargate"
API[NestJS API]
WORKER[Background Workers]
CLIP_SCORER[CLIP Scorer<br/>Python + ML]
end
subgraph "Managed Services"
DB[RDS PostgreSQL]
CACHE[ElastiCache Redis]
SEARCH[Meilisearch Cloud]
OBJ[Cloudflare R2]
end
subgraph "CI/CD"
GHA[GitHub Actions]
TURBO[Turborepo Cache]
end
GHA -->|Docker Build| API
GHA -->|Deploy| WEB
WEB -->|tRPC| API
API --> DB
WORKER --> DB
WORKER --> CACHE
WORKER -->|subprocess| CLIP_SCORER
CLIP_SCORER --> DB
```
## Production Stacks (Coolify / Render)
Beyond the ECS layout above, the repo ships two ready-to-deploy production
blueprints that include the Erlang/OTP real-time engine (`mic-realtime`).
### Coolify (`docker-compose.coolify.yml`)
Full self-hosted stack: Postgres, Redis, API, worker, **mic-realtime**, web,
admin, and the self-hosted integration fleet. Caddy terminates TLS (wildcard
cert via DNS-01) and routes:
| Host | Upstream |
|---|---|
| `<root>` / `*.` (workspace subdomains) | web :3000 |
| `api.<root>` | api :4000 |
| `admin.<root>` | admin :3000 |
| `realtime.<root>` | mic-realtime :4001 (WebSocket) |
### Render (`render.yaml`)
Backend-only blueprint (web + admin deploy on Vercel):
- `micprotocol-api` — NestJS API
- `micprotocol-redis` — Redis add-on
- `micprotocol-realtime` — Erlang/OTP engine (`env: docker`,
`rootDir: services/mic-realtime`, health at `/healthz`)
### Real-time engine & required secrets
The `mic-realtime` service is a first-class citizen of both stacks and ships
**fail-closed**: both it and the API (`RealtimeClient` in
`apps/api/src/chat/realtime.client.ts`) refuse to boot in production when the
secrets are missing or equal to the known defaults.
| Env var | Required | Notes |
|---|---|---|
| `MIC_REALTIME_INTERNAL_TOKEN` | ✅ | Bearer used by the API to publish events via `/internal/dispatch` |
| `MIC_REALTIME_SHARED_SECRET` | ✅ | HMAC secret for WebSocket auth tokens |
| `MIC_REALTIME_PUBLIC_URL` | ✅ | Browser-facing `wss://realtime.<root>` URL; forwarded to web/admin as `NEXT_PUBLIC_MIC_REALTIME_URL` |
| `MIC_REALTIME_URL` | API only | API→realtime base URL (Coolify default `http://mic-realtime:4001`) |
Generate both secrets with `openssl rand -hex 32` and set
`MIC_REALTIME_PUBLIC_URL` to a `wss://` URL **before first boot** — the
services will not start with missing or default values.
## Environment Strategy
| Environment | Web Frontend | API Backend | Database |
|-------------|-------------|-------------|----------|
| **Development** | `localhost:3000` | `localhost:4000` | Local PostgreSQL |
| **Preview** | Vercel Preview | ECS Staging | RDS Staging |
| **Production** | Vercel Edge | ECS Production | RDS Multi-AZ |
## Scaling Triggers
### < 1,000 Podcasts (Startup Phase)
- Single ECS task for API + Workers
- Shared Redis instance (ElastiCache `t3.micro`)
- Cloudflare R2 for all media storage
- Single PostgreSQL instance (RDS `db.t3.medium`)
### > 1,000 Podcasts (Growth Phase)
- Split Workers into separate ECS service
- Dedicated Redis for BullMQ vs. caching
- Read replicas for PostgreSQL
- CDN for static assets
- Migrate complex workflows to **Temporal.io**
### > 10,000 Podcasts (Scale Phase)
- Extract Outreach Domain into standalone microservice
- Outreach gets own MongoDB cluster (high write volume on email events)
- Event bus (AWS SNS/SQS) for cross-service communication
- Meilisearch cluster for search scaling
- Multi-region deployment for global guest portal latency
## CI/CD Pipeline
```mermaid
flowchart TD
PUSH[Git Push] --> LINT[Lint & Type Check]
LINT --> TEST[Unit Tests]
TEST --> BUILD[Turbo Build]
BUILD --> |Main Branch| DEPLOY_STAGING[Deploy to Staging]
BUILD --> |Release Tag| DEPLOY_PROD[Deploy to Production]
BUILD --> |PR| PREVIEW[Vercel Preview]
DEPLOY_STAGING --> INTEGRATION[Integration Tests]
INTEGRATION --> |Pass| PROMOTE[Promote to Production]
DEPLOY_PROD --> MIGRATE[Run DB Migrations]
MIGRATE --> HEALTH[Health Checks]
HEALTH --> |Healthy| COMPLETE([Deployment Complete])
HEALTH --> |Unhealthy| ROLLBACK[Automatic Rollback]
```
## Infrastructure Configuration
### Docker (API)
```dockerfile
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/api/package.json apps/api/
COPY packages/shared-types/package.json packages/shared-types/
RUN pnpm install --frozen-lockfile
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm --filter @micprotocol/api prisma generate
RUN pnpm --filter @micprotocol/api build
FROM base AS runtime
WORKDIR /app
COPY --from=build /app/apps/api/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/api/prisma ./prisma
RUN apk add --no-cache ffmpeg
EXPOSE 4000
CMD ["node", "dist/main.js"]
```
### Docker (CLIP Scorer)
Canonical source of truth is `services/clip-scorer/Dockerfile`. It builds a one-shot CLI image:
```dockerfile
FROM python:3.11-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
RUN uv venv /opt/venv \
&& VIRTUAL_ENV=/opt/venv uv pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.0.0" \
&& VIRTUAL_ENV=/opt/venv uv pip install "open-clip-torch>=2.20.0" "transformers>=4.30.0" "Pillow>=10.0.0" "soundfile>=0.12.0" "pyloudnorm>=0.1.0" "numpy>=1.24.0"
ENV PATH="/opt/venv/bin:$PATH"
RUN python3 -c "import open_clip; open_clip.create_model_and_transforms('ViT-B-32', pretrained='laion2b_s34b_b79k')"
COPY scoring/ scoring/
COPY pipeline.py .
ENTRYPOINT ["python3", "pipeline.py"]
```
In production the same Python stack is baked into the **worker** image (`apps/api/Dockerfile`), where the BullMQ clip-scoring processor executes the pipeline via `execFile`.
### Docker Compose Configuration
The worker service requires `ipc: host` for `/dev/shm` access (needed by ffmpeg shared memory). CLIP scoring runs inside the worker as a subprocess of the processor — there is no separate clip-scorer container or shared volume:
```yaml
worker:
container_name: micprotocol-worker
command: node /app/apps/api/dist/main.js
ipc: host
deploy:
resources:
limits:
cpus: "2"
memory: 2G
```
### Scaling Considerations for CLIP Scorer
| Metric | Value | Notes |
|--------|-------|-------|
| CPU per clip | ~0.5 cores | ~30–120s for CLIP inference (CPU) |
| Memory per clip | ~1.5GB | CLIP model + frame buffers |
| Concurrent clips | 2 per worker (set in processor) | Bump cautiously — Python ML is memory-heavy |
| Horizontal scaling | Add worker containers | Each container handles 2 concurrent clips |
| GPU acceleration | Optional | CLIP runs on CPU by default; GPU cuts time by 5-10x |
## Security
- **GDPR/CCPA**: Right-to-erasure cascading through all related records
- **Email Compliance**: CAN-SPAM footer injection, automatic unsubscribe
- **Data Isolation**: Row-level scoping through workspace → podcast hierarchy
- **Upload Security**: Presigned URLs (15-min expiry), MIME verification, virus scanning
- **Auth**: Clerk JWT verification with workspace-scoped claims
|