# 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*