# Port Nimara CRM Multi-tenant CRM for marina/port management. Built with Next.js 15 App Router (standalone output), React 19, TypeScript (strict), Tailwind CSS 3, and Drizzle ORM on PostgreSQL. ## Quick reference ```bash pnpm dev # Start dev server pnpm build # Production build pnpm lint # ESLint pnpm format # Prettier pnpm db:generate # Generate Drizzle migrations pnpm db:push # Push schema to DB pnpm db:studio # Drizzle Studio GUI pnpm db:seed # Seed database (tsx src/lib/db/seed.ts) # Tests pnpm exec vitest run # Unit + integration (~3s) pnpm exec playwright test --project=smoke # Click-through smoke (~10min) pnpm exec playwright test --project=exhaustive # Full UI exhaustive pnpm exec playwright test --project=destructive # Archive/delete flows pnpm exec playwright test --project=realapi # Real Documenso/IMAP (opt-in) pnpm exec playwright test --project=visual # Pixel-diff baselines pnpm exec playwright test --project=visual --update-snapshots # Regenerate baselines # Dev helpers pnpm tsx scripts/dev-trigger-portal-invite.ts # Send a portal activation email pnpm tsx scripts/dev-imap-probe.ts # Dump recent IMAP inbox messages ``` ## Tech stack - **Framework:** Next.js 15.1 App Router, `output: 'standalone'`, `experimental.typedRoutes` - **Auth:** better-auth (session cookie: `pn-crm.session_token`) - **Database:** PostgreSQL via `postgres` driver + Drizzle ORM - **Queue:** BullMQ + Redis (ioredis) - **Storage:** MinIO (S3-compatible) - **Realtime:** Socket.IO with Redis adapter - **UI:** Radix UI primitives, shadcn/ui components (`src/components/ui/`), Lucide icons, CVA + tailwind-merge + clsx - **Forms:** react-hook-form + zod resolvers - **Tables:** TanStack Table - **State:** Zustand stores (`src/stores/`), TanStack React Query - **PDF:** pdfme - **Email:** nodemailer + imapflow + mailparser - **AI:** OpenAI SDK (optional) - **Testing:** Vitest (unit), Playwright (e2e) - **Logging:** pino + pino-pretty ## Project structure ``` src/ app/ (auth)/ # Login/auth pages (dashboard)/ # Main app - route: /[portSlug]/... (portal)/ # Client portal api/ # API routes components/ ui/ # shadcn/ui base components layout/ # Shell, sidebar, header [domain]/ # Domain components (clients, invoices, berths, etc.) shared/ # Cross-domain shared components hooks/ # React hooks (use-auth, use-permissions, use-socket, etc.) lib/ api/ # API client utilities auth/ # better-auth config db/ schema/ # Drizzle schema (one file per domain) migrations/ # Generated Drizzle migrations env.ts # Zod env validation (SKIP_ENV_VALIDATION=1 bypasses) services/ # Business logic services validators/ # Zod schemas for API input validation utils/ # Shared utilities middleware.ts # Auth middleware (cookie check, redirects) providers/ # React context providers stores/ # Zustand stores types/ # Shared TypeScript types ``` ## Conventions - **TypeScript:** Strict mode with `noUncheckedIndexedAccess`. No `any` (ESLint error). - **Formatting:** Prettier - single quotes, semicolons, trailing commas, 2-space indent, 100 char line width. - **Lint:** ESLint flat config extending `next/core-web-vitals`, `next/typescript`, `prettier`. Unused vars prefixed with `_` are allowed. - **Imports:** Use `@/*` path alias (maps to `src/*`). - **Components:** shadcn/ui pattern - base components in `src/components/ui/`, domain components in `src/components/[domain]/`. Yacht / company / reservation domains live in `components/yachts`, `components/companies`, `components/reservations` respectively. - **DB schema:** One file per domain in `src/lib/db/schema/`, re-exported from `index.ts`. Relations in `relations.ts`. Domain files include `clients.ts`, `yachts.ts`, `companies.ts`, `reservations.ts`, `interests.ts`, `berths.ts`, `documents.ts`, `invoices.ts`, etc. - **Polymorphic ownership:** Yachts and invoice billing-entities use `_type` + `_id` column pairs (`'client' | 'company'`). Resolve owner identity through `src/lib/services/yachts.service.ts` / `eoi-context.ts` rather than reading the columns ad hoc — those services apply the type discriminator. - **EOI generation:** Two pathways share the same `EoiContext` (`src/lib/services/eoi-context.ts`). Documenso pathway calls the template-generate endpoint via `documenso-payload.ts`; in-app pathway fills the same source PDF (`assets/eoi-template.pdf`) via `src/lib/pdf/fill-eoi-form.ts` (pdf-lib AcroForm). Routed through `generateAndSign(...)` in `src/lib/services/document-templates.ts` with a `pathway` parameter. - **Merge fields:** Token catalog lives in `src/lib/templates/merge-fields.ts`; the `createTemplateSchema` validator uses `VALID_MERGE_TOKENS` as an allow-list, so unknown tokens are rejected at template creation time. - **Documenso webhooks:** Documenso (both v1.13 and 2.x) authenticates outbound webhooks by sending the configured secret in plaintext via the `X-Documenso-Secret` header — there is no HMAC. The receiver at `src/app/api/webhooks/documenso/route.ts` does a timing-safe equality check via `verifyDocumensoSecret`. Event names arrive as the uppercase Prisma enum on the wire (`DOCUMENT_SIGNED`, `DOCUMENT_COMPLETED`, etc.) even though the UI displays them as lowercase-dotted. The route also normalizes lowercase-dotted variants for forward-compat. - **Documenso API responses:** 2.x renamed `id` → `documentId` and recipient `id` → `recipientId`; v1.13 still uses `id`. `src/lib/services/documenso-client.ts` runs every response through `normalizeDocument()` which reads either field name and surfaces the legacy `id` form to downstream consumers. - **Email templates:** Branded HTML lives in `src/lib/email/templates/`. The portal-auth flow uses `portal-auth.ts` (activation + reset). All templates use the legacy table-based layout with the Port Nimara logo + blurred overhead background, max-width 600px and `width:100%` for responsive shrink. The `` URLs reference `s3.portnimara.com` directly (will move to `/public` later). - **Portal auth pages:** `/portal/login`, `/portal/activate`, `/portal/reset-password` all wrap their content in `` (`src/components/portal/portal-auth-shell.tsx`) which renders the same blurred background + logo + white card the email templates use, so the in-app and email surfaces look unified. - **Routes:** Multi-tenant via `[portSlug]` dynamic segment. Typed routes enabled. - **Pre-commit:** Husky + lint-staged runs ESLint fix + Prettier on staged `.ts`/`.tsx` files. The hook also blocks `.env*` files (including `.env.example`) from being committed; pass them via a separate workflow if needed. ## Environment Copy `.env.example` to `.env` for local dev. See `src/lib/env.ts` for the full schema. Set `SKIP_ENV_VALIDATION=1` to bypass validation (used in Docker build). Optional dev/test-only env vars (not in `.env.example`): - `EMAIL_REDIRECT_TO=
` — when set, every outbound email is rerouted to this address regardless of the requested recipient and the subject is prefixed with `[redirected from ]`. Dev safety net so seeded fake-client emails don't escape; **must be unset in production**. - `IMAP_HOST` / `IMAP_PORT` / `IMAP_USER` / `IMAP_PASS` — read by `tests/e2e/realapi/portal-imap-activation.spec.ts` to fetch the activation email from a real mailbox during the IMAP round-trip test. The spec skips when any are missing. ## Testing Five Playwright projects, defined in `playwright.config.ts`: - `setup` — global setup (seeds users, port, berths, system settings). - `smoke` — fast click-through over every major flow. Run on every change (~10 min, 125 specs). - `exhaustive` — deeper UI coverage that takes longer. - `destructive` — archive/delete/cancel paths against throwaway entities. - `realapi` — opt-in suite that hits real external services (Documenso send-side + IMAP round-trip). Requires `DOCUMENSO_API_*`, `SMTP_*`, `IMAP_*` env. Cloudflared tunnel needs to be running so Documenso can call the local webhook receiver. - `visual` — pixel-diff baselines for stable list/landing pages. Snapshots committed under `tests/e2e/visual/snapshots.spec.ts-snapshots/`. Regenerate with `--update-snapshots` after intentional UI changes. Vitest covers unit + integration with mocked external services (`tests/unit/`, `tests/integration/`). ## Docker - `Dockerfile` - Production multi-stage build (deps -> build -> runner) - `Dockerfile.dev` - Dev with bind-mounted source - `Dockerfile.worker` - BullMQ worker process - `docker-compose.yml` / `docker-compose.dev.yml` / `docker-compose.prod.yml` ## Architecture docs Numbered spec files in repo root (`01-CONSOLIDATED-SYSTEM-SPEC.md` through `15-DESIGN-TOKENS.md`) contain detailed architecture decisions, feature specs, DB schema docs, API catalog, and implementation sequence. Domain-specific references: - `docs/eoi-documenso-field-mapping.md` — canonical mapping from `EoiContext` paths to the Documenso template's `formValues` keys, with the matching AcroForm field names used by the in-app pathway. - `assets/README.md` — what the in-app EOI source PDF must contain and how to override its path in dev/test.