# Port Nimara CRM — Architecture Decision Record **Compiled:** 2026-03-11 **Status:** Draft — requires review and sign-off before implementation begins Each decision is numbered for reference. Decisions marked **[NEEDS INPUT]** require Matt's explicit choice before proceeding. --- ## ADR-001: Replace NocoDB with PostgreSQL + Drizzle ORM **Status:** Recommended **Context:** NocoDB serves as both database and admin UI, accessed entirely via REST API. This causes: no transactions (manual rollback code everywhere), no JOINs (N+1 queries), no migrations (schema changes invisible), no type safety (field names as scattered strings), no foreign key constraints (data integrity enforced by application code only). The invoice deletion filter bug and expense_ids comma-separated string are direct consequences. **Decision:** PostgreSQL as primary database. Drizzle ORM for type-safe schema definition, migrations, and query building. **Consequences:** - Every API endpoint must be rewritten against the ORM instead of raw REST calls - Data migration from NocoDB required (export JSON → transform → seed PostgreSQL) - NocoDB can optionally remain connected to PostgreSQL as a read-only admin viewer - Gains: real transactions, JOINs, migrations tracked in version control, foreign key constraints, pg_trgm for fuzzy duplicate detection, proper pagination with cursors **[NEEDS INPUT]:** Do you want to keep NocoDB connected as an admin read view on top of PostgreSQL, or remove it entirely? --- ## ADR-002: Keep Keycloak for Authentication **Status:** Accepted **Context:** Keycloak OIDC works correctly. The circuit breaker, session caching, and token refresh patterns are good. Problems are implementation-level (dev bypass exposed publicly, 3 redundant auth composables) not architectural. **Decision:** Keep Keycloak. Clean up implementation: - Single `useAuth()` composable replacing the current three - Dev bypass moved to server-only config with build-time production guard - Remove all `useDirectusUser()` references - Internal service auth replaced with proper HMAC using env-var secret **Consequences:** - Same Keycloak realm and client configuration can be reused - No user migration needed — Keycloak is the identity source of truth - Auth flow tested in Phase 1 of rebuild --- ## ADR-003: Keep Documenso for E-Signatures **Status:** Accepted **Context:** The 3-party EOI signing workflow is a critical legal process that works end-to-end. Webhook handling includes proper deduplication (signature hashing), locking, and notification chaining. Two duplicate ~400-line implementations exist but the underlying logic is sound. **Decision:** Keep Documenso (self-hosted). Consolidate into a single `EOIService` module with: - Single document generation path (eliminating duplicate code) - Recipient configuration from database/environment (not hardcoded) - Signature events stored as first-class database records (replacing 15+ nullable fields) - Template-based PDF generation with external template files **Consequences:** - Pin Documenso version and add integration tests for webhook handler - Normalize `eoi_documents`, `eoi_recipients`, and `signature_events` as proper database tables - Webhook idempotency at database layer (replacing in-memory store) --- ## ADR-004: Keep MinIO for File Storage **Status:** Accepted **Context:** S3-compatible object storage via MinIO is solid infrastructure. Presigned URLs, organized prefix-based storage, and the file browser pattern all work well. **Decision:** Keep MinIO. Improvements: - ALL credentials via environment variables (rotate current hardcoded keys immediately) - Database-backed file metadata (instead of relying solely on MinIO listing) - Formalized storage namespaces: separate prefixes or buckets for documents, EOIs, email attachments, exports - Proper audit trail (current file audit functions only print to console) **Consequences:** - File metadata table in PostgreSQL enables search, filtering, and relationship tracking - MinIO credentials must be rotated before rebuild goes live --- ## ADR-005: UI Framework — Tailwind CSS + Component Library **Status:** Recommended **Context:** Three UI systems currently coexist: Vuetify (legacy), Maritime Design System (custom CSS tokens), and Nuxt UI v3. The dual-system support adds ~30-40% code volume to templates. Maritime design tokens (colors, typography, spacing, glassmorphism) provide the visual identity. **Decision:** Tailwind CSS as the styling foundation. Maritime design tokens become Tailwind theme extensions. Drop Vuetify entirely. **[NEEDS INPUT]:** Component library choice: - **Option A: Nuxt UI v3** — Already partially in use. Built on Radix Vue. Tight Nuxt integration. Opinionated but productive. - **Option B: Headless UI (or Radix Vue directly)** — More control over styling. Less opinionated. More work for common patterns. - **Option C: shadcn-vue** — Copy-paste components. Full ownership. Good Tailwind integration. **Consequences:** - Maritime glassmorphism aesthetic preserved as Tailwind utilities/theme - All Vuetify imports, theme config, and feature flag toggle code eliminated - Single component library used consistently throughout --- ## ADR-006: State Management — Pinia + TanStack Query **Status:** Recommended **Context:** Currently one Pinia store (expenses) with good patterns (5-min cache, optimistic updates, rollback). All other state is scattered across page-level refs, composable-level refs, and payload data. Auth state read from 3 different composables. **Decision:** Pinia stores for domain state (auth, UI preferences). TanStack Query (VueQuery) for all server state (interests, berths, expenses, invoices, files). **Consequences:** - Automatic cache invalidation and background refetching - Built-in loading/error states per query - Optimistic updates with rollback - Eliminates manual cache TTL patterns - Consistent data fetching pattern across all domains --- ## ADR-007: API Design — REST with Zod Validation **Status:** Recommended **Context:** Current API mixes RPC-style, REST-style, and ad-hoc patterns (~100 endpoints). No input validation. Inconsistent error responses. No pagination. **Decision:** Consistent REST conventions. Reduce from ~100 to ~40-50 well-designed endpoints. Standardize on: - Resource-oriented URLs: `GET /api/interests`, `POST /api/interests`, `PATCH /api/interests/:id` - Zod schemas for ALL request validation (body, query, params) - Standard error response: `{ error: { code, message, details? } }` with proper HTTP status codes - Cursor-based pagination for all list endpoints - Status transitions as explicit actions: `POST /api/interests/:id/transitions` instead of raw field mutation **[NEEDS INPUT]:** Do you want to add OpenAPI/Swagger auto-generated documentation? Useful for future integrations but adds setup overhead. **Consequences:** - Cleaner, documentable API surface - Type-safe request/response contracts - Proper pagination from day one (no more unbounded fetches) --- ## ADR-008: Email Architecture — Hybrid Approach **Status:** Recommended **Context:** Current email system has two parallel IMAP implementations, hardcoded credentials, inline HTML templates in 4+ files, TLS verification disabled, and in-memory credential cache lost on restart. **Decision:** - **Outbound (transactional):** Dedicated email service (Resend or SendGrid) for CRM notifications, reminders, EOI emails. Template management via service. Better deliverability and analytics. - **Outbound (user mailbox):** Keep SMTP for sending-as-user capability. Store credentials encrypted in database (not in-memory). - **Inbound:** Keep IMAP sync for sales inbox and user thread viewing. Move to background worker (not blocking HTTP requests). Store thread metadata in PostgreSQL. - **Templates:** Extract all inline HTML to MJML template files compiled at build time. **[NEEDS INPUT]:** Which transactional email service? Resend (simpler, developer-focused) vs. SendGrid (more established, more features)? **Consequences:** - Single IMAP implementation (eliminate V1/V2 duplication) - All credentials in encrypted database storage or env vars - TLS verification enabled everywhere - Email thread metadata queryable via PostgreSQL --- ## ADR-009: Job Queue — BullMQ + Redis **Status:** Recommended **Context:** Background tasks currently use Nitro experimental tasks and `setInterval` with node-cron. Task results are not tracked. Failures require log analysis. In-memory state (webhook event store, credential cache) lost on restart. **Decision:** BullMQ with Redis for all background processing: - Notification processing (currently 30s poll → event-driven via queue) - Signature polling fallback (keep as scheduled job) - EOI reminders (if re-enabled) - Email sync (background worker) - Currency rate refresh Redis also serves as: - Session store (enables horizontal scaling) - Cache layer (replacing file-based and in-memory caches) - Webhook event deduplication store **Consequences:** - Proper job tracking: start time, end time, success/failure, error details - Built-in retry with exponential backoff and dead letter queue - Admin dashboard for task monitoring - Multi-instance deployment possible (no more single-container state dependency) --- ## ADR-010: Deployment — Single Container + Redis Sidecar **Status:** Recommended **Context:** Current single Nuxt container works but holds all state in memory. Docker + Gitea CI/CD pipeline has no test stage. **Decision:** Keep single Nuxt container for app (Nitro serves both API and SPA). Add Redis container as sidecar. Add PostgreSQL (or connect to existing managed instance). **Improvements to CI/CD:** - Add test stage (Vitest unit + Playwright E2E) before image publish - Environment-specific builds (dev/staging/production) - Health check endpoint for container orchestration **[NEEDS INPUT]:** Where does PostgreSQL run? Options: - **Managed service** (e.g., on the same host, or cloud-managed) — less ops burden - **Another Docker container** — simpler initial setup, more ops responsibility - **Existing infrastructure** — do you already have a PostgreSQL instance anywhere? **Consequences:** - Horizontal scaling possible with Redis handling shared state - Tests run before every deployment - Container health checks enable automatic restart on failure --- ## ADR-011: Testing Strategy **Status:** Recommended **Context:** Currently one test file exists (`session-manager.test.ts`). No integration or E2E tests. CI/CD publishes without running any tests. **Decision:** - **Vitest** for unit tests: Services (EOIService, email, invoice generation), business rule validation, utility functions - **Playwright** for E2E tests: Critical workflows (interest creation, berth linking, EOI generation, expense management) - **Integration tests**: Documenso webhook handler, database migrations, auth flow - Test coverage targets: 80% for services, 100% for business rules (the 13 critical rules listed in the spec) **Consequences:** - CI/CD blocks deployment on test failure - Regression protection for business rules - Confidence in refactoring --- ## ADR-012: Logging and Monitoring **Status:** Recommended **Context:** Nearly every endpoint uses `console.log()` with no level distinction. Debug blocks shipped in production. Audit logging inconsistent (some to database, some to console only). **Decision:** - **Structured logger**: `consola` with level control (debug/info/warn/error) - **Audit logging**: All domain operations write to `audit_logs` table consistently - **Keep custom alert system**: Per-type failure counters, cooldowns, admin notifications — enhance with dashboard - **Remove all debug blocks**: No `console.log("DEBUGGING")` in production **Consequences:** - Log levels configurable per environment - Audit trail complete and queryable - Alert dashboard gives ops team visibility into system health --- ## Decision Summary | # | Decision | Status | | ------- | ----------------------------------------- | --------------------------------------------------- | | ADR-001 | PostgreSQL + Drizzle ORM (replace NocoDB) | Recommended — **needs input on NocoDB admin view** | | ADR-002 | Keep Keycloak | Accepted | | ADR-003 | Keep Documenso | Accepted | | ADR-004 | Keep MinIO | Accepted | | ADR-005 | Tailwind CSS + component library | Recommended — **needs input on component library** | | ADR-006 | Pinia + TanStack Query | Recommended | | ADR-007 | REST + Zod validation | Recommended — **needs input on OpenAPI docs** | | ADR-008 | Hybrid email (service + IMAP) | Recommended — **needs input on email service** | | ADR-009 | BullMQ + Redis | Recommended | | ADR-010 | Single container + Redis sidecar | Recommended — **needs input on PostgreSQL hosting** | | ADR-011 | Vitest + Playwright testing | Recommended | | ADR-012 | Structured logging + consistent audit | Recommended | --- ## Proposed Technology Stack | Layer | Current | Rebuild | | ---------------- | -------------------------------------- | --------------------------------------------- | | Framework | Nuxt 3 | Nuxt 3 (latest) | | UI | Vuetify + Maritime CSS + Nuxt UI | Tailwind CSS + shadcn/ui + Maritime tokens | | State | Mixed (1 Pinia store + refs + payload) | Pinia + TanStack Query | | Database | NocoDB (REST API) | PostgreSQL + Drizzle ORM | | File Storage | MinIO | MinIO (credentials in env) | | Auth | Keycloak OIDC | Keycloak OIDC (cleaned up) | | Signing | Documenso | Documenso (consolidated service) | | Email (outbound) | Nodemailer direct SMTP | Transactional service + SMTP for user mailbox | | Email (inbound) | IMAP (dual implementation) | IMAP (single, background worker) | | PDF | PDFKit + @pdfme | @pdfme only | | Jobs | Nitro tasks + node-cron + setInterval | BullMQ + Redis | | Cache | File-based + in-memory | Redis | | Icons | Lucide + mdi | Lucide only | | Logging | console.log | consola (structured) | | Testing | None | Vitest + Playwright | --- ## Proposed Build Phases **Phase 1 — Foundation (weeks 1-2):** Nuxt 3 + TypeScript strict, PostgreSQL + Drizzle schema, Keycloak auth (single composable), Tailwind + Maritime tokens, layout shell **Phase 2 — Core CRUD (weeks 3-4):** Interest management, berth management, berth-interest linking, RBAC **Phase 3 — Business Workflows (weeks 5-7):** EOI/Documenso integration, email composer + thread viewer, expense management, invoice creation, file management **Phase 4 — Admin & Operations (week 8):** Admin dashboard, audit logging, alert/reminder settings, scheduled background tasks **Phase 5 — Polish & Migration (weeks 9-10):** Data migration from NocoDB → PostgreSQL, performance optimization, PWA (if keeping), testing suite, security hardening