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>
23 KiB
Port Nimara CRM — Full-Stack Architecture Comparison
Compiled: 2026-03-11 Context: AI-first development (Claude Code + Codex), self-hosted Docker, real-time updates required, PostgreSQL + Drizzle ORM already decided, website is separate codebase needing public API
Constraints (apply to ALL options)
These are fixed regardless of framework choice:
| Constraint | Detail |
|---|---|
| Database | PostgreSQL + Drizzle ORM |
| Auth | Keycloak OIDC (existing infrastructure) |
| File storage | MinIO S3 (existing infrastructure) |
| E-signatures | Documenso (existing infrastructure, self-hosted) |
| Deployment | Docker containers, self-hosted server, Gitea CI/CD |
| Real-time | Live updates required (berth status changes, interest updates, signature events) |
| Public API | REST endpoints for website berth map + interest registration |
| Development | AI-assisted (Claude Code / Codex writing ~90%+ of code) |
| Team size | Solo developer (Matt) + AI tools |
Option A: Next.js (React) + tRPC
The Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript (strict) |
| UI library | React 19 |
| Styling | Tailwind CSS + Maritime design tokens |
| Components | shadcn/ui (copy-paste, Radix primitives, fully customizable) |
| Internal API | tRPC v11 (type-safe, no API routes to define for CRM pages) |
| Public API | Next.js Route Handlers (REST endpoints for website) |
| Real-time | tRPC subscriptions over WebSocket (via ws or uWebSocket) |
| State | TanStack Query (server state) + Zustand (UI state) |
| ORM | Drizzle ORM |
| Jobs | BullMQ + Redis |
| Containerization | Docker (node:20-alpine) + nginx reverse proxy |
How it handles your CRM use cases
Interest CRUD: tRPC procedure interests.create / interests.update / interests.list — fully type-safe from database schema to UI component. Change a field in Drizzle schema, TypeScript immediately flags every component that references it.
Real-time berth status: tRPC subscription berths.onStatusChange — when a berth is linked to an interest and auto-moves to "Under Offer", all connected clients receive the update via WebSocket. No polling needed.
EOI/Documenso workflow: Server-side tRPC procedures handle the full lifecycle. Webhook endpoint is a standard Next.js Route Handler (REST, since Documenso calls it externally).
Public berth map API: Standard Next.js Route Handlers — GET /api/public/berths and POST /api/public/interests. These are separate from tRPC and serve the website.
Spec sheet import (AI-assisted): Upload endpoint receives PDF/Excel, server-side processing with SheetJS (Excel) or PDF extraction, AI API call to interpret/map columns, preview diff shown to admin, confirmation triggers bulk upsert via Drizzle.
Docker deployment: Official self-hosting guide. Standalone output mode produces a minimal Node.js server. Requires nginx reverse proxy for production (handles TLS, rate limiting, slow connections). Redis sidecar for BullMQ + session cache.
Assessment
| Dimension | Rating | Notes |
|---|---|---|
| AI code generation quality | Excellent | Largest training corpus. Claude Code is most fluent with React/Next.js. shadcn/ui has massive example coverage. |
| Component ecosystem | Excellent | Deepest of any framework. Every library has a React-first version. TanStack Table, React Hook Form, Recharts, etc. |
| tRPC integration | Excellent | First-class support. Most tRPC examples are Next.js. |
| Real-time (WebSocket) | Good | Works but requires separate WebSocket server alongside Next.js (Vercel doesn't support WS, but irrelevant for self-hosting). Needs next-ws or custom server. |
| Self-hosted Docker | Good | Works well with standalone output mode. Requires nginx reverse proxy. More configuration than Nuxt/SvelteKit. |
| Bundle size / performance | Adequate | React runtime is larger than alternatives. For an internal CRM used by a small team, this is irrelevant. |
| Learning curve (reading code) | Moderate | JSX, hooks, useEffect patterns. React has more "gotchas" than Vue/Svelte but AI handles them well. |
| Long-term maintenance | Excellent | Backed by Vercel. Massive community. Won't disappear. |
Pros:
- AI tools generate the most reliable code in this ecosystem
- Largest component library ecosystem by far
- tRPC + Drizzle + Next.js is an extremely well-documented combination
- shadcn/ui gives you beautiful, accessible components you fully own and can restyle to Maritime
- TanStack Query handles caching, optimistic updates, background refresh out of the box
- Most Stack Overflow answers, most GitHub examples, most blog posts
Cons:
- React's mental model (hooks, closures, re-renders) can be confusing when reading/debugging
- WebSocket support needs custom server setup (not complex, but not built-in)
- Next.js is optimized for Vercel — self-hosting works but you miss some platform features
- Heavier runtime than Svelte (irrelevant for small team internal tool)
- App Router patterns still evolving — some tutorials use old Pages Router
Option B: Nuxt 3 (Vue) + tRPC
The Stack
| Layer | Technology |
|---|---|
| Framework | Nuxt 3 (latest) |
| Language | TypeScript (strict) |
| UI library | Vue 3 (Composition API) |
| Styling | Tailwind CSS + Maritime design tokens |
| Components | Nuxt UI v3 (Radix Vue based, Tailwind-native) OR Radix Vue + custom |
| Internal API | trpc-nuxt (community adapter) |
| Public API | Nitro server routes (REST endpoints for website) |
| Real-time | Nitro WebSocket (experimental) or Socket.io alongside Nitro |
| State | VueQuery / TanStack Query Vue (server state) + Pinia (UI state) |
| ORM | Drizzle ORM |
| Jobs | BullMQ + Redis |
| Containerization | Docker (node:20-alpine), Nitro handles both API + SPA |
How it handles your CRM use cases
Interest CRUD: trpc-nuxt procedures with same type-safety pattern as Next.js. Vue's Composition API (ref, computed, watch) is arguably more intuitive than React hooks for someone reading code.
Real-time berth status: Nitro's experimental WebSocket support (CrossWasm v1 in Nitro v3). Less mature than the Next.js WebSocket ecosystem. Alternatively, run Socket.io alongside Nitro — works but adds complexity.
EOI/Documenso workflow: Nitro server routes handle webhooks. Same pattern as current system (Nitro is what you already have).
Public berth map API: Nitro server routes — cleaner than Next.js Route Handlers. Nitro is genuinely good at this.
Spec sheet import: Same approach as Next.js — server-side processing with AI interpretation.
Docker deployment: Nitro's output is a self-contained Node.js server. No reverse proxy strictly required (though recommended). Simpler Docker setup than Next.js.
Assessment
| Dimension | Rating | Notes |
|---|---|---|
| AI code generation quality | Good | Well-represented in training data. Slightly fewer examples than React. Claude Code handles Vue/Nuxt competently. |
| Component ecosystem | Good | Nuxt UI v3 is excellent and built specifically for Nuxt. Fewer third-party options than React. |
| tRPC integration | Adequate | Community adapter (trpc-nuxt). Works but fewer examples, less battle-tested than Next.js tRPC. |
| Real-time (WebSocket) | Adequate | Nitro WebSocket is experimental. Works with adapter-node but less mature. Socket.io fallback is proven. |
| Self-hosted Docker | Excellent | Nitro produces clean standalone output. Simplest Docker setup of the three. |
| Bundle size / performance | Good | Smaller than React, larger than Svelte. |
| Learning curve (reading code) | Easy | Vue's template syntax is the most readable. <script setup> is clean. Closest to plain HTML. |
| Long-term maintenance | Good | Backed by Nuxt Labs. Strong community. Stable but smaller than React ecosystem. |
Pros:
- You already have familiarity with the patterns from the current codebase
- Vue's template syntax is the most human-readable (easiest to understand when reviewing AI output)
- Nitro server is genuinely excellent for API routes and deployment flexibility
- Nuxt UI v3 is tightly integrated and well-maintained by the Nuxt team
- Simplest Docker deployment (Nitro just works)
- Pinia + VueQuery is a clean state management story
Cons:
- Smaller AI training corpus than React — more edge cases where Claude Code may hallucinate APIs
- tRPC support is community-maintained, not official
- WebSocket/real-time support is less mature than alternatives
- Fewer third-party component libraries to choose from
- Risk of repeating patterns from the current codebase rather than improving them
Option C: SvelteKit + tRPC
The Stack
| Layer | Technology |
|---|---|
| Framework | SvelteKit 2 |
| Language | TypeScript (strict) |
| UI library | Svelte 5 (runes) |
| Styling | Tailwind CSS + Maritime design tokens |
| Components | Skeleton UI or shadcn-svelte (Bits UI based) |
| Internal API | trpc-sveltekit (community adapter) |
| Public API | SvelteKit server routes (REST endpoints for website) |
| Real-time | trpc-sveltekit experimental WS or Socket.io sidecar |
| State | TanStack Query Svelte (server state) + Svelte stores (UI state) |
| ORM | Drizzle ORM |
| Jobs | BullMQ + Redis |
| Containerization | Docker (node:20-alpine) via adapter-node |
How it handles your CRM use cases
Interest CRUD: trpc-sveltekit procedures. Svelte's reactivity model is the simplest of the three — $state rune replaces React's useState, Vue's ref. Less boilerplate.
Real-time berth status: Experimental WebSocket via trpc-sveltekit (only works with adapter-node). Less mature than both Next.js and Nuxt options.
EOI/Documenso workflow: SvelteKit server routes. Clean pattern but fewer examples specific to webhook handling.
Public berth map API: SvelteKit server routes. Works well, similar to Nuxt's Nitro routes.
Spec sheet import: Same approach as others. Fewer AI-processing library examples in Svelte ecosystem.
Docker deployment: adapter-node produces a clean, lightweight Node server. Smallest footprint of the three.
Assessment
| Dimension | Rating | Notes |
|---|---|---|
| AI code generation quality | Adequate | Smallest training corpus. Svelte 5 runes are new — AI tools may generate Svelte 4 patterns. More manual correction needed. |
| Component ecosystem | Limited | Skeleton UI and shadcn-svelte exist but are less mature. Many libraries are React/Vue only. |
| tRPC integration | Adequate | Community adapter. WebSocket support explicitly marked "experimental". |
| Real-time (WebSocket) | Limited | SvelteKit doesn't natively support WebSockets. Experimental via adapter-node only. Most constrained option. |
| Self-hosted Docker | Excellent | Smallest container image. Lowest resource usage. Cleanest adapter-node output. |
| Bundle size / performance | Excellent | 40-60% smaller bundles than React. Fastest runtime. Lowest memory usage. |
| Learning curve (reading code) | Easy | Most intuitive syntax. Closest to vanilla HTML/JS. Svelte 5 runes are clean. |
| Long-term maintenance | Good | Strong community, Vercel backing. Smaller ecosystem means fewer resources if you hit edge cases. |
Pros:
- Smallest bundle size and lowest resource usage (best for self-hosting efficiency)
- Most intuitive syntax — easiest to read and understand as a non-programmer
- Svelte 5 runes are the cleanest reactivity model available
- Lightest Docker footprint
- Developer experience is genuinely the best of the three for humans
Cons:
- Smallest AI training corpus — Claude Code and Codex will produce less reliable code, especially for complex patterns
- Svelte 5 is recent — AI tools may mix up Svelte 4 and Svelte 5 syntax
- Weakest real-time/WebSocket story — experimental only
- Smallest component ecosystem — you'll build more from scratch
- Fewer Stack Overflow answers and GitHub examples to draw from when stuck
- Some React-only libraries have no Svelte equivalent (form builders, advanced table components, etc.)
Option D: Next.js (React) + REST (no tRPC)
Same as Option A but replacing tRPC with traditional REST API routes. Included because tRPC adds complexity and you may prefer a simpler architecture.
Differences from Option A
| Aspect | tRPC (Option A) | REST (Option D) |
|---|---|---|
| API definition | Procedures on server, auto-typed on client | Route Handlers on server, manual fetch + types on client |
| Type safety | Automatic end-to-end | Manual (Zod schemas shared, but not enforced across boundary) |
| Code generation | Less code (no fetch wrappers needed) | More code (API client layer, type definitions) |
| Public API for website | Separate REST routes alongside tRPC | Same REST routes serve both CRM and website |
| Real-time | tRPC subscriptions (WebSocket) | Socket.io or Server-Sent Events (separate from API) |
| External consumers | tRPC only works with TypeScript clients | REST works with any client (mobile app, website, third-party) |
| Complexity | Higher (tRPC + REST coexist for different consumers) | Lower (one pattern for everything) |
| Debugging | tRPC calls don't show in browser Network tab as clean REST calls | Standard HTTP requests, easy to inspect |
Assessment
| Dimension | Rating | Notes |
|---|---|---|
| AI code generation quality | Excellent | Even more training data for REST patterns than tRPC. |
| Simplicity | Excellent | One pattern. No tRPC learning curve. Every endpoint is a standard HTTP route. |
| Type safety | Good | Zod validation + shared types. Not automatic like tRPC but works well. |
| Real-time | Good | Socket.io alongside Next.js is well-documented. Or Server-Sent Events for simpler cases. |
| External API consumers | Excellent | Same API serves CRM, website, and any future mobile app or integration. |
| OpenAPI compatibility | Excellent | REST routes can auto-generate OpenAPI docs. tRPC cannot. |
Pros:
- Simplest mental model — everything is HTTP requests
- One API pattern instead of tRPC + REST coexisting
- Easier to debug (browser Network tab shows clean requests)
- Same API serves CRM and website (no separate public routes needed)
- If you ever build a mobile app or external integration, the API is ready
- More AI training examples for REST than tRPC
Cons:
- More boilerplate (API client layer, manual type sharing)
- No automatic type safety across client-server boundary
- Need to manually keep request/response types in sync
- Real-time requires a separate Socket.io or SSE layer
Head-to-Head Comparison
| Factor | Next.js + tRPC (A) | Nuxt 3 + tRPC (B) | SvelteKit + tRPC (C) | Next.js + REST (D) |
|---|---|---|---|---|
| AI code gen reliability | ★★★★★ | ★★★★ | ★★★ | ★★★★★ |
| Component ecosystem | ★★★★★ | ★★★★ | ★★★ | ★★★★★ |
| Real-time maturity | ★★★★ | ★★★ | ★★ | ★★★★ |
| Docker self-hosting | ★★★★ | ★★★★★ | ★★★★★ | ★★★★ |
| Type safety (end-to-end) | ★★★★★ | ★★★★ | ★★★★ | ★★★ |
| Code readability | ★★★ | ★★★★ | ★★★★★ | ★★★ |
| Architecture simplicity | ★★★ | ★★★ | ★★★ | ★★★★★ |
| Bundle size / performance | ★★★ | ★★★★ | ★★★★★ | ★★★ |
| Public API flexibility | ★★★ | ★★★★ | ★★★ | ★★★★★ |
| Long-term ecosystem | ★★★★★ | ★★★★ | ★★★★ | ★★★★★ |
Recommendation Matrix
If AI code generation reliability is your top priority → Option A or D (Next.js)
If deployment simplicity matters most → Option B (Nuxt) or C (SvelteKit)
If you want the simplest architecture to understand and maintain → Option D (Next.js + REST)
If you want maximum type safety with minimum code → Option A (Next.js + tRPC)
If performance and resource efficiency matter most → Option C (SvelteKit)
Author's Recommendation
Option D (Next.js + REST) is the strongest fit for your specific situation. Here's why:
-
AI-first development: Next.js React has the deepest training corpus. REST has even more examples than tRPC. You're optimizing for AI output quality, and this combination maximizes it.
-
Architectural simplicity: tRPC is elegant but adds a second API paradigm alongside the REST routes you need for the website anyway. With REST-only, every endpoint follows the same pattern — simpler for AI tools to generate consistently and simpler for you to understand.
-
Real-time: Socket.io alongside Next.js is battle-tested and well-documented. Simpler than tRPC subscriptions, which require a custom WebSocket server.
-
Public API: The same REST endpoints serve the CRM frontend, the website berth map, and any future integration. No separate public routes to maintain.
-
Future-proofing: If you ever need a mobile app, an external integration, or OpenAPI documentation, REST is ready. tRPC locks you into TypeScript clients.
-
Self-hosting: Next.js standalone output in Docker behind nginx is well-documented. Not the simplest (Nuxt wins there), but the trade-off for the ecosystem advantages is worth it.
The one thing you'd give up vs. Option A is automatic end-to-end type safety. But with Zod schemas shared between server and client plus TanStack Query's typed hooks, the gap is small in practice.
Runner-up: Option A (Next.js + tRPC) if you value automatic type safety highly and don't mind two API paradigms coexisting.