Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)

This commit is contained in:
2026-08-11 12:27:44 +02:00
commit d5af006443
304 changed files with 74604 additions and 0 deletions
@@ -0,0 +1,99 @@
# Deployment and Operations
---
## Production Stack
The production setup is a single-host Docker Compose deployment using **Traefik** as a reverse proxy. There is no Kubernetes, cloud-managed infrastructure, or horizontal scaling configuration.
**Services** (`infrastructure/docker-compose.yml`):
| Service | Image | Role |
|---|---|---|
| `migrate` | backend Dockerfile | Runs `prisma migrate deploy` once before API starts |
| `api` | backend Dockerfile | NestJS HTTP API on port 3001 |
| `worker` | backend Dockerfile | BullMQ queue processor (`node dist/worker.js`) |
| `frontend` | frontend Dockerfile | Next.js on port 3000 |
| `postgres` | postgres:16-alpine | Database — bound to `127.0.0.1:5432` (not public) |
| `redis` | redis:7-alpine | BullMQ queues — `noeviction` policy, AOF persistence |
**SSL / routing**: Traefik handles TLS termination with automatic Let's Encrypt certificates. Both API (`/api` prefix) and frontend run on the same domain — Traefik routes by path prefix. The `traefik-network` external network must exist before deploy.
**Build**: Multi-stage Dockerfile (`node:22-alpine`). Builder compiles TypeScript; runner installs prod-only deps. The Prisma CLI is copied from builder stage so the `migrate` service can run schema migrations.
---
## Secrets Management
All secrets are passed as environment variables from `infrastructure/.env` (based on `.env.example`). There is no secrets vault, no encrypted secret store, and no runtime secret injection. The `.env` file must be present on the host before `docker compose up`.
Secrets that must be set:
| Variable | Notes |
|---|---|
| `TOKEN_ENCRYPTION_KEY` | Exactly 32 characters. AES-256 key for YouTube OAuth tokens. See rotation note below. |
| `JWT_SECRET` / `JWT_REFRESH_SECRET` | Generate with `openssl rand -base64 48` |
| `POSTGRES_PASSWORD` / `REDIS_PASSWORD` | Strong random passwords |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | From Google Cloud Console |
---
## Health Check
`GET /api/v1/health` returns `{ "status": "ok" }`. This is a **shallow** check — it only confirms the Node process is responding; it does not verify database connectivity or Redis availability. Docker healthcheck polls this endpoint every 15 s; the `frontend` service waits for the API to be healthy before starting.
---
## Logging
No structured logging is configured. The application uses NestJS's default logger (`console.log`/`console.error`). In production, logs go to stdout and are captured by Docker's logging driver (default: `json-file`). There is no log aggregation, no Sentry integration, and no OpenTelemetry instrumentation.
---
## Job Failure Handling
BullMQ jobs do not have automatic retry configured except where noted:
| Queue | Retry | Failed job retention |
|---|---|---|
| `youtube-sync` | None | Last 5 failed jobs kept (`removeOnFail: { count: 5 }`); completed jobs removed |
| `render` | None | BullMQ default (kept until manually cleared) |
| `lint` | None | BullMQ default |
| `bulk-metadata` | None | BullMQ default |
| `import` | None | BullMQ default |
| `conflict-detection` | None | Last 10 failed jobs kept (`removeOnFail: 10`); last 10 completed kept (`removeOnComplete: 10`). Registered as a BullMQ repeatable by `ConflictDetectionScheduler` when `CONFLICT_DETECTION_ENABLED=true`. |
There is no dead-letter queue and no mechanism to notify users when a background job fails. A failed sync job means the user's video was not pushed to YouTube — the UI will continue to show "push pending" with no error indication. Failed jobs can be inspected directly in Redis or via a BullMQ dashboard (none is currently deployed).
---
## Rate Limiting
There is no rate limiting on any API endpoint. `@nestjs/throttler` is not installed. All routes are unthrottled.
---
## Database Backups
No backup strategy is configured. PostgreSQL data lives in the `postgres_data` Docker volume on the host. There is no automated backup job, no pg_dump schedule, and no offsite backup.
To back up manually:
```bash
docker exec <postgres_container> pg_dump -U $POSTGRES_USER $POSTGRES_DB > backup.sql
```
---
## TOKEN_ENCRYPTION_KEY Rotation
YouTube OAuth tokens are AES-256 encrypted in the database using `TOKEN_ENCRYPTION_KEY`. **Changing this key breaks all channel connections** — all stored tokens become unreadable and every connected channel must be fully re-authenticated by its owner.
There is no tooling for key rotation. If rotation is required (e.g. key compromise), the procedure is:
1. Take the application offline.
2. Write a one-off migration script that decrypts every `Channel.accessToken` and `Channel.refreshToken` with the old key and re-encrypts them with the new key.
3. Update `TOKEN_ENCRYPTION_KEY` in `.env`.
4. Bring the application back online.
Until such a script is written, **key rotation = forced re-authentication for all channel owners**. See also the [[01 - Technical Debt and Future Work]] backlog.