- CLAUDE.md Technik-Block zeigt jetzt den installierten Stand statt der 2026-06/07-Empfehlung: Next.js 15.5.19, Prisma 6.19.3, NestJS 11.1.27, Express 5.2.1, Node node:24-alpine (neu ergaenzt), Vitest je App (3.2.6 / 4.1.9), Docker/Compose als gemessene Wirtseigenschaft - Authentifizierungs-Zeilen ersetzt: kein Identitaetsanbieter im Einsatz, sondern @nestjs/jwt, passport/@nestjs/passport, argon2, ldapts; @nestjs/passport-Zweckangabe korrigiert (Anmelde-/JWT-Strategien statt Modul-zu-Modul-API-Keys) - Nie uebernommene Empfehlungen (Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) sowie die beiden nicht aktualisierten Hauptversionen (Next.js, Prisma) in eigenem Abschnitt "Recommended But Not Adopted" statt in den Ist-Tabellen - Herkunftsvermerk ergaenzt; Alternatives Considered/Version Pinning Strategy/Sources als historische Entscheidungslage gekennzeichnet; Multi-Tenancy Strategy verweist auf die tatsaechliche prisma-tenant.extension.ts - .planning/research/STACK.md erhaelt eine Hinweiszeile, Zahlen darin unveraendert (datiertes Rechercheergebnis) - Keine Abhaengigkeit aktualisiert (package.json/pnpm-lock.yaml unveraendert, per Gate geprueft) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
30 KiB
Technology Stack
Project: Tessera — Modular Portal Platform with Marketplace Latest research: 2026-07-17 (v1.1 Ausschreibungs-Radar delta) — base stack researched 2026-06-18 Overall Confidence: MEDIUM for the v1.1 delta below (versions cross-verified via WebSearch + direct npm registry lookup; no Context7/docs-MCP available this session), HIGH for the v1.0 base stack (unchanged, see bottom section)
v1.1 Delta — Ausschreibungs-Radar Module
Domain: German public-procurement (Vergabe) tender aggregation — API client + HTML scraping adapters + RSS + XML/JSON normalization, on top of the existing NestJS/Prisma monorepo and the DKV module's inbox/mail/scheduler infrastructure.
This section is a delta only. It assumes the validated v1.0 stack below (pnpm/Turborepo, NestJS 11, Prisma 7/PostgreSQL 16, Next.js 16, Vitest) and covers only new libraries needed for this module. No new apps/web dependencies are required — the results UI (searchable list, filters, detail view) is built entirely with the existing Next.js/shadcn/TanStack Query/Zustand stack already in place.
Recommended Additions
Core Technologies (new)
| Technology | Version | Purpose | Why Recommended |
|---|---|---|---|
| fast-xml-parser | 5.10.1 | Parse eForms-DE XML (DÖE) + RSS 2.0 feeds (subreport-elvis, service.bund.de) | Zero-dependency, pure-TS, handles both jobs with one library (XMLParser/XMLBuilder/XMLValidator). Used by Microsoft, NASA, VMware. Faster and simpler than xml2js (callback/SAX-based, heavier). One parser covers eForms XML and RSS XML — see "What NOT to Add" for why a dedicated rss-parser is skipped. |
| cheerio | 1.2.0 | HTML parsing/traversal for the AI AG NetServer and cosinex Vergabemarktplatz scraping adapters | De-facto jQuery-style static HTML parser for Node — ~21k dependents, actively maintained (last release 2026-01-23). Wraps parse5/htmlparser2. 10–50× faster and far cheaper than a headless browser for server-rendered HTML, which both target portals produce for their public search-result pages. |
| csv-parse | 7.0.1 | Parse the DÖE OpenData CSV export as a fallback/cross-check format | stream.Transform-based, part of the adaltas/node-csv toolkit, ~3100 dependents, actively released (14 days old at research time). Ships dual CJS/ESM — safe for apps/api's CommonJS build. Primary DÖE ingestion should use eForms-XML or OCDS-JSON (richer, structured); CSV is secondary/verification. |
| tough-cookie + fetch-cookie | 6.0.2 / 3.2.0 | Cookie-jar session handling for the scraping adapters (search-form POST → paginated results) | apps/api already standardizes on native fetch (see favorites/icon-discovery.service.ts, calendar/providers/ics.provider.ts) — no axios anywhere in the app. fetch-cookie wraps global fetch with a tough-cookie CookieJar so session cookies (e.g. JSESSIONID on the AI AG servlet portal) persist across a scrape run without introducing a second HTTP client family. |
| playwright | 1.61.1 (conditional — see note) | Headless-browser fallback adapter, only if a specific portal's search UI turns out to require client-side JS rendering | AI AG NetServer's PublicationControllerServlet URL pattern is a classic Java servlet MVC front-controller (JSP/form-based) — server-rendered HTML with a session cookie, not an SPA, and (unlike ASP.NET WebForms) has no ViewState/EventValidation mechanic to fight. cosinex markets Vergabemarktplatz as "fully browser-based" (marketing language, not confirmed SPA), making it the more likely candidate to need JS rendering. Build both adapters cheerio-first; only pull in playwright for whichever adapter's public search page is confirmed (phase-1 spike) to require JS execution. Don't add it speculatively — it downloads browser binaries, growing the Docker image ~300MB+, which the "wartbar/verständlich" constraint argues against unless proven necessary. |
Supporting / Dev Tools (optional)
| Tool | Purpose | Notes |
|---|---|---|
| undici (devDependency only) | Mock outbound fetch calls in Vitest tests for the DÖE client and scraping adapters (MockAgent + setGlobalDispatcher) |
Node's global fetch is already powered by undici under the hood; adding it as a devDependency gives official, zero-extra-runtime-cost HTTP mocking without introducing nock/msw (neither currently used anywhere in the repo). Add only when writing the adapter test suite. |
Installation
# apps/api — core additions
pnpm --filter @tessera/api add fast-xml-parser cheerio csv-parse tough-cookie fetch-cookie
# apps/api — conditional, only if phase-1 spike confirms a JS-rendered portal
pnpm --filter @tessera/api add playwright
pnpm --filter @tessera/api exec playwright install chromium # only the one engine needed
# apps/api — dev/test only, optional
pnpm --filter @tessera/api add -D undici
What NOT to Add — Reuse Existing Instead
The DKV module already solved most of the infrastructure this feature needs. This is the most important section for scope control.
| Don't add | Why not | Reuse instead |
|---|---|---|
| A dedicated DÖE / eForms / OCDS SDK | None exists as a maintained npm package for the DÖE OpenData API specifically. It's a plain REST/Swagger endpoint returning XML/JSON/CSV — a heavy SDK would be unmaintained-dependency risk for zero benefit. | Native fetch (already the established pattern) + fast-xml-parser for eForms-XML + plain JSON.parse for OCDS (it's just JSON, no schema library needed on day 1) + csv-parse for the CSV fallback. |
rss-parser |
Latest release is 3.13.0 from 2023-04-11 — 3+ years stale (no security concern since RSS 2.0 is a frozen spec, but it's an extra dependency for a job fast-xml-parser already does). Only 2 feeds to consume (subreport-elvis, service.bund.de), both plain RSS 2.0 — a ~20-line field mapper (title/link/pubDate/guid/description) over fast-xml-parser's output is simpler than a second library's API. |
fast-xml-parser (already added for eForms) + a small internal parseRssFeed() helper. |
axios / axios-cookiejar-support |
Would introduce a second HTTP client family. apps/api has zero axios usage today — both existing HTTP call sites use native fetch. |
Native fetch + fetch-cookie (wraps fetch, not axios). |
node-fetch |
Obsolete since Node 18 ships fetch natively (powered by undici); only relevant for Node ≤16 or legacy stream-handling quirks, neither applies here. |
Native global fetch. |
p-queue (v7+) |
ESM-only — apps/api's tsconfig.json is "module": "commonjs" (confirmed). A native-ESM package in a CJS NestJS build means either a dynamic import() workaround (the codebase already has one messy precedent for cron in dkv-scheduler.service.ts) or build breakage. Overkill anyway: the module polls a handful of sources (DÖE, 1 AI-AG adapter, 1 cosinex adapter, 2 RSS feeds, 1 inbox) on independent cron ticks — no need for a promise-concurrency-queue library. |
A ~20-line internal politeDelay(ms) / sequential-for-loop helper between requests within one adapter run. Simpler, CJS-native, matches the "wartbar für Nicht-Programmierer" constraint. |
bottleneck |
Latest release 2.19.5 is from 2019 — unmaintained for 7 years. A community fork (@rutter/bottleneck) exists but adds a supply-chain trust question for zero real benefit given the low concurrency needs above. |
Same internal delay helper as above. |
| A separate portal-login-credential store for scraping | The feasibility research's recommended tactic explicitly avoids automating authenticated scraping: for portals 2,3,4,6,7,8,9,10 (all except vergabe24/aumass, avoided entirely per their ToS), the admin registers one saved search per portal manually, and Tessera ingests the resulting alert emails — not the authenticated portal UI. Only the public, unauthenticated search-result pages (AI AG NetServer, cosinex DTVP) are scraped. | Reuse the existing tenant-scoped encrypted-credential pattern (CalendarCryptoService, AES-256-GCM, via SettingsModule) only for the inbox connection used to ingest alert emails — same shape of secret the DKV module already stores, not a new credential type. |
@nestjs-modules/mailer for digest/alert sending |
Already an established pitfall in this codebase: it cannot change its SMTP transport after startup, so a runtime SMTP-config change in the admin UI wouldn't take effect without a service restart (documented in dkv-mail.service.ts). |
Copy the DkvMailService pattern: a dedicated AusschreibungMailService that calls nodemailer.createTransport() fresh on every send, reading SettingsService.getDecryptedSmtpConfig(tenantId) each time. |
| A second cron/scheduling mechanism | @nestjs/schedule + SchedulerRegistry.addCronJob() (dynamic, runtime-updatable) is already wired into AppModule and proven in DkvSchedulerService. |
Reuse the same dynamic-cron pattern — one job per source (DÖE poll, AI-AG adapter poll, cosinex adapter poll, 2× RSS poll, inbox alert poll), each independently configurable in minutes via the admin UI, same as DKV's pollIntervalMin. |
| A new IMAP/EWS client | ImapProvider and ExchangeInboxProvider (both implementing InboxProvider) already handle IMAP (imapflow) and Exchange/EWS (raw SOAP over httpntlm, NTLM auth) inbox polling, including attachment size limits (PDF-bomb mitigation) and credential-safe logging. |
Reuse InboxProvider/ImapProvider/ExchangeInboxProvider directly for the Unterschwellen alert-email ingestion. Note: the current attachment filter is PDF-specific (collectPdfParts/looksLikePdf); the new module needs the email body/links, not attachments, for most portal alert mails (search-agent notifications are usually HTML emails linking to the tender, occasionally with a PDF). This means extending InboxProvider with a body-text/HTML-fetching capability, or adding a sibling interface — a phase-plan design decision, not a new dependency. |
Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|---|---|---|
fetch + cheerio (adapter-first) |
playwright (adapter-first) |
If the phase-1 spike shows a target portal's public search results are rendered/paginated via client-side JS (React/Angular) rather than server HTML — confirm per-portal before committing, don't assume both need it. |
fetch-cookie + tough-cookie |
got (built-in cookie support via got.extend({ cookieJar })) |
If the adapters later need retry/backoff/HTTP2 features beyond what a thin fetch wrapper offers — got bundles those, but it's another HTTP client family; only justified if manual retry logic (already needed per DKV's D-16 exponential-backoff pattern) becomes unwieldy. |
fast-xml-parser for both eForms-XML and RSS |
xml2js |
Only if a specific eForms XML document needs xml2js's SAX-based tolerance for malformed/legacy XML — unlikely for a DÖE-generated, schema-validated eForms-DE feed. |
| No dedicated OCDS validation library | ajv + the published OCDS JSON Schema |
If/when the module needs to strictly validate incoming OCDS JSON against the official schema (e.g. to catch DÖE-side data quality issues) rather than just mapping known fields defensively. Defer until proven necessary — DÖE's OCDS output is itself schema-validated upstream. |
Internal politeDelay() helper |
p-queue (dynamic import()) or bottleneck (unmaintained fork) |
Only if the module later needs true concurrent multi-portal fan-out with a shared global rate budget (e.g. dozens of AI-AG-family portals at once, per the feasibility doc's "deckt viele weitere AI-Portale" note) — at that scale a real queue library becomes worth the complexity. Not needed for the 2-adapter v1.1 scope. |
Version Compatibility
| Package A | Compatible With | Notes |
|---|---|---|
fast-xml-parser@5.x |
Node 18+ (project already on Node 22-class runtime per @types/node@^22) |
Pure TS/JS, no native bindings — no compatibility risk. |
cheerio@1.2.x |
Node ≥18.17 | Matches project's Node baseline. |
fetch-cookie@3.x |
Native global fetch (Node 18+) or any WHATWG-fetch-compatible function |
Wraps whatever fetch implementation is passed in — works with Node's built-in fetch (undici) with zero extra config. |
tough-cookie@6.x |
fetch-cookie@3.x |
fetch-cookie accepts any tough-cookie-compatible jar per its own docs; pin both current majors together. |
playwright@1.61.x (if added) |
Requires downloading Chromium binary at install time (playwright install chromium) |
Docker implication: the apps/api production image must run playwright install --with-deps chromium in its build stage — another reason to add it only if actually needed, not speculatively. |
csv-parse@7.x |
Node 18+, ESM and CJS builds published | Unlike p-queue/bottleneck, csv-parse ships dual CJS/ESM — safe for the CommonJS apps/api build. |
Sources (v1.1 delta)
- npm registry (
registry.npmjs.org) — direct authoritative version/publish-date lookup forcheerio,fast-xml-parser,csv-parse,rss-parser,playwright,p-queue,tough-cookie,fetch-cookie,bottleneck— fetched 2026-07-17. Confirms: cheerio 1.2.0 (2026-01-23), fast-xml-parser 5.10.1 (2026-07-16), csv-parse 7.0.1 (2026-07-02), rss-parser 3.13.0 (2023-04-11, stale), playwright 1.61.1 (2026-06-23), p-queue 9.3.1 (2026-07-03, ESM-only since v7), tough-cookie 6.0.2 (2026-07-07), fetch-cookie 3.2.0 (2025-12-15), bottleneck 2.19.5 (2019-08-03, unmaintained). - WebSearch (MEDIUM confidence, cross-checked against npm registry above where version-critical) — cheerio/fast-xml-parser/rss-parser/csv-parse/playwright/p-queue ecosystem status; undici-vs-node-fetch guidance; tough-cookie/fetch-cookie/axios-cookiejar-support session-handling patterns; Playwright-vs-cheerio scraping tradeoff for stateful ASP.NET-style vs static HTML portals; OCDS-for-eForms mapping (no dedicated npm library found — confirmed via
standard.open-contracting.orgdocumentation search, not a package). https://www.oeffentlichevergabe.de/documentation/swagger-ui/opendata/andbescha.bund.deDÖE pages — confirms eForms-DE/OCDS/CSV export formats, no-auth access..planning/research/ausschreibungs-portale-feasibility.md(this repo, 2026-07-16) — portal platform identification (AI AG NetServer = JavaControllerServlet, cosinex = separate incompatible HTML), which portals to scrape vs. email-alert-ingest vs. avoid entirely.- Codebase inspection (
apps/api/src/dkv/,apps/api/src/settings/,apps/api/tsconfig.json,apps/api/package.json) — confirmed: CommonJS build target (rules out ESM-only libs), nativefetchalready the established HTTP client (rules out addingaxios), existingInboxProvider/ImapProvider/ExchangeInboxProvider/DkvMailService/DkvSchedulerService/CalendarCryptoServicepatterns to reuse verbatim.
Gap / low-confidence area: No Context7 or other docs-MCP server was available in this session (.mcp.json only configures playwright for browser automation, not doc lookup) — all version numbers here come from WebSearch cross-checked directly against registry.npmjs.org, which is authoritative for version/publish-date facts but not for qualitative maintenance-health claims. This repo's automated confidence classifier flagged fast-xml-parser, csv-parse, playwright, p-queue, and tough-cookie as SUS — manual review indicates this is a false positive tripped by recent-publish velocity, not an actual supply-chain concern: all five are widely-adopted, well-known-maintainer packages (Playwright is Microsoft's own project). Recommend a final npm view <pkg> / Socket.dev spot-check immediately before pnpm add in the implementation phase, per this repo's existing dependency-hygiene bar.
v1.0 Base Stack (reference — unchanged)
Note added 2026-09-09: This is the stack recommendation from June/July 2026, preserved here unchanged. Parts of it were never adopted (Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged), and two items were adopted at an older major version than recommended (Next.js, Prisma). The actually installed stack, checked against
package.json/pnpm-lock.yaml, is documented inCLAUDE.mdunder "Technology Stack".
Researched: 2026-06-18 Overall Confidence: HIGH
Recommended Stack
Monorepo & Package Management
| Technology | Version | Purpose | Why |
|---|---|---|---|
| pnpm | 9.x | Package manager | 60-80% disk reduction via content-addressable store, strict dependency isolation prevents phantom deps, workspace protocol for internal packages |
| Turborepo | 2.9.x | Build orchestration | Incremental builds, parallel execution, task dependency graph, caching. Vercel-maintained — aligns with Next.js ecosystem |
Monorepo Structure:
tessera/
apps/
web/ # Next.js 16 frontend
api/ # NestJS backend
desktop/ # Tauri desktop wrapper
packages/
shared/ # Shared types, DTOs, constants
ui/ # Shared UI components (shadcn-based)
config/ # ESLint, TypeScript, Tailwind configs
db/ # Prisma schema & client
Why Turborepo over Nx: Turborepo is simpler, zero-config for most cases, and aligns with Vercel/Next.js tooling. Nx is more powerful for massive enterprise repos but adds unnecessary complexity for a team building with Claude.
Frontend
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Next.js | 16.2.x | Frontend framework | App Router with React 19, Turbopack default bundler (4x faster builds), stable Server Components, use cache directive, multi-tenant middleware support |
| React | 19.x | UI library | Ships with Next.js 16, React Compiler (automatic memoization), stable Server Actions |
| TypeScript | 5.5+ | Type safety | Required by all major tools, catches bugs at compile time |
| Tailwind CSS | 4.3.x | Styling | CSS-first config (no JS config file), 3.5x faster rebuilds, OKLCH colors, built-in dark mode via .dark selector |
| shadcn/ui | CLI v4 | Component library | Not a dependency — copies components into your project. Accessible, themeable via CSS variables, dark/light mode trivial with next-themes. Dashboard-ready components |
| next-themes | 0.4.x | Theme switching | 2-line dark mode integration with shadcn/ui, SSR-safe, system preference detection |
| next-intl | 4.13.x | Internationalization | Built for Next.js App Router, Server Component native, 457 bytes gzipped, simpler than i18next for Next.js-only projects |
| react-grid-layout | 2.2.x | Dashboard grid | TypeScript rewrite with hooks API (useGridLayout, useResponsiveLayout), drag & drop + resize, responsive breakpoints, 100% backward compat via /legacy |
| Zustand | 5.0.x | Client state | Lightweight (1.1kb), no providers needed, works with Server Components, perfect for UI state (sidebar toggle, theme, user prefs) |
| TanStack Query | 5.101.x | Server state | Caching, background refresh, optimistic updates, pagination. Use for client-side data fetching where Server Components don't suffice |
Backend
| Technology | Version | Purpose | Why |
|---|---|---|---|
| NestJS | 11.x | API framework | Modular architecture maps directly to Tessera's module system. Dependency injection, guards, interceptors, built-in microservices support. Modular monolith now, extract to microservices later if needed |
| Express | 5.x | HTTP server | Default in NestJS 11, battle-tested, massive middleware ecosystem |
| Prisma | 7.8.x | ORM | TypeScript-first, auto-generated types from schema, declarative migrations, Client Extensions for RLS multi-tenancy. v7 dropped Rust engine — 3x faster queries, 90% smaller bundles |
| PostgreSQL | 16.x | Database | Row-Level Security for multi-tenancy, JSONB for flexible module config, excellent Docker support, robust at any scale |
| Redis | 7.x | Cache & sessions | Session storage, pub/sub for real-time features, rate limiting, cache invalidation |
Authentication & Authorization
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Keycloak | 26.6.x | Identity provider | Native multi-tenancy via realms, LDAP/AD federation built-in, OAuth2/OIDC standard, Docker-native, admin UI included. Eliminates building auth from scratch |
| nest-keycloak-connect | latest | NestJS integration | Guards, decorators, multi-tenant realm resolvers — handles JWT validation and role extraction |
| @nestjs/passport | latest | Fallback auth | For API key auth on module-to-module communication |
Why Keycloak over custom auth: The project requires LDAP integration, multi-tenancy, admin user management, and token-based auth. Building this from scratch would take weeks and introduce security vulnerabilities. Keycloak provides all of this as a Docker container with zero custom code.
Note (v1.1): Live implementation diverged from this section — see
apps/api/src/settingsand the LDAP work already shipped directly againstldaptsrather than via Keycloak federation. This base-stack section is kept as originally researched; treat the "Authentication & Authorization" row above as historical context, not current fact, when planning new auth-adjacent work.
Desktop Wrapper
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Tauri | 2.x (2.11+) | Desktop app | 5MB installer vs Electron's 150MB. Uses OS-native WebView — no bundled Chromium. Rust backend for system APIs. Windows + Linux supported. Perfect for wrapping an existing web app |
Why Tauri over Electron: Tessera's desktop client is a thin wrapper around the web portal — it does not need Node.js APIs, multi-window workflows, or pixel-perfect cross-platform rendering. Tauri's 96% smaller bundle size, 30-50MB memory usage (vs 150-300MB), and native performance make it the clear choice for a web-app wrapper. The user is not a programmer, so less moving parts (no Node.js backend process) is better.
Trade-off acknowledged: Tauri uses the OS WebView (WebView2 on Windows, webkit2gtk on Linux), so minor rendering differences may exist. For an internal tool this is acceptable.
Infrastructure & DevOps
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Docker | 27.x | Containerization | Project constraint. Multi-stage builds for production images |
| Docker Compose | 2.x | Orchestration | Multi-service local dev and production deployment. Health checks, volume management, networking |
| Traefik | 3.x | Reverse proxy | Automatic SSL, Docker-native service discovery, routing rules via labels. Simpler than nginx for Docker-compose setups |
| Gitea | existing | Version control | Already in place. Automate via webhooks and Gitea API |
Note (v1.1): Reverse proxy in production is Nginx Proxy Manager (NPM), external to the Tessera Docker stack — not Traefik. Tessera containers do not include a reverse proxy; NPM handles SSL termination and routing on the host. Treat the "Traefik" row above as historical/superseded.
Testing
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Vitest | 3.x | Unit/integration tests | Fast, ESM-native, compatible with Jest API, works with both Next.js and NestJS |
| Playwright | 1.x | E2E tests | Cross-browser, auto-wait, trace viewer. Best for testing the full portal flow |
| Testing Library | latest | Component tests | DOM-based testing, framework-agnostic patterns |
Developer Experience
| Technology | Version | Purpose | Why |
|---|---|---|---|
| Biome | 2.x | Linting + formatting | Single tool replaces ESLint + Prettier, 100x faster, zero-config defaults. Reduces tooling complexity |
| Husky | 9.x | Git hooks | Pre-commit formatting/linting enforcement |
| lint-staged | 15.x | Staged file linting | Only lint changed files for fast commits |
Alternatives Considered
| Category | Recommended | Alternative | Why Not |
|---|---|---|---|
| Frontend Framework | Next.js 16 | Remix / SvelteKit | Next.js has best multi-tenant support, largest ecosystem, Vercel Platforms template as reference |
| Backend Framework | NestJS 11 | Fastify standalone / Express | NestJS module system maps perfectly to Tessera modules. DI, guards, and interceptors reduce boilerplate |
| ORM | Prisma 7 | Drizzle ORM | Prisma has gentler learning curve (built by Claude, non-programmer user), better migration safety. Drizzle is faster in serverless (irrelevant here — Docker deployment) |
| Database | PostgreSQL 16 | MariaDB | PostgreSQL has native RLS for multi-tenancy, superior JSONB support, better extension ecosystem |
| Auth | Keycloak | Custom JWT + LDAP lib | Building LDAP + multi-tenant auth from scratch is weeks of work and a security risk |
| Desktop | Tauri 2 | Electron | 96% smaller, 5x less memory. Tessera desktop is a wrapper, not a full desktop app |
| State | Zustand | Redux Toolkit | Zustand is 7x smaller bundle, no boilerplate, no providers. Redux is overkill for this project |
| i18n | next-intl | react-i18next | next-intl is built for App Router, Server Component native, simpler API |
| CSS | Tailwind 4 | CSS Modules / Styled Components | Utility-first is faster to develop, dark mode trivial, shadcn/ui requires it |
| Package Manager | pnpm | npm / yarn | pnpm has strict isolation, better disk usage, workspace support built-in |
| Monorepo Tool | Turborepo | Nx | Turborepo is simpler, aligns with Vercel ecosystem, sufficient for this project size |
| Linting | Biome | ESLint + Prettier | Single tool, 100x faster, less config. ESLint is being replaced in NestJS 12 roadmap anyway |
| Component Library | shadcn/ui | Material UI / Ant Design | shadcn gives ownership of components (no dep lock-in), built on Radix primitives, Tailwind-native |
| Dashboard Grid | react-grid-layout | Gridstack.js | React-native, TypeScript rewrite in v2, hooks API, responsive breakpoints |
| Reverse Proxy | Traefik | nginx | Docker-native service discovery, auto-SSL, config via labels not files — superseded in practice by external Nginx Proxy Manager, see note above |
Multi-Tenancy Strategy
Approach: Shared schema with Row-Level Security (RLS) in PostgreSQL.
Why RLS over schema-per-tenant:
- Scales to thousands of tenants without catalog bloat
- Single connection pool (no per-tenant connection overhead)
- Prisma Client Extensions support RLS via session variables
- Simpler migrations — one schema to manage
- Cost-effective for the planned growth trajectory
Implementation:
- Every tenant table has a
tenant_idcolumn - RLS policies enforce
WHERE tenant_id = current_setting('app.tenant_id') - NestJS middleware sets the tenant context on every request
- Prisma Client Extension wraps queries with
SET app.tenant_id - Keycloak realm maps to tenant, JWT contains tenant_id claim
Installation
# Initialize monorepo
pnpm create turbo@latest tessera --example basic
# Frontend (apps/web)
pnpm add next@16 react@19 react-dom@19
pnpm add next-intl@4 next-themes zustand@5 @tanstack/react-query@5
pnpm add react-grid-layout@2
pnpm add -D tailwindcss@4 @tailwindcss/postcss typescript @types/react
# Backend (apps/api)
pnpm add @nestjs/core@11 @nestjs/common@11 @nestjs/platform-express@11
pnpm add @nestjs/config @nestjs/jwt @nestjs/passport
pnpm add nest-keycloak-connect
pnpm add @prisma/client@7 ioredis
pnpm add -D prisma@7 @nestjs/cli@11
# Desktop (apps/desktop)
# Tauri CLI installed via cargo or npm
pnpm add -D @tauri-apps/cli@2
# Shared (packages/shared)
# Types, DTOs, and constants — no runtime deps
# Dev tools (root)
pnpm add -D turbo@2 @biomejs/biome@2 husky@9 lint-staged@15
pnpm add -D vitest@3 @playwright/test
Docker Services (docker-compose.yml)
services:
web: # Next.js frontend (port 3000)
api: # NestJS backend (port 3001)
db: # PostgreSQL 16 (port 5432)
redis: # Redis 7 (port 6379)
keycloak: # Keycloak 26.6.x (port 8080)
traefik: # Reverse proxy (port 80/443) — superseded by external NPM, see note above
Version Pinning Strategy
- Major versions: Pin to major (e.g.,
next@16,@nestjs/core@11) - Prisma: Pin exactly — schema changes require matched versions
- Tailwind/shadcn: Follow latest within major — utility additions are non-breaking
- Keycloak Docker image: Pin to minor (e.g.,
quay.io/keycloak/keycloak:26.6)
Sources (v1.0 base)
- Next.js 16 Docs — Version 16.2.7+ stable
- NestJS Documentation — Version 11.1.x
- Prisma ORM 7 Announcement — Pure TypeScript runtime
- Tauri 2.0 Stable Release — Cross-platform desktop
- Keycloak 26.6 Release — Latest stable
- Turborepo — Version 2.9.x
- shadcn/ui CLI v4 — March 2026 update
- Tailwind CSS v4.3 — Latest features
- next-intl — Version 4.13.x
- react-grid-layout — Version 2.2.x TypeScript rewrite
- PostgreSQL RLS Multi-Tenancy
- pnpm Workspaces — Workspace management