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>
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, andsignature_eventsas 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/transitionsinstead 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:
consolawith level control (debug/info/warn/error) - Audit logging: All domain operations write to
audit_logstable 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) |
| 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