Files
MOPC-Portal/.planning/codebase/ARCHITECTURE.md

194 lines
10 KiB
Markdown
Raw Normal View History

# Architecture
**Analysis Date:** 2026-02-26
## Pattern Overview
**Overall:** Role-gated multi-tenant monolith with end-to-end type safety (Prisma → tRPC → React)
**Key Characteristics:**
- Single Next.js 15 App Router application serving all roles (admin, jury, applicant, mentor, observer, public)
- All API calls go through tRPC with superjson serialization; no separate REST API for client data
- Role enforcement happens at two levels: layout-level (`requireRole()`) and procedure-level (tRPC middleware)
- Service layer (`src/server/services/`) contains all business logic — routers delegate immediately to services
- All state machine transitions are audited via `DecisionAuditLog` through `src/server/utils/audit.ts`
## Layers
**Data Layer:**
- Purpose: Schema definition, migrations, query client
- Location: `prisma/schema.prisma`, `src/lib/prisma.ts`
- Contains: Prisma schema, singleton PrismaClient with connection pool (limit=20, timeout=10)
- Depends on: PostgreSQL 16
- Used by: Service layer, tRPC context
**Service Layer:**
- Purpose: Business logic, state machines, external integrations
- Location: `src/server/services/`
- Contains: Round engine, deliberation, assignment, AI services, submission manager, live control, result lock, notification
- Depends on: Prisma client (passed as param to allow transactional usage), `src/lib/` utilities
- Used by: tRPC routers only
**API Layer (tRPC):**
- Purpose: Type-safe RPC procedures with role-based access control
- Location: `src/server/routers/`, `src/server/trpc.ts`, `src/server/context.ts`
- Contains: 44+ domain routers assembled in `src/server/routers/_app.ts`, middleware hierarchy, tRPC context
- Depends on: Service layer, Prisma (via context)
- Used by: Client components via `src/lib/trpc/client.ts`
**UI Layer:**
- Purpose: Server and client React components, pages, layouts
- Location: `src/app/`, `src/components/`
- Contains: Route groups per role, layouts with role guards, client components using tRPC hooks
- Depends on: tRPC client, shadcn/ui, Tailwind CSS
- Used by: Browser clients
**Shared Utilities:**
- Purpose: Cross-cutting helpers available everywhere
- Location: `src/lib/`
- Contains: Auth config, Prisma singleton, email, MinIO client, OpenAI client, logger, rate limiter, feature flags, storage provider abstraction
- Depends on: External services
- Used by: Service layer, routers, layouts
## Data Flow
**Client Query Flow:**
1. React component calls `trpc.domain.procedure.useQuery()` via `src/lib/trpc/client.ts`
2. Request hits `src/app/api/trpc/[trpc]/route.ts` — rate limited at 100 req/min per IP
3. tRPC resolves context (`src/server/context.ts`): auth session + prisma singleton + IP/UA
4. Middleware chain runs: authentication check → role check → procedure handler
5. Router delegates to service (e.g., `roundEngineRouter``src/server/services/round-engine.ts`)
6. Service queries Prisma, may call external APIs, writes audit log
7. Superjson-serialized result returns to React Query cache
**Mutation Flow (with audit trail):**
1. Component calls `trpc.domain.action.useMutation()`
2. tRPC middleware validates auth + role
3. Router calls `logAudit()` before or after service call
4. Service performs database work inside `prisma.$transaction()` when atomicity required
5. Service writes its own `logAudit()` for state machine transitions
6. Cache invalidated via `utils.trpc.invalidate()`
**Server-Sent Events Flow (live voting/deliberation):**
1. Client subscribes via `src/hooks/use-live-voting-sse.ts` or `src/hooks/use-stage-live-sse.ts`
2. SSE route `src/app/api/live-voting/stream/route.ts` polls database on interval
3. Events emitted for vote count changes, cursor position changes, status changes
4. Admin cursor controlled via `src/server/services/live-control.ts` → tRPC `liveRouter`
**State Management:**
- Server state: React Query via tRPC hooks (cache + invalidation)
- Edition/program selection: `src/contexts/edition-context.tsx` (localStorage + URL param + React Context)
- Form state: Local React state with autosave timers (evaluation page uses refs to prevent race conditions)
- No global client state library (no Redux/Zustand)
## Key Abstractions
**Competition/Round State Machine:**
- Purpose: Governs round lifecycle and per-project states within rounds
- Examples: `src/server/services/round-engine.ts`
- Pattern: Pure functions with explicit transition maps; `VALID_ROUND_TRANSITIONS` and `VALID_PROJECT_TRANSITIONS` constants define allowed moves. All transitions are transactional and audited.
- Round transitions: `ROUND_DRAFT → ROUND_ACTIVE → ROUND_CLOSED → ROUND_ARCHIVED`
- Project-in-round transitions: `PENDING → IN_PROGRESS → PASSED/REJECTED → COMPLETED/WITHDRAWN`
**tRPC Procedure Types (RBAC middleware):**
- Purpose: Enforce role-based access at the API boundary
- Examples: `src/server/trpc.ts`
- Pattern: `publicProcedure`, `protectedProcedure`, `adminProcedure`, `superAdminProcedure`, `juryProcedure`, `mentorProcedure`, `observerProcedure`, `awardMasterProcedure`, `audienceProcedure`
- Each is a pre-configured middleware chain; routers simply choose the correct type
**CompetitionContext resolver:**
- Purpose: Loads the full typed context for a round (competition + round + parsed configJson + juryGroup + submissionWindows)
- Examples: `src/server/services/competition-context.ts`
- Pattern: `resolveCompetitionContext(roundId)` used by any service that needs the full picture. Also provides `resolveMemberContext()` for jury-member-specific context with assignment counts.
**Round-Type Config Schemas:**
- Purpose: Each RoundType (`INTAKE`, `FILTERING`, `EVALUATION`, etc.) has a dedicated Zod config schema stored in `Round.configJson`
- Examples: `src/types/competition-configs.ts``IntakeConfigSchema`, `FilteringConfigSchema`, `EvaluationConfigSchema`, etc.
- Pattern: `safeValidateRoundConfig(roundType, configJson)` returns typed config or null; `validateRoundConfig()` throws on invalid
**Storage Provider Abstraction:**
- Purpose: Swap MinIO (production) for local filesystem (dev/test) without changing service code
- Examples: `src/lib/storage/types.ts`, `src/lib/storage/s3-provider.ts`, `src/lib/storage/local-provider.ts`, `src/lib/storage/index.ts`
- Pattern: `StorageProvider` interface with `getUploadUrl`, `getDownloadUrl`, `deleteObject`, `putObject`, `getObject`
**AI Pipeline with Anonymization:**
- Purpose: All AI calls strip PII before sending to OpenAI
- Examples: `src/server/services/anonymization.ts` + any `src/server/services/ai-*.ts`
- Pattern: `anonymizeProjectsForAI()` returns `AnonymizedProjectForAI[]` + `ProjectAIMapping`; AI service uses anonymous IDs; results mapped back via mapping object
## Entry Points
**tRPC API Handler:**
- Location: `src/app/api/trpc/[trpc]/route.ts`
- Triggers: All client data queries and mutations
- Responsibilities: Rate limiting (100 req/min), `fetchRequestHandler` with `appRouter` + `createContext`, error logging
**Auth Handler:**
- Location: `src/app/api/auth/[...nextauth]/route.ts`
- Triggers: Login, magic link verification, session management
- Responsibilities: NextAuth v5 with Email + Credentials providers, 5-attempt lockout
**Cron Endpoints:**
- Location: `src/app/api/cron/` (audit-cleanup, digest, draft-cleanup, reminders)
- Triggers: External scheduler via `CRON_SECRET` header check
- Responsibilities: Periodic maintenance — evaluation reminders, draft cleanup, digest emails, audit log rotation
**SSE Stream:**
- Location: `src/app/api/live-voting/stream/route.ts`
- Triggers: Live voting/deliberation pages subscribe as long-running GET connections
- Responsibilities: Poll DB for changes, push events for vote counts, cursor position, status transitions
**Next.js Middleware:**
- Location: `middleware.ts` (root, uses `src/lib/auth.config.ts`)
- Triggers: Every request
- Responsibilities: Auth check (edge-compatible), redirect to `/login` if unauthenticated, redirect to `/set-password` if `mustSetPassword` flag set
**Role Layout Guards:**
- Location: `src/app/(admin)/layout.tsx`, `src/app/(jury)/layout.tsx`, etc.
- Triggers: Navigation into role-specific route group
- Responsibilities: Server-side `requireRole()` call, redirect to role dashboard if unauthorized, onboarding gate (jury)
## Error Handling
**Strategy:** Boundary-based — tRPC errors propagate to React Query; service errors use `TRPCError`; audit never throws
**Patterns:**
- Services return typed result objects (`{ success: boolean, errors?: string[] }`) for state machine operations — no throwing
- tRPC procedures throw `TRPCError` with code (`NOT_FOUND`, `FORBIDDEN`, `CONFLICT`, `BAD_REQUEST`) for client-visible errors
- `logAudit()` is wrapped in try-catch — audit failures are logged to console but never surface to users
- AI services use `classifyAIError()` from `src/server/services/ai-errors.ts` to translate OpenAI errors
- Round notification functions explicitly catch all errors and log them — notifications never block round transitions
- Client uses `toast.error()` from sonner for user-facing error display
## Cross-Cutting Concerns
**Logging:**
- `src/lib/logger.ts` — tagged, level-aware (`debug/info/warn/error`), respects `LOG_LEVEL` env var, defaults to `debug` in development and `warn` in production
- Pattern: `logger.info('ServiceName', 'message', { data })` — tag identifies the calling service
**Validation:**
- Zod schemas on all tRPC procedure inputs (`.input(z.object({...}))`)
- `ZodError` is formatted and included in tRPC error response via `errorFormatter` in `src/server/trpc.ts`
- Round config Zod schemas in `src/types/competition-configs.ts` validate `configJson` at activation time
**Authentication:**
- NextAuth v5 with JWT strategy; session available server-side via `auth()` from `src/lib/auth.ts`
- `requireRole(...roles)` in `src/lib/auth-redirect.ts` used by all role layouts — checks `user.roles[]` array with `user.role` fallback
- `userHasRole()` in `src/server/trpc.ts` used inline for fine-grained procedure-level checks
**Audit Trail:**
- `logAudit()` in `src/server/utils/audit.ts` — writes to `AuditLog` table
- Called from both routers (with `ctx.prisma` to share transaction) and services
- Never throws — always wrapped in try-catch
**Feature Flags:**
- `src/lib/feature-flags.ts` — reads `SystemSetting` records with `category: FEATURE_FLAGS`
- Currently one active flag: `feature.useCompetitionModel` (defaults to `true`)
---
*Architecture analysis: 2026-02-26*