Files
tessera-ctl/.planning/research/STACK.md
T
schalli d2ac1997d6 docs: complete project research
Ausschreibungs-Radar (v1.1) STACK/FEATURES/ARCHITECTURE/PITFALLS research plus SUMMARY.md synthesis.
2026-07-17 10:12:59 +02:00

303 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
# 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 for `cheerio`, `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.org` documentation search, not a package).
- `https://www.oeffentlichevergabe.de/documentation/swagger-ui/opendata/` and `bescha.bund.de` DÖ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 = Java `ControllerServlet`, 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), native `fetch` already the established HTTP client (rules out adding `axios`), existing `InboxProvider`/`ImapProvider`/`ExchangeInboxProvider`/`DkvMailService`/`DkvSchedulerService`/`CalendarCryptoService` patterns 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)
**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/settings` and the LDAP work already shipped directly against `ldapts` rather 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:**
1. Every tenant table has a `tenant_id` column
2. RLS policies enforce `WHERE tenant_id = current_setting('app.tenant_id')`
3. NestJS middleware sets the tenant context on every request
4. Prisma Client Extension wraps queries with `SET app.tenant_id`
5. Keycloak realm maps to tenant, JWT contains tenant_id claim
## Installation
```bash
# 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)
```yaml
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](https://nextjs.org/docs/app/guides/upgrading/version-16) — Version 16.2.7+ stable
- [NestJS Documentation](https://docs.nestjs.com/) — Version 11.1.x
- [Prisma ORM 7 Announcement](https://www.prisma.io/blog/announcing-prisma-orm-7-0-0) — Pure TypeScript runtime
- [Tauri 2.0 Stable Release](https://v2.tauri.app/blog/tauri-20/) — Cross-platform desktop
- [Keycloak 26.6 Release](https://www.keycloak.org/2026/04/keycloak-2660-released) — Latest stable
- [Turborepo](https://turborepo.dev/) — Version 2.9.x
- [shadcn/ui CLI v4](https://ui.shadcn.com/docs/changelog/2026-03-cli-v4) — March 2026 update
- [Tailwind CSS v4.3](https://tailwindcss.com/blog/tailwindcss-v4-3) — Latest features
- [next-intl](https://next-intl.dev/) — Version 4.13.x
- [react-grid-layout](https://github.com/react-grid-layout/react-grid-layout) — Version 2.2.x TypeScript rewrite
- [PostgreSQL RLS Multi-Tenancy](https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/)
- [pnpm Workspaces](https://pnpm.io/workspaces) — Workspace management