Files
pn-new-crm/CLAUDE.md
Matt Ciaccio fac8021156
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 59s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped
docs: reflect testing infra + Documenso/portal auth conventions in CLAUDE.md
- Quick reference: add commands for every Playwright project + dev tsx helpers
- Conventions: document the Documenso webhook auth pattern (X-Documenso-Secret
  plaintext, not HMAC), the v1.13/2.x response shape normalization layer,
  the email template module location + responsive table layout, and the
  PortalAuthShell pattern that unifies the in-app and email branding
- Environment: document EMAIL_REDIRECT_TO and IMAP_* dev/test-only vars
- New Testing section enumerating the five Playwright projects (setup,
  smoke, exhaustive, destructive, realapi, visual) and what each covers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:48:18 +02:00

9.4 KiB

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

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 <entity>_type + <entity>_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 iddocumentId and recipient idrecipientId; 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 <img> 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 <PortalAuthShell> (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=<address> — when set, every outbound email is rerouted to this address regardless of the requested recipient and the subject is prefixed with [redirected from <original>]. 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.