# 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
