Files
pn-new-crm/01-CONSOLIDATED-SYSTEM-SPEC.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

22 KiB
Raw Blame History

Port Nimara CRM — Consolidated System Specification

Compiled: 2026-03-11 Sources: Claude Code audit + Codex audit of client-portal/

This document merges both independent audit outputs into a single authoritative reference for the rebuild.


1. System Architecture

1.1 Current Stack

Layer Technology Role
Framework Nuxt 3 SPA (ssr: false) + Nitro server Client shell, server API, scheduled tasks
Database NocoDB REST API System of record for interests, berths, expenses, invoices, settings, audit logs
Auth Keycloak OIDC SSO, refresh tokens, role/group extraction via JWT groups claim
File Storage MinIO (S3-compatible) Documents, EOI PDFs, expense exports, cached email JSON/attachments
E-Signatures Documenso (self-hosted at signatures.portnimara.dev) EOI generation, 3-party signing, webhook lifecycle, signed PDF retrieval
Outbound Email SMTP via Nodemailer User mailbox sending + reminder notifications (separate SMTP config)
Inbound Email IMAP Thread sync, cached mail archive, sales inbox PDF harvesting
Currency Frankfurter API Exchange rates with local file-based cache fallback
Automations automation.portnimara.com No-code webhooks for request forms and sales handoffs
Deployment Docker (node:20-alpine) + Gitea CI/CD Builds .output, pushes image on main pushes — no tests in pipeline

1.2 Runtime Dependencies (33 production, 6 dev)

Key packages: @nuxt/ui ^3.2.0, @pdfme/common + @pdfme/generator ^5.4.0, @pinia/nuxt, chart.js, date-fns, imap, lodash-es, lucide-vue-next, mailparser, minio, node-cron, nodemailer, nuxt-directus (legacy, unused), pdfkit, sharp, vue-chartjs, vue-toastification, vuetify-nuxt-module (legacy).

Notable: @nuxt/ui and Vuetify coexist alongside a custom Maritime Design System. The frontend is mid-migration. nuxt-directus is installed but auth/data paths use Keycloak and NocoDB.

1.3 External Service Configuration

Service Config Source Credential Status
NocoDB NUXT_NOCODB_URL, NUXT_NOCODB_TOKEN Environment variables (OK)
Keycloak Hardcoded realm/client URLs + KEYCLOAK_CLIENT_SECRET env Mixed — URLs hardcoded, secret in env
MinIO Runtime config in nuxt.config.ts CRITICAL: Access key + secret key hardcoded in source
Documenso Env-driven base URL/API key/template IDs/webhook secret Environment variables (OK)
SMTP/IMAP (user) Per-user credentials cached in-memory (encrypted) In-memory only — lost on restart
SMTP/IMAP (sales) process-sales-eois.ts CRITICAL: Password hardcoded in source
SMTP (reminders) NUXT_REMINDER_SMTP_* env vars Environment variables (OK)
automation.portnimara.com Hardcoded webhook URLs in 3 handlers No auth beyond URL secrecy

1.4 Background Tasks

Task Schedule Mechanism
Pending notification processing Every 30 seconds Nitro experimental task (active)
Signature polling fallback Every 5 minutes (default) setInterval in server plugin
EOI reminders Every 10 minutes (check), fires at 09:00/16:00 Paris time setIntervalcurrently disabled
Sales email processing Manual trigger via internal API Task file exists but not scheduled
Currency rate refresh Manual/startup Task file exists

2. Data Model

2.1 Entity Relationship Map

Relationship Cardinality Storage Notes
Interests ↔ Berths (committed) Many-to-many NocoDB link field cj7v7bb9pa5eyo3 / reverse c7q2z2rb27c1cb5 Core berth assignment. Linking auto-moves berth to Under Offer
Interests ↔ Berths (recommended) Many-to-many NocoDB link field cgthyq2e95ajc52 Suggestions separate from committed links
Expenses → Invoices Many-to-one (soft) Expenses.invoice_id + denormalized Invoices.expense_ids (comma-separated) No relational constraint; manual sync required
Interests → EOI Documents Attachment array Interests.EOI Document field Not modeled as own table
Interests → Documenso Soft one-to-one Interests.documensoID Remote document can drift from local state
Email threads Object storage only MinIO client-emails bucket as JSON No NocoDB table stores thread records
Users/Roles External only Keycloak JWT groups No app-managed user table

2.2 Core Tables

Interests (table: mbs9hjauug4eseo) — ~60 fields including:

  • Identity: Full Name, Email Address, Phone Number, Address
  • Vessel: Yacht Name, Length, Width, Depth, Berth Size Desired
  • Pipeline: Sales Process Level (8 stages), Lead Category, Source, Date Added
  • EOI State: EOI Status, documensoID, 6 signature link fields, 6 embedded signature link fields, EOI Client/Developer/Oscar Links
  • Timing: EOI Time Sent, Time LOI Sent, Request Form Sent
  • Milestones: Berth Info Sent Status, Contract Sent Status, Deposit 10% Status, Contract Status
  • Notification machinery: ~15 fields for webhook locks, cooldowns, signature timestamps, notification tracking, pending email flags
  • Links: Berths (many-to-many), Berth Recommendations (many-to-many)

Berths (table: mczgos9hr3oa9qc) — Physical marina berths:

  • Identity: Mooring Number, Area, Status (Available/Under Offer/Sold)
  • Dimensions: Length, Width, Draft, Water Depth, Nominal Boat Size (imperial + metric pairs)
  • Infrastructure: Side Pontoon, Power Capacity, Voltage, Mooring Type, Access, Cleat/Bollard Type/Capacity
  • Commercial: Price, Bow Facing
  • Override: status_override_mode, status_last_modified_at
  • Links: Interested Parties (reverse many-to-many)

Expenses (table: mxfcefkk4dqs6uq) — Operational costs:

  • Core: Establishment Name, Price, currency, Payment Method, Category, Payer, Time, Contents
  • Receipts: NocoDB attachment field
  • Payment: payment_status, payment_date, payment_method, payment_reference, payment_notes
  • Link: invoice_id (back-reference)

Invoices (table: mvyvz0lpc30p01s) — Generated from expense groups:

  • Core: invoice_number (INV-YYYYMM-###), client_name, billing_email, due_date, payment_terms, currency, total_amount
  • Payment: status, payment_status, payment_date, payment_method, payment_reference
  • Links: expense_ids (comma-separated string — integrity risk)
  • Output: pdf_path

Singleton tables: Reminder Settings (mfdltoib4bji21u), Alert Settings (m5xl992f4i6e9q7), Audit Logs (audit_logs)

2.3 Non-Table Entities

  • EOI documents: Dual storage — MinIO files under EOIs/ prefix AND Documenso remote documents keyed by Interests.documensoID. The EOI Document field stores arrays of file metadata, not normalized child rows.
  • Email threads: JSON objects and attachments in MinIO client-emails bucket under interest-<id>/.... Not indexed in NocoDB.
  • Users/roles: No app-managed user table. Identity from nuxt-oidc-auth cookie + Keycloak JWT groups claim.

2.4 Data Integrity Risks

  1. All relationships enforced by application code only — NocoDB provides no transaction boundary
  2. Invoices.expense_ids (comma-separated) and Expenses.invoice_id can drift out of sync
  3. EOI state spans three systems (NocoDB fields, Documenso documents, MinIO files) — deleting any layer orphans the others
  4. Signature/reminder state is 15+ nullable text fields instead of a workflow/history table
  5. Audit logging inconsistent: some domains write to audit_logs, others only console.log()
  6. Invoice deletion handler calls getExpenses() with wrong filter shape — linked expenses missed and left orphaned
  7. updateSignatureTimestamps() returns empty update object — timestamps can remain stale
  8. File audit logging functions never write to the real audit_logs table

3. Business Workflows

3.1 Authentication

  • Primary: Keycloak OIDC Authorization Code flow via nuxt-oidc-auth cookie
  • Session caching: 3-min TTL + 5-min grace, 1s rate limit between checks, request deduplication
  • Circuit breaker: 5 failures → open state, 60s reset, 3 retries with exponential backoff
  • Roles: admin > sales > user (from Keycloak groups claim)
  • Internal auth: Static header x-tag: 094ut234 for server-to-server calls (weak)
  • Dev bypass: NUXT_PUBLIC_DEV_AUTH_BYPASS=true skips ALL auth — exposed as public runtime config

3.2 Interest Lifecycle (Sales Pipeline)

8 stages: Open → Details Sent → In Communication → Visited → Signed EOI and NDA → 10% Deposit → Contract → Completed

  • Create: Whitelist filter on fields, date format conversion (dd-mm-yyyy → yyyy-mm-dd), audit logged
  • Update: 3 retries with exponential backoff on 404, monitored fetch wrapper triggers alerts
  • Berth linking: Auto-moves berth status to Under Offer; unlinking only resets to Available when berth is still Under Offer, interest not at high-close state, and no other interested parties remain
  • Frontend auto-promotion: Entering yacht dimensions auto-upgrades Sales Process Level from General to Specific Qualified Interest
  • Duplicate detection: Blocking by name prefix/email domain/phone prefix, then scoring by same email (1.0), same phone (1.0), similar name+address (0.8). Master selected by completeness + recency.

3.3 EOI/Signature Workflow (Documenso)

3-party sequential signing: Client (order 1) → Developer (order 2) → Sales/Approver (order 3)

  • Generation: Requires populated client name, email, yacht name, L×W×D, and ≥1 linked berth. Blocked if manual EOI docs exist. Uses @pdfme/generator with Documenso template. Creates document via API, assigns 3 recipients, stores 12+ signing URL fields on Interest.
  • Webhooks: DOCUMENT_SIGNED → deduplication via signature hash + in-memory lock → queues notification flag (developer or sales) → background task sends email every 30s. DOCUMENT_COMPLETED → downloads signed PDF → emails all 3 parties → stores in MinIO under EOIs/{Client_Name}/.
  • Fallback polling: Every 5 minutes, checks all interests with documensoID against Documenso API, constructs synthetic webhook payloads.
  • Reminders: Time-gated (09:00/16:00 Paris), per-interest reminder_enabled toggle, system-wide cooldown window. Currently disabled in production.
  • Manual upload: Bypasses Documenso entirely — immediately sets EOI Status = Signed and Sales Process Level = Signed EOI and NDA.

3.4 Email System

  • Outbound: Nodemailer SMTP with per-user encrypted credentials (in-memory cache, lost on restart). Sent emails stored in MinIO. HTML templates inlined as string literals in 4+ files.
  • Inbound: IMAP sync with connection pool. Threads cached as JSON in MinIO client-emails bucket. Two parallel implementations exist (pool-based + standalone). V1 and V2 fetch endpoints.
  • Sales inbox: Dedicated polling endpoint with hardcoded credentials harvests EOI-related PDFs.

3.5 Expense/Invoice Workflow

  • Expenses: CRUD with receipt upload (MinIO), filtering by date/payer/category/payment status, CSV and PDF export. Export computes EUR subtotal + 5% processing fee. N+1 fetch patterns for related records.
  • Invoices: Created from grouped expenses with auto-generated INV-YYYYMM-### numbers. Payment terms: immediate/net10/net15/net30/net45/net60. PDF generation applies 2% discount for net10 terms. Cascading payment status updates attempt manual rollback on failure. Invoice deletion has a bug that can miss linked expenses.

3.6 File Management

MinIO-backed file browser with upload/download/preview/rename/delete and folder management. Email attachments surfaced alongside stored documents. Presigned URLs for downloads. Audit logging defined but only prints to console (never writes to audit_logs table).


4. Security & Technical Debt

4.1 Critical (fix before any production traffic)

Issue Location Impact
Hardcoded MinIO credentials nuxt.config.ts lines 159161 Full object storage access to anyone with repo access
Hardcoded sales email credentials server/api/email/process-sales-eois.ts line 27 Full mailbox access exposed in source
Dev auth bypass as public config nuxt.config.ts NUXT_PUBLIC_DEV_AUTH_BYPASS Entire CRM accessible without auth if flag set in production

4.2 High

Issue Location
Trivially guessable internal auth tag (094ut234) Task endpoints, internal API calls
TLS certificate verification disabled email-utils.ts, fetch-thread.ts, send.ts
Debug endpoints expose config without admin guard server/api/debug/*
Destructive test endpoints in production test-eoi-cleanup.ts, test-berth-connection.ts
N+1 query patterns on berth endpoints get-all-interest-berths.ts, get-berth-interested-parties.ts
~400 lines of duplicate EOI generation code generate-eoi-document.ts + generate-quick-eoi.ts
No database migration strategy All table IDs hardcoded as string literals
No transaction support (NocoDB REST API) All multi-step write operations
Inconsistent error response shapes Mixed throw createError() and return { success: false }
Frontend route auth metadata inconsistency roles vs auth.roles — middleware only checks to.meta.roles
Mixed auth systems (Keycloak + leftover Directus) Multiple components still use useDirectusUser()
Invoice deletion filter bug [id].delete.ts calls getExpenses() with wrong filter shape

4.3 Medium

No rate limiting, dual design system (Vuetify + Maritime), critical state in process memory only (webhook store, IMAP pool, credential cache), disabled scheduled reminders, inconsistent API design patterns, silent failures in core operations, retry without circuit breaker, inconsistent ID types, orphaned Documenso references, no caching + unbounded list fetches, blocking I/O in request handlers, weak TypeScript typing (any throughout), dead/unused code, EOI webhook timestamp persistence returning empty objects, file audit logging only printing to console.

4.4 Low

Reminder settings default to test mode, excessive console logging, naming inconsistencies (documeso.ts missing 'n'), missing input validation, ~16 mockup/abandoned pages in production source.


5. API Surface

The current system has ~100 endpoints mixing RPC-style (/api/create-interest), REST-style (/api/invoices/[id].get.ts), and ad-hoc patterns. No consistent pagination, error response shape, or input validation.

Codex proposed clean API (~40 endpoints)

Resource-oriented surface replacing the current ~100:

  • Auth: /auth/login, /auth/logout, /auth/refresh, /auth/session
  • Interests: Standard REST + /interests/{id}/berths, /interests/{id}/recommendations, /interests/{id}/status-transitions, /interests/{id}/eoi-documents/*
  • Berths: GET /berths, GET /berths/{id}, PATCH /berths/{id}
  • Expenses: Standard REST + /expenses/{id}/payments, /expenses/export/csv, /expenses/export/pdf
  • Invoices: Standard REST + /invoices/{id}/payments
  • Files: Standard REST + /files/{id}/download, /files/{id}/preview
  • Email: /email/threads, /email/threads/{id}, /email/messages, /email/connections/test
  • Admin: /admin/audit-logs, /admin/duplicate-jobs, /admin/settings/reminders, /admin/settings/alerts

6. Frontend Architecture

6.1 Component Systems

  • Maritime Design System (new): CSS custom properties (design tokens) for colors, typography, spacing, glassmorphism effects. Components: MaritimeButton, MaritimeCard, MaritimeInput, MaritimeModal, etc. Feature-flagged rollout.
  • Vuetify (legacy): Still loaded globally. Competing styles increase bundle size.
  • Nuxt UI (v3): Also present — three UI systems coexisting.

6.2 State Management

  • One Pinia store (expenses) with 5-min cache TTL, optimistic updates, rollback on failure
  • All other state: page-level refs, composable-level refs, nuxtApp.payload.data
  • Auth state flows through middleware → payload → 3 different composables reading same data

6.3 Pages to Drop in Rebuild

~16 mockup/abandoned/test pages: interest-list-mockup.vue, files-mockup.vue, expense-mockup.vue, dropdown-demo.vue, dropdown-test.vue, sidebar-test.vue, client-support.vue, data.vue, interest-analytics.vue, interest-berth-list.vue, interest-emails.vue, interest-eoi-queue.vue, portnimaraAI.vue, site.vue, social-media.vue, expenses-old.vue

6.4 Iframe Embeds (9 pages)

Metabase (analytics, data), NocoDB views (EOI queue, berth gallery), webmail, Port Nimara AI, site analytics (Umami), social media marketing, client support. Recommendation: replace NocoDB views + webmail with native CRM features; keep 5-6 as iframes.


7. Critical Business Rules to Preserve

These rules are encoded in the current system (sometimes in both frontend and backend) and must carry forward:

  1. Linking a berth to an interest auto-moves Berth.Status from Available to Under Offer
  2. Unlinking a berth only resets to Available when: berth is still Under Offer, interest not at high-close state, and no other interested parties remain
  3. Creating/editing an interest with yacht dimensions can auto-promote sales level from General to Specific Qualified Interest (frontend logic)
  4. Quick EOI generation requires: populated client name, email, yacht name, L×W×D, and ≥1 linked berth
  5. EOI generation blocked if manual/uploaded EOI documents already exist on the Interest
  6. Generated EOIs set EOI Status = Waiting for Signatures and Sales Process Level = EOI and NDA Sent
  7. Manual uploaded EOI documents immediately set EOI Status = Signed and Sales Process Level = Signed EOI and NDA
  8. Documenso completion sends finalized PDF to client + developer + sales, stores signed PDF in MinIO, stamps all_signed_notified_at
  9. Reminder sending gated by per-interest reminder_enabled + system-wide cooldown window
  10. Expense exports compute EUR subtotal + 5% processing fee
  11. PDF invoice generation applies 2% discount for net10 payment terms before any optional fee
  12. Invoice creation must update both Invoice record and every linked Expense back-reference (with rollback attempt on failure)
  13. Roles enforced from Keycloak groups — no app-managed role table