# System Overview YouTube Studio Flow is a multi-tenant SaaS tool for YouTube channel management. Teams connect their YouTube channels, import their video library, manage video metadata using a description template and block system, and push changes back to YouTube via the API. ## Tech Stack | Layer | Technology | |---|---| | Frontend | Next.js 15 App Router, TypeScript, CSS Modules | | Data fetching | TanStack Query v5 | | Auth state | Zustand | | Backend | NestJS 10, TypeScript | | ORM | Prisma (PostgreSQL) | | Queue | BullMQ + Redis | | Auth | Google OAuth → JWT (access + refresh tokens) | | YouTube integration | `googleapis` package, quota-tracked | ## Process Architecture The backend runs as **two separate Node.js processes** from the same build: | Process | Entry point | Role | |---|---|---| | API server | `src/main.ts` | HTTP API on port 3001 | | Queue worker | `src/worker.ts` | BullMQ job processor (no HTTP) | Both must be running for full functionality. The API enqueues jobs; the worker executes them. ## System Diagram ``` Browser (Next.js) │ │ HTTP/REST (NEXT_PUBLIC_API_URL) ▼ NestJS API (:3001) │ ├──── PostgreSQL (Prisma) ├──── Redis (BullMQ queues) │ └──── BullMQ worker process │ ├──── PostgreSQL (reads/writes) └──── YouTube API (googleapis) ``` ## Request Scoping Every authenticated request is scoped to a team. The JWT payload contains `userId` and `teamId`. All database queries must filter by `channel: { teamId }` or direct `teamId` field. See [[02 - Backend]] for the auth pattern. ## Key Design Decisions - **Denormalized `lintStatus`**: `Video.lintStatus` is a cached column. Any code path that deletes `LintResult` rows must recompute it manually — it is not computed on-the-fly. - **YouTube token encryption**: OAuth tokens are AES-256 encrypted in the DB using `TOKEN_ENCRYPTION_KEY`. If this key changes all channels need re-authentication. - **Queue deduplication**: BullMQ job IDs are used for deduplication. The pattern is `lint-{videoId}` for lint-once and `lint-{videoId}-{timestamp}` for forced reruns. - **No shared code**: Backend and frontend are fully independent — no shared packages, no shared types. The frontend maintains its own API interface definitions in `api.ts`. - **`hasPendingChanges` null-hash fallback**: `Video.lastSyncedHash` is set at import time to establish the YouTube baseline. For videos that existed before hashing was introduced (i.e. `lastSyncedHash IS NULL`), `findAll` falls back to computing a synthetic baseline from `youtubeDescription` and `recordingDate: null` and compares that against the current state hash. This means pre-existing videos correctly show "push pending" only when local changes diverge from the YouTube state, rather than always appearing as pending. Re-importing a channel resets the hash baseline and will clear any locally-pending changes that have not yet been pushed. ## Related - [[02 - Backend]] — Module structure, patterns, shared services - [[03 - Frontend]] — Routing, state management, API layer - [[04 - Database Schema]] — All Prisma models - [[05 - Queue System]] — BullMQ queues and processors - [[06 - Authentication]] — OAuth flow, JWT, guards