# External Integrations

**Analysis Date:** 2026-02-26

## APIs & External Services

**AI Providers (configurable via SystemSettings DB table):**
- OpenAI - AI filtering, jury assignment suggestions, evaluation summaries, project tagging, award eligibility, shortlist recommendations
  - SDK: `openai` ^6.16.0
  - Auth: `OPENAI_API_KEY` env var or `openai_api_key` SystemSetting
  - Base URL: `OPENAI_BASE_URL` env var or `openai_base_url` SystemSetting (for OpenAI-compatible proxies)
  - Model: `OPENAI_MODEL` env var or `ai_model` SystemSetting (default: `gpt-4o`)
  - Client: `src/lib/openai.ts` - lazy singleton, reset via `resetOpenAIClient()`

- Anthropic Claude - Alternative AI provider, same AI feature set as OpenAI
  - SDK: `@anthropic-ai/sdk` ^0.78.0
  - Auth: `ANTHROPIC_API_KEY` env var or `anthropic_api_key` SystemSetting
  - Adapter: `src/lib/openai.ts` wraps Anthropic SDK behind OpenAI `.chat.completions.create()` interface
  - Supported models: `claude-opus-4-5-20250514`, `claude-sonnet-4-5-20250514`, `claude-haiku-3-5-20241022`, `claude-opus-4-20250514`, `claude-sonnet-4-20250514`
  - Extended thinking enabled automatically for Opus models

- LiteLLM proxy - Third option for ChatGPT subscription routing (no real API key needed)
  - Config: `ai_provider = 'litellm'` in SystemSettings + `openai_base_url` pointing to proxy
  - Token limit fields stripped for `chatgpt/*` model prefix

- AI provider selection: `getConfiguredProvider()` in `src/lib/openai.ts` reads `ai_provider` SystemSetting; defaults to `openai`

**All AI data is anonymized before sending** via `src/server/services/anonymization.ts`

**Notion:**
- Used for project import (alternative to CSV)
- SDK: `@notionhq/client` ^2.3.0
- Auth: API key stored in SystemSettings (`notion_api_key`), per-import flow
- Client: `src/lib/notion.ts` - `createNotionClient(apiKey)` per-request (not singleton)
- Router: `src/server/routers/notion-import.ts`

**Typeform:**
- Used for project import from form responses
- Auth: API key stored in SystemSettings per-import
- Client: `src/lib/typeform.ts` - plain fetch against `https://api.typeform.com`, no SDK
- Router: `src/server/routers/typeform-import.ts`

**WhatsApp (optional, configurable):**
- Two provider options: Meta WhatsApp Business Cloud API or Twilio WhatsApp
  - Meta provider: `src/lib/whatsapp/meta-provider.ts`
  - Twilio provider: `src/lib/whatsapp/twilio-provider.ts`
  - Abstraction: `src/lib/whatsapp/index.ts` - `getWhatsAppProvider()` reads `whatsapp_provider` SystemSetting
- Auth: API keys stored in SystemSettings (`whatsapp_enabled`, `whatsapp_provider`, provider-specific keys)
- Used for: notification delivery (alternative to email)

## Data Storage

**Databases:**
- PostgreSQL 16 (primary datastore)
  - Connection: `DATABASE_URL` env var (e.g., `postgresql://mopc:${password}@postgres:5432/mopc`)
  - Client: Prisma 6 ORM, `src/lib/prisma.ts` singleton with connection pool (limit=20, timeout=10)
  - Schema: `prisma/schema.prisma` (~95KB, ~100+ models)
  - Migrations: `prisma/migrations/` directory, deployed via `prisma migrate deploy` on startup
  - Test DB: `DATABASE_URL_TEST` env var (falls back to `DATABASE_URL` in test setup)

**File Storage:**
- MinIO (S3-compatible, self-hosted on VPS)
  - Internal endpoint: `MINIO_ENDPOINT` env var (server-to-server)
  - Public endpoint: `MINIO_PUBLIC_ENDPOINT` env var (browser-accessible pre-signed URLs)
  - Auth: `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`
  - Bucket: `MINIO_BUCKET` (default: `mopc-files`)
  - Client: `src/lib/minio.ts` - lazy singleton via Proxy, `getMinioClient()`
  - Access pattern: pre-signed URLs only (15-minute expiry by default), never direct public bucket
  - Key structure: `{ProjectName}/{RoundName}/{timestamp}-{fileName}`
  - File types stored: EXEC_SUMMARY, PRESENTATION, VIDEO, BUSINESS_PLAN, VIDEO_PITCH, SUPPORTING_DOC, OTHER

**Caching:**
- None (in-memory rate limiter in `src/lib/rate-limit.ts`, not a caching layer)
- Note: rate limiter is in-memory only — not suitable for multi-instance deployments

## Authentication & Identity

**Auth Provider: NextAuth v5 (self-hosted)**
- Implementation: `src/lib/auth.ts` + `src/lib/auth.config.ts`
- Adapter: `@auth/prisma-adapter` (stores sessions/tokens in PostgreSQL)
- Strategy: JWT sessions (24-hour default, configurable via `SESSION_MAX_AGE`)
- Session includes: `user.id`, `user.email`, `user.name`, `user.role`, `user.roles[]`, `user.mustSetPassword`

**Auth Providers:**
1. Email (Magic Links)
   - NextAuth `EmailProvider` — magic link sent via Nodemailer
   - Link expiry: 15 minutes (`MAGIC_LINK_EXPIRY` env var or default 900s)
   - Custom send function: `sendMagicLinkEmail()` in `src/lib/email.ts`
2. Credentials (Password + Invite Token)
   - Email/password with bcryptjs hashing (`src/lib/password.ts`)
   - Invite token flow: one-time token clears on first use, sets `mustSetPassword: true`
   - Failed login tracking: 5-attempt lockout, 15-minute duration (in-memory, not persistent)
   - `mustSetPassword` flag forces redirect to `/set-password` before any other page

**Role System:**
- User model has `role` (primary, legacy scalar) and `roles` (array, multi-role)
- `userHasRole()` helper in `src/server/trpc.ts` checks `roles[]` with `[role]` fallback
- 8 roles: `SUPER_ADMIN`, `PROGRAM_ADMIN`, `JURY_MEMBER`, `MENTOR`, `OBSERVER`, `APPLICANT`, `AWARD_MASTER`, `AUDIENCE`

## Monitoring & Observability

**Error Tracking:**
- Not detected (no Sentry, Datadog, or similar third-party service)

**Logs:**
- Custom structured logger: `src/lib/logger.ts`
- Tagged format: `{timestamp} [LEVEL] [Tag] message data`
- Levels: debug, info, warn, error
- Default: `debug` in development, `warn` in production
- Configurable via `LOG_LEVEL` env var
- All output to `console.*` (stdout/stderr)

**Audit Logging:**
- All auth events and admin mutations logged to `AuditLog` DB table
- `src/server/utils/audit.ts` - `logAudit()` helper
- Events tracked: LOGIN_SUCCESS, LOGIN_FAILED, INVITATION_ACCEPTED, all CRUD operations on major entities
- Cron for cleanup: `src/app/api/cron/audit-cleanup/`

**Application Metrics:**
- Health check endpoint: `GET /api/health` (used by Docker healthcheck)

## CI/CD & Deployment

**Hosting:**
- Self-hosted VPS with Docker
- Nginx reverse proxy with SSL (external to compose stack)
- Domain: `monaco-opc.com`

**CI Pipeline:**
- Gitea Actions (self-hosted Gitea at `code.monaco-opc.com/MOPC/MOPC-Portal`)
- Pipeline builds Docker image and pushes to private container registry
- `REGISTRY_URL` env var configures the registry in `docker/docker-compose.yml`

**Docker Setup:**
- Production: `docker/docker-compose.yml` — app + postgres services
- Dev: `docker/docker-compose.dev.yml` — dev stack variant
- App image: standalone Next.js build (`output: 'standalone'`)
- Entrypoint: `docker/docker-entrypoint.sh` — migrations → generate → seed → start

## Environment Configuration

**Required env vars:**
- `DATABASE_URL` - PostgreSQL connection string
- `NEXTAUTH_URL` - Full URL of the app (e.g., `https://monaco-opc.com`)
- `NEXTAUTH_SECRET` / `AUTH_SECRET` - JWT signing secret
- `MINIO_ENDPOINT` - MinIO server URL (internal)
- `MINIO_ACCESS_KEY` - MinIO access key
- `MINIO_SECRET_KEY` - MinIO secret key
- `MINIO_BUCKET` - MinIO bucket name
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS` - SMTP credentials
- `EMAIL_FROM` - Sender address
- `CRON_SECRET` - Shared secret for cron endpoint authentication

**Optional env vars:**
- `MINIO_PUBLIC_ENDPOINT` - Public-facing MinIO URL for pre-signed URLs
- `OPENAI_API_KEY` - OpenAI API key (also in SystemSettings)
- `OPENAI_MODEL` - Default AI model (default: `gpt-4o`)
- `OPENAI_BASE_URL` - Custom base URL for OpenAI-compatible providers
- `ANTHROPIC_API_KEY` - Anthropic Claude API key
- `POSTE_API_URL` - Poste.io mail server API URL
- `POSTE_ADMIN_EMAIL`, `POSTE_ADMIN_PASSWORD`, `POSTE_MAIL_DOMAIN` - Poste.io admin
- `SESSION_MAX_AGE` - JWT session duration in seconds (default: 86400)
- `MAX_FILE_SIZE` - Max upload size in bytes (default: 524288000 = 500MB)
- `LOG_LEVEL` - Logging verbosity (debug/info/warn/error)
- `MAGIC_LINK_EXPIRY` - Magic link lifetime in seconds (default: 900)

**Secrets location:**
- `.env` file at repo root (read by Docker Compose via `env_file: .env`)
- Runtime secrets also configurable via `SystemSettings` DB table (admin UI)

## Webhooks & Callbacks

**Incoming:**
- `/api/auth/[...nextauth]` - NextAuth callback routes (magic link verification, OAuth if added)
- No third-party webhook receivers detected

**Outgoing:**
- Configurable webhooks via `Webhook` DB model and `src/server/services/webhook-dispatcher.ts`
- Admin-managed via `src/server/routers/webhook.ts` (SUPER_ADMIN only)
- Signed with HMAC-SHA256 (`X-Webhook-Signature: sha256={sig}`)
- Events dispatched: `evaluation.submitted`, `evaluation.updated`, `project.created`, `project.statusChanged`, `round.activated`, `round.closed`, `assignment.created`, `assignment.completed`, `user.invited`, `user.activated`
- Retry logic: configurable max retries per webhook (0–10), retry via cron

## Real-Time

**Server-Sent Events (SSE):**
- Endpoint: `/api/sse/` - in-app notifications push
- Used for: real-time notification delivery to connected clients

**Live Voting Stream:**
- Endpoint: `/api/live-voting/stream/` - SSE stream for live ceremony voting cursor
- Service: `src/server/services/live-control.ts`

## Cron Jobs

All cron endpoints protected by `CRON_SECRET` header check:
- `GET /api/cron/reminders` - Evaluation reminders via `src/server/services/evaluation-reminders.ts`
- `GET /api/cron/digest` - Email digests via `src/server/services/email-digest.ts`
- `GET /api/cron/draft-cleanup` - Remove stale draft evaluations
- `GET /api/cron/audit-cleanup` - Purge old audit log entries

## Email

**SMTP Transport:**
- Provider: Poste.io (self-hosted mail server, port 587)
- Client: Nodemailer 7 via `src/lib/email.ts`
- Config priority: SystemSettings DB > env vars
- Transporter cached, rebuilt when config hash changes
- Error handling: email errors logged but never thrown (non-blocking)

---

*Integration audit: 2026-02-26*
