Files
pn-new-crm/03-ARCHITECTURE-DECISIONS.md
Matt 67d7e6e3d5
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00

16 KiB

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