docs: complete project research for Tessera portal platform

Synthesize stack, features, architecture, and pitfalls research into
unified summary with roadmap implications and phase suggestions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 08:28:58 +02:00
parent 77ca2421ca
commit 16c2d5b6af
5 changed files with 1082 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
# Architecture Patterns
**Domain:** Modular portal platform with marketplace, multi-tenancy, configurable dashboard, and desktop wrapper
**Researched:** 2026-06-18
## Recommended Architecture
Tessera follows a **modular monolith** pattern: a single deployable backend that internally separates concerns into distinct modules, fronted by a micro-frontend-capable shell. This avoids premature microservice complexity while maintaining clean module boundaries for future extraction.
```
+-------------------------------------------------------------------+
| Desktop Wrapper (Tauri) |
+-------------------------------------------------------------------+
| Frontend Shell (React SPA) |
| +------------+ +------------+ +------------+ +-------------+ |
| | Sidebar | | Dashboard | | Marketplace| | Module UI | |
| | Navigation | | (Widgets) | | Browser | | (lazy-load) | |
| +------------+ +------------+ +------------+ +-------------+ |
+-------------------------------------------------------------------+
| API Gateway (Traefik / Nginx) |
+-------------------------------------------------------------------+
| Backend (Node.js / Fastify) |
| +--------+ +--------+ +--------+ +---------+ +----------+ |
| | Auth | | Tenant | | Module | | Market- | | Dashboard| |
| | Module | | Module | | Loader | | place | | Service | |
| +--------+ +--------+ +--------+ +---------+ +----------+ |
+-------------------------------------------------------------------+
| PostgreSQL (shared, RLS) |
+-------------------------------------------------------------------+
| Docker Compose (orchestration) |
+-------------------------------------------------------------------+
```
### Component Boundaries
| Component | Responsibility | Communicates With |
|-----------|---------------|-------------------|
| **Frontend Shell** | Application frame (header, sidebar, routing), theme, i18n | Backend API via REST/WebSocket |
| **Dashboard Engine** | Widget grid, drag-and-drop, layout persistence | Backend Dashboard Service for saving layouts |
| **Marketplace UI** | Browse modules, view details, request activation | Backend Marketplace Service |
| **Module UI Slots** | Lazy-loaded UI for activated modules | Module-specific backend endpoints |
| **API Gateway** | Reverse proxy, rate limiting, tenant header injection | All backend services |
| **Auth Module** | Login, session/JWT, LDAP integration, user management | PostgreSQL, LDAP server |
| **Tenant Module** | Tenant CRUD, tenant context resolution, tenant-specific config | PostgreSQL, injected into every request |
| **Module Loader** | Plugin lifecycle (discover, validate, activate, deactivate) | Filesystem/registry, PostgreSQL |
| **Marketplace Service** | Module catalog, licensing, activation per tenant | PostgreSQL, Module Loader |
| **Dashboard Service** | Widget registry, layout CRUD per user per tenant | PostgreSQL |
| **PostgreSQL** | Persistent storage, row-level security for tenant isolation | All backend modules |
| **Desktop Wrapper (Tauri)** | Native window, system tray, local shortcuts | Frontend Shell (wraps the web app) |
### Data Flow
**Request flow (authenticated):**
```
User Action
-> Desktop Wrapper / Browser
-> Frontend Shell (React Router)
-> HTTP Request with JWT + Tenant-ID header
-> API Gateway (validates JWT, injects tenant context)
-> Backend Route Handler
-> Service Layer (business logic)
-> PostgreSQL (RLS enforces tenant isolation)
<- Response
<- JSON Response
<- Frontend renders
```
**Module activation flow:**
```
Admin browses Marketplace
-> Selects module, clicks "Activate"
-> POST /api/marketplace/modules/:id/activate
-> Marketplace Service checks license entitlement
-> Module Loader registers module for tenant
-> INSERT module_activations (tenant_id, module_id, status)
-> Frontend sidebar updates (module appears)
<- Success response
```
**Dashboard widget flow:**
```
User opens Dashboard
-> GET /api/dashboard/layout
-> Returns user's widget layout (positions, sizes)
-> Frontend renders react-grid-layout with widget components
-> User drags/resizes widget
-> PUT /api/dashboard/layout (debounced save)
-> Persists to PostgreSQL
```
## Core Architecture Decisions
### 1. Modular Monolith over Microservices
**Why:** Tessera is built by a single developer (with Claude). Microservices add deployment, debugging, and network complexity that provides zero benefit at this scale. A modular monolith gives clean separation with a single deployment unit.
**Structure:** Each domain (auth, tenant, marketplace, dashboard, modules) lives in its own directory with its own routes, services, and repository files. They communicate through in-process function calls, not HTTP.
**Future path:** If a module becomes a bottleneck, extract it to a separate service behind the API gateway. The clean boundaries make this straightforward.
### 2. Shared Database with Row-Level Security (RLS)
**Why:** Separate databases per tenant adds massive operational overhead. PostgreSQL RLS enforces tenant isolation at the database level, meaning even application bugs cannot leak data across tenants.
**Implementation:**
```sql
-- Every tenant-scoped table has a tenant_id column
ALTER TABLE modules ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON modules
USING (tenant_id = current_setting('app.current_tenant')::uuid);
```
The backend sets `app.current_tenant` on each database connection based on the authenticated user's tenant. RLS handles the rest transparently.
### 3. Plugin/Module System as Data-Driven Registry
**Why:** Modules should not require restarting the server to be discovered. A database-driven registry with filesystem-based module code gives hot-activation without runtime code loading risks.
**How it works:**
- Module metadata (name, version, category, routes, permissions) stored in `modules` table
- Module code lives in `src/modules/<module-name>/` with a standard interface
- Activation is per-tenant: `module_activations` table links tenant to module
- Frontend lazy-loads module UI bundles only when the module is active for the current tenant
- Backend routes for a module are only registered/accessible when the module is active
### 4. Frontend Shell with Lazy Module Loading
**Why:** Loading all module UIs upfront wastes bandwidth and exposes code for modules the tenant has not licensed. Lazy loading (React.lazy + dynamic import) loads module UIs on demand.
**Pattern:**
```typescript
// Module registry maps module_id -> lazy component
const moduleRegistry: Record<string, () => Promise<{ default: ComponentType }>> = {
'domaincheck': () => import('./modules/domaincheck/DomaincheckPage'),
'email-tools': () => import('./modules/email-tools/EmailToolsPage'),
};
```
The shell only renders module routes that are in the tenant's active module list (fetched from the backend on login).
### 5. Tauri over Electron for Desktop Wrapper
**Why:** Tauri produces ~10MB binaries vs Electron's 100MB+. RAM usage is 20-40MB vs 200-400MB. Tauri uses the system WebView (no bundled Chromium), has a Rust backend for native features, and has a smaller attack surface. The non-programmer maintainer benefits from the simpler, lighter deployment.
**Architecture:** The Tauri wrapper is a thin shell. It loads the same web application served locally or from the server. Native features (system tray, auto-update, window management) are exposed through Tauri commands.
## Patterns to Follow
### Pattern 1: Tenant Context Middleware
**What:** A middleware that extracts tenant identity from the JWT/session, sets it on the request context, and configures the database connection with RLS.
**When:** Every authenticated request.
```typescript
// middleware/tenantContext.ts
async function tenantContext(req: FastifyRequest, reply: FastifyReply) {
const tenantId = req.user.tenantId;
if (!tenantId) return reply.code(403).send({ error: 'No tenant context' });
// Set RLS context on the database connection
await req.db.query(`SET app.current_tenant = '${tenantId}'`);
req.tenantId = tenantId;
}
```
### Pattern 2: Module Interface Contract
**What:** Every module (backend) exports a standard interface so the platform can discover, mount, and manage it uniformly.
**When:** Building any new module.
```typescript
// modules/<name>/index.ts
export interface TesseraModule {
id: string;
version: string;
category: string;
routes: (app: FastifyInstance) => void;
widgets?: WidgetDefinition[]; // Optional dashboard widgets
permissions?: string[]; // Required permissions
onActivate?: (tenantId: string) => Promise<void>;
onDeactivate?: (tenantId: string) => Promise<void>;
}
```
### Pattern 3: Widget Component Contract
**What:** Dashboard widgets follow a standard interface for the grid layout system.
**When:** Building any dashboard widget (core or module-provided).
```typescript
// widgets/types.ts
export interface WidgetDefinition {
id: string;
name: string; // i18n key
defaultSize: { w: number; h: number };
minSize?: { w: number; h: number };
component: () => Promise<{ default: ComponentType<WidgetProps> }>;
configSchema?: JSONSchema; // Optional widget settings
}
export interface WidgetProps {
config: Record<string, unknown>;
tenantId: string;
userId: string;
}
```
### Pattern 4: Feature Flag via Module Activation
**What:** Instead of traditional feature flags, Tessera uses module activation as the feature gating mechanism. If a module is not activated for a tenant, its routes return 403, its UI does not load, and its sidebar entry is hidden.
**When:** Controlling feature access per tenant.
## Anti-Patterns to Avoid
### Anti-Pattern 1: Module-to-Module Direct Dependencies
**What:** Module A directly imports and calls Module B's internal functions.
**Why bad:** Creates coupling that makes modules impossible to activate independently. If Module B is deactivated, Module A breaks.
**Instead:** Use an event bus or shared service layer. Modules publish events; other modules subscribe. If the publisher is missing, subscribers simply receive no events.
### Anti-Pattern 2: Tenant ID in Application Logic
**What:** Manually filtering by `tenant_id` in every query throughout the codebase.
**Why bad:** A single missed filter leaks data across tenants. Hundreds of places to maintain.
**Instead:** Use PostgreSQL RLS. Set tenant context once per request at the middleware level. All queries are automatically filtered. Defense in depth: the application layer still passes tenant_id, but RLS is the safety net.
### Anti-Pattern 3: Monolithic Frontend Bundle
**What:** Bundling all module UIs into a single JavaScript bundle.
**Why bad:** Users download code for modules they cannot access. Bundle size grows linearly with module count. Exposes unlicensed module code.
**Instead:** Code-split per module. Use dynamic imports. Only load module bundles when the user navigates to an active module.
### Anti-Pattern 4: Per-Tenant Database/Schema
**What:** Creating a separate PostgreSQL database or schema for each tenant.
**Why bad:** Operational nightmare at scale — migrations must run N times, connection pooling is per-tenant, monitoring multiplies. Overkill for a portal where tenants share the same data model.
**Instead:** Shared schema with RLS. Single migration path. Single connection pool. Isolation enforced at row level.
## Scalability Considerations
| Concern | At 10 users (internal) | At 100 tenants | At 1000+ tenants |
|---------|------------------------|----------------|------------------|
| **Database** | Single PostgreSQL, no pooler needed | PgBouncer for connection pooling | Read replicas, consider partitioning large tables by tenant_id |
| **Backend** | Single container | Horizontal scaling behind gateway (2-4 replicas) | Auto-scaling, consider extracting hot modules to separate services |
| **Frontend** | Single static bundle | CDN for static assets | CDN + edge caching, consider module federation for team-developed modules |
| **Module isolation** | Shared process, trust all modules | Same, but add resource limits per module route | Consider containerized module backends for untrusted/third-party modules |
| **File storage** | Local volume | S3-compatible object storage | Same + lifecycle policies |
## Suggested Build Order
Based on component dependencies, the recommended build order is:
```
Phase 1: Foundation
├── Docker Compose setup (PostgreSQL + backend + frontend containers)
├── PostgreSQL schema with tenant_id columns + RLS policies
├── Backend skeleton (Fastify + request lifecycle)
└── Frontend shell (React + routing + layout frame)
Phase 2: Authentication & Tenancy
├── Auth module (login, JWT, session management)
├── Tenant middleware (context injection, RLS activation)
├── User management (CRUD, roles)
└── LDAP integration
Phase 3: Module System
├── Module interface contract
├── Module registry (database-driven)
├── Module loader (route mounting, activation/deactivation)
└── First example module (Domaincheck)
Phase 4: Marketplace & Dashboard
├── Marketplace service (catalog, categories, licensing)
├── Marketplace UI (browse, activate, manage)
├── Dashboard service (layout persistence)
└── Dashboard UI (react-grid-layout, core widgets)
Phase 5: Polish & Desktop
├── i18n (DE + EN)
├── Light/Dark theme
├── Tauri desktop wrapper
└── Gitea CI/CD integration
```
**Dependency rationale:**
- Phase 1 first because everything depends on the database, backend framework, and frontend shell
- Phase 2 before modules because module activation requires knowing WHO is asking and WHICH tenant they belong to
- Phase 3 before marketplace because the marketplace manages modules — the module system must exist first
- Phase 4 can partially parallelize (dashboard is independent of marketplace) but both need the module system
- Phase 5 is pure enhancement — i18n/theme are cross-cutting but easier to retrofit than to block on
## Sources
- [Multi-Tenant Databases with Postgres Row-Level Security](https://www.midnytecity.com.au/blogs/multi-tenant-databases-with-postgres-row-level-security)
- [AWS: Multi-tenant data isolation with PostgreSQL Row Level Security](https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/)
- [Approaches to implementing multi-tenancy in SaaS applications - Red Hat](https://developers.redhat.com/articles/2022/05/09/approaches-implementing-multi-tenancy-saas-applications)
- [Node.js Plugin Architecture: Build Your Own Plugin System](https://medium.com/codeelevation/node-js-plugin-architecture-build-your-own-plugin-system-with-es-modules-5b9a5df19884)
- [How to Build Plugin Architecture in Node.js](https://oneuptime.com/blog/post/2026-01-26-nodejs-plugin-architecture/view)
- [Building Customizable Dashboard Widgets Using React Grid Layout](https://www.antstack.com/blog/building-customizable-dashboard-widgets-using-react-grid-layout/)
- [react-grid-layout - GitHub](https://github.com/react-grid-layout/react-grid-layout)
- [Micro-Frontend Architecture with Module Federation](https://module-federation.io/)
- [Tauri vs Electron: The Complete Developer's Guide (2026)](https://blog.nishikanta.in/tauri-vs-electron-the-complete-developers-guide-2026)
- [Tauri in 2026: Build Cross-Platform Desktop Apps](https://dev.to/ottoaria/tauri-in-2026-build-cross-platform-desktop-apps-with-web-technologies-better-than-electron-11mo)
- [Using a Reverse Proxy to Expose Multiple Microservices Through a Single Port in Docker Compose](https://dev.to/syed_omair/using-a-reverse-proxy-to-expose-multiple-microservices-through-a-single-port-in-docker-compose-4h9e)
- [Developing a Multi-Tenant SaaS Application: The 2026 Architecture Guide](https://apipilot.com/developing-a-multi-tenant-saas-application-the-2026-architecture-guide/)
+112
View File
@@ -0,0 +1,112 @@
# Feature Landscape
**Domain:** Modular portal platform with marketplace, multi-tenancy, and workflow tool integration
**Researched:** 2026-06-18
## Table Stakes
Features users expect. Missing = product feels incomplete or unprofessional.
| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| User authentication (email/password + admin-created accounts) | Every portal needs login; without it nothing works | Medium | Foundation for all access control |
| Role-Based Access Control (RBAC) | Users expect permission boundaries; admins expect control | Medium | Tenant-scoped roles are critical for multi-tenancy |
| Multi-tenancy with data isolation | Core requirement per PROJECT.md; customers expect their data is isolated | High | Row-level security (tenant_id on every table) or schema-per-tenant |
| Sidebar navigation with categories | Standard portal pattern; users expect hierarchical navigation | Low | Collapsible, categorized by module type |
| Module activation/deactivation per tenant | Marketplace without on/off is just a list; tenants expect control | Medium | Admin toggles module visibility and access |
| Module licensing (admin-managed) | Core business model; tenants expect clear "you have access to X" | Medium | License = permission to activate; no payment integration yet |
| Responsive layout | Web apps must work on different screen sizes | Medium | Desktop-first, but must not break on tablet |
| Light/Dark theme | Expected in modern apps; users notice its absence | Low | CSS custom properties + toggle; store preference per user |
| Internationalization (i18n) - DE + EN | Core requirement; German users expect German UI | Medium | Must be baked in from day one; retrofitting i18n is painful |
| Basic dashboard with widgets | Central landing area gives users a "home" | Medium | Clock, search, notes, calendar as starting widgets |
| Session management | Users expect to stay logged in, and to be logged out on timeout | Low | Token-based with configurable expiry |
| Error handling and user feedback | Users expect clear feedback on actions (success/error toasts) | Low | Global notification/toast system |
| Loading states and skeleton screens | Without them, users think the app is broken | Low | Standard UX pattern |
| Search / filter within module lists | Even with 10 modules, users expect to type-to-find | Low | Client-side filter on marketplace and sidebar |
| User profile and settings | Users expect to change language, theme, password | Low | Per-user preferences storage |
## Differentiators
Features that set Tessera apart from generic portals. Not expected, but valued.
| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| Drag-and-drop dashboard with resizable widgets | Personal workspace feel; makes dashboard actually useful vs static page | High | Use react-grid-layout or gridstack.js; persist layout per user per tenant |
| Widget marketplace/gallery | Users can add widgets to their dashboard from a catalog | Medium | Distinct from module marketplace; lightweight UI components |
| Module hot-activation without restart | Modules appear instantly after license grant; no deploy needed | High | Requires dynamic route loading or micro-frontend approach |
| LDAP/AD directory sync | Enterprise differentiator; auto-provision users from corporate directory | High | JIT provisioning at login + periodic group sync |
| Per-tenant branding/customization | Each tenant gets their own logo, color accent | Medium | Stored in tenant config; CSS variable override |
| Module-provided dashboard widgets | Modules can contribute widgets to the dashboard system | Medium | Module manifest declares available widgets; loose coupling |
| Activity feed / audit log | Transparency on who did what; compliance-ready | Medium | Event sourcing pattern; filterable by user, action, module |
| Admin impersonation ("view as user") | Support tool; admin can see exactly what a user sees | Medium | Scoped session with clear visual indicator |
| Keyboard shortcuts and command palette | Power-user acceleration; "Ctrl+K" to jump anywhere | Low | Global listener + fuzzy search over routes and actions |
| Onboarding wizard for new tenants | Guides new tenant admins through setup; reduces support burden | Medium | Multi-step flow: branding, users, module selection |
| Module dependency declaration | Module A requires Module B; platform enforces this | Low | Manifest-level declaration; block activation if dependency missing |
| Notification center | Unified inbox for system events, module alerts, admin messages | Medium | WebSocket or SSE for real-time; persisted read/unread state |
| Desktop wrapper (Electron/Tauri) | Installable app feel; taskbar presence, native notifications | Medium | Tauri preferred (smaller binary, Rust-backed) |
## Anti-Features
Features to explicitly NOT build. These add complexity without proportional value for Tessera's use case.
| Anti-Feature | Why Avoid | What to Do Instead |
|--------------|-----------|-------------------|
| Third-party module SDK / developer portal | Massive complexity (sandboxing, review pipeline, versioning); only own modules planned | Build a clean internal module contract; open up later if demand arises |
| Payment/billing integration | Out of scope per PROJECT.md; adds regulatory and UX burden | Admin-managed license grants; add Stripe/payment only when selling externally |
| Real-time collaboration (multiplayer editing) | Workflow tools are typically single-user operations; CRDT/OT is enormously complex | Each module handles its own data; no shared editing state |
| AI/ML-powered recommendations | "Suggested modules" adds little value with a small catalog; ML overhead is huge | Manual curation and categories; maybe simple "popular modules" counter later |
| Native mobile app | Web-first + desktop wrapper covers the use case; mobile adds two platforms to maintain | Responsive web design; PWA if truly needed later |
| Complex workflow orchestration engine (BPMN) | Tessera modules ARE the tools; building a meta-workflow layer is a product in itself | Each module handles its own workflow; cross-module orchestration is future scope |
| White-label/full-rebrand per tenant | Different from "per-tenant branding"; full white-label means separate builds, domains, assets | Offer logo + accent color customization; not full theme overhaul per tenant |
| Plugin sandboxing (iframe/WASM isolation) | Only own modules are deployed; sandboxing is for untrusted third-party code | Modules are trusted first-party Docker containers; share the same runtime |
| Social features (comments, reactions, @mentions) | Not a collaboration tool; adds social complexity without clear workflow value | Keep modules focused on their task; add module-specific notes if needed |
| Granular per-field permissions | RBAC at role/module level is sufficient; field-level ACL is enterprise overkill | Role -> Module access mapping; maybe per-module "read/write/admin" tiers |
## Feature Dependencies
```
Authentication -> RBAC -> Multi-Tenancy (each layer builds on the previous)
Multi-Tenancy -> Module Licensing (licenses are tenant-scoped)
Module Licensing -> Module Activation (can't activate without license)
Module Activation -> Marketplace UI (marketplace displays activation state)
Dashboard Framework -> Widget System -> Drag-and-Drop Layout
Dashboard Framework -> Module-Provided Widgets (modules contribute to dashboard)
i18n Framework -> All UI Components (must be in place before building UI)
Theme System -> All UI Components (CSS variables must exist before components)
LDAP Integration -> User Management (extends, does not replace manual management)
Notification Center -> Module Events (modules emit events to notification system)
Sidebar Navigation -> Module Registry (sidebar reflects activated modules)
```
## MVP Recommendation
Prioritize in this order:
1. **Authentication + RBAC + Multi-Tenancy** - Foundation; nothing works without it
2. **Sidebar navigation + Module registry** - Portal shell; gives the app structure
3. **i18n framework (DE + EN)** - Must be first, before building UI text
4. **Theme system (light/dark)** - Must be first, before building styled components
5. **Marketplace UI with licensing/activation** - Core business logic
6. **Basic dashboard with static widgets** - User home; clock, search, notes
7. **Domaincheck module** - First real module; validates the entire module architecture
8. **Drag-and-drop dashboard** - Differentiator; upgrade from static layout
Defer:
- **LDAP integration**: High complexity, not needed for initial internal use (manual user creation suffices)
- **Desktop wrapper**: Adds build pipeline complexity; browser works fine initially
- **Notification center**: Useful but not critical until multiple modules exist
- **Admin impersonation**: Support tool; not needed until external customers arrive
- **Onboarding wizard**: Only valuable with external tenants; internal users get manual setup
## Sources
- [WorkOS: Multi-tenant RBAC design](https://workos.com/blog/how-to-design-multi-tenant-rbac-saas)
- [Logto: Build a multi-tenant SaaS application](https://logto.medium.com/build-a-multi-tenant-saas-application-a-complete-guide-from-design-to-implementation-d109d041f253)
- [Cloudscape Design System: Configurable Dashboard](https://cloudscape.design/patterns/general/service-dashboard/configurable-dashboard/)
- [FreeCodeCamp: Type-safe plugin architecture in React](https://www.freecodecamp.org/news/how-to-design-a-type-safe-lazy-and-secure-plugin-architecture-in-react/)
- [Backstage.io: Plugin-based developer portal](https://backstage.io/)
- [AppMaster: Audit logging for internal tools](https://appmaster.io/blog/audit-logging-internal-tools-activity-feed)
- [Frontegg: SaaS Multitenancy components](https://frontegg.com/blog/saas-multitenancy)
- [Medium: Node.js Plugin Architecture with ES Modules](https://medium.com/codeelevation/node-js-plugin-architecture-build-your-own-plugin-system-with-es-modules-5b9a5df19884)
- [DevelopersVoice: Plugin-ready modular monolith](https://developersvoice.com/blog/dotnet/building_plugin_ready_modular_monolith/)
- [Gridstack.js: Interactive dashboards](https://gridstackjs.com/)
+282
View File
@@ -0,0 +1,282 @@
# Domain Pitfalls
**Domain:** Modular portal platform with marketplace, multi-tenancy, Docker deployment
**Project:** Tessera
**Researched:** 2026-06-18
## Critical Pitfalls
Mistakes that cause rewrites, data breaches, or architectural dead-ends.
### Pitfall 1: Tenant Data Leaks via Missing tenant_id Filtering
**What goes wrong:** In a shared-database multi-tenant model, every database query must be scoped to the current tenant. A single missed WHERE clause exposes one tenant's data to another. This is not theoretical — it has caused real security incidents in production SaaS platforms.
**Why it happens:** Developers rely on remembering to add tenant filtering per query rather than enforcing it structurally. As the codebase grows, a new endpoint or background job skips the filter, and cross-tenant data leaks silently.
**Consequences:** Complete breach of tenant isolation. Legal liability. Loss of customer trust. Forces emergency patching and potential data breach notifications.
**Prevention:**
- Use PostgreSQL Row-Level Security (RLS) as a database-level safety net. Application bugs cannot bypass RLS policies.
- Set `current_setting('app.current_tenant')` on every database session/connection via middleware, then let RLS policies enforce filtering automatically.
- Create a base repository/query class that injects tenant scope — never let raw queries bypass it.
- Integration tests must explicitly verify that Tenant A cannot access Tenant B's data at the database layer.
**Detection:** Code review flag: any raw SQL or ORM query on a tenant-scoped table that does not include tenant filtering. Automated test suite with cross-tenant access attempts.
**Phase relevance:** Must be addressed in Phase 1 (core architecture). Retrofitting RLS after data exists is painful.
---
### Pitfall 2: Module API Contract Instability (Breaking Changes)
**What goes wrong:** The platform core exposes APIs to modules. As the platform evolves, these APIs change, breaking existing modules. Without versioning, every core update risks breaking the marketplace.
**Why it happens:** The module API is treated as internal code rather than a public contract. Tight coupling between core and modules means core refactoring cascades into module breakage.
**Consequences:** Module developers (even if initially just Claude) cannot trust the platform. Upgrading the core requires simultaneously updating all modules. At scale, this becomes a coordination nightmare that blocks all releases.
**Prevention:**
- Define a versioned Module SDK/API from day one (e.g., `@tessera/sdk@1.x`).
- Use semantic versioning: breaking changes require a new major version with a migration path.
- Modules declare which SDK version they target. Platform maintains backward compatibility for at least one prior major version.
- Keep the API surface small — expose only what modules actually need.
**Detection:** Any PR that modifies the module-facing interface without a version bump. Module integration tests failing after core changes.
**Phase relevance:** Phase 2 (module system design). The API contract must be defined before the first marketplace module ships.
---
### Pitfall 3: Monolithic Module Loading Destroys Isolation
**What goes wrong:** Modules run in the same process as the platform core, sharing memory, dependencies, and crash domains. A buggy module takes down the entire platform. A module with a conflicting dependency version breaks other modules.
**Why it happens:** In-process loading is simpler to implement initially. Teams plan to "isolate later" but the architecture cements coupling.
**Consequences:** One module crash = entire platform down for all tenants. Dependency conflicts between modules. Security: a malicious module can access other modules' data in-process.
**Prevention:**
- Each module runs as its own Docker container (or at minimum, its own isolated process).
- Communication between core and modules via well-defined API (REST/gRPC), not shared memory.
- Module containers have their own dependency trees — no shared node_modules or library conflicts.
- Resource limits (CPU/memory) per module container prevent a runaway module from starving the platform.
**Detection:** Architecture review: can you stop/restart one module without affecting others? If not, isolation is insufficient.
**Phase relevance:** Phase 1 (architecture decisions). This is a foundational constraint that shapes everything.
---
### Pitfall 4: Docker Network Flat Topology Enables Lateral Movement
**What goes wrong:** All containers (frontend, backend, database, modules, Redis) placed on a single Docker network. A compromised module container can directly access the database, other modules, and internal services.
**Why it happens:** Single-network setups are simpler during development. Teams use docker-compose with one default network and never segment.
**Consequences:** A vulnerability in any module gives attackers access to the database, cache, and all other modules. The blast radius of any compromise is the entire system.
**Prevention:**
- Segment Docker networks by trust boundary:
- `frontend-net`: reverse proxy + frontend containers
- `backend-net`: API server + module containers
- `data-net`: database + cache (only accessible from API server)
- Module containers CANNOT reach the database directly — they communicate only with the platform API.
- Never publish internal ports (PostgreSQL 5432, Redis 6379) to the host unless explicitly needed for development.
- Use Docker Compose network aliases to control service discovery.
**Detection:** Run `docker network inspect` — if all containers appear on one network, isolation is broken. Penetration test: from a module container, can you reach the database port?
**Phase relevance:** Phase 1 (infrastructure setup). Docker Compose network topology must be designed before anything else runs.
---
### Pitfall 5: LDAP as Primary Authentication Instead of Identity Layer
**What goes wrong:** The application directly binds to LDAP with user credentials for authentication (the "simple bind" pattern). Users' passwords pass through the application, which must handle them in cleartext. The application becomes a credential-harvesting target.
**Why it happens:** LDAP bind is the most "obvious" integration — send username/password to LDAP, check if bind succeeds. It works, so teams ship it without considering the security implications.
**Consequences:** If the application server is compromised, all credentials used during the compromise window are exposed. Cannot add MFA later (LDAP bind is password-only). Cannot support SSO without rearchitecting.
**Prevention:**
- Use LDAP for user directory sync (group membership, attributes) but NOT as the authentication mechanism.
- Implement authentication via a proper identity layer: local password hashing (bcrypt/argon2) for the initial admin flow, LDAP sync for user provisioning, and optionally OIDC/SAML for enterprise SSO later.
- If LDAP bind is required for compatibility: always use LDAPS (port 636), validate certificates, use a dedicated service account for directory lookups, and rate-limit bind attempts.
- Architecture should allow swapping the auth backend without rewriting the application.
**Detection:** Code that passes raw user passwords to an LDAP bind operation without TLS. No abstraction layer between "authenticate user" and "LDAP bind."
**Phase relevance:** Phase 2 (authentication system). Design the auth abstraction layer first, then implement LDAP as one provider behind it.
---
## Moderate Pitfalls
### Pitfall 6: i18n Retrofitting After UI Is Built
**What goes wrong:** UI components use hardcoded strings. When i18n is added later, every component must be touched. German text is 30-40% longer than English, breaking layouts that were designed for English string lengths.
**Why it happens:** "We'll add translations later" feels reasonable because it seems like a search-and-replace task. In reality, it requires layout redesign, context-aware pluralization, date/number formatting changes, and string extraction tooling.
**Prevention:**
- Use i18n from the very first component. Every user-visible string goes through a translation function (`t('key')`).
- Design layouts with flexible widths — avoid fixed-width containers for text.
- Use ICU MessageFormat for pluralization rules (German has different plural forms than English).
- Store translations in separate JSON files per locale, never inline.
- Test the UI with the longest locale (German) as the default during development to catch overflow early.
**Detection:** Any user-visible string literal in component code that is not wrapped in a translation call. Layout breakage when switching to German.
**Phase relevance:** Phase 1 (frontend scaffolding). i18n infrastructure must be present from the first rendered component.
---
### Pitfall 7: Dashboard Widget State Explosion
**What goes wrong:** The drag-and-drop dashboard stores layout state per user per tenant, with frequent updates on every drag/resize event. This generates massive write amplification to the database and state management complexity.
**Why it happens:** Naive implementations persist layout on every mouse event. Combined with multi-tenancy, the permutations of (user x tenant x widget configuration) grow rapidly.
**Prevention:**
- Debounce layout persistence — save only after drag/resize completes (on mouse-up), not during.
- Store layout as a single JSON column per user-dashboard, not as individual widget position rows.
- Use optimistic UI updates with background sync — don't block on database writes.
- Limit the maximum number of widgets per dashboard (e.g., 20) to bound rendering complexity.
- Use a proven grid library (react-grid-layout or gridstack.js) rather than building from scratch.
**Detection:** Database write frequency during dashboard interaction. UI jank when >10 widgets are rendered simultaneously.
**Phase relevance:** Phase 3 (dashboard implementation). Choose the grid library and persistence strategy before building widgets.
---
### Pitfall 8: Licensing State Scattered Across the System
**What goes wrong:** Module activation/licensing checks are scattered throughout the codebase — in route guards, API middleware, UI rendering logic, and module loaders. When the licensing model changes, dozens of locations need updating.
**Why it happens:** Each feature gate feels like a simple `if (licensed)` check. Over time, these proliferate into an unmaintainable web.
**Prevention:**
- Centralize licensing in a single service/module: `LicenseService.canAccess(tenant, module) -> boolean`.
- All other code calls this service — no direct database checks for license status.
- Cache license state per tenant (licenses change rarely — invalidate on admin action only).
- Module containers should not even start/be routable if the tenant lacks a license — enforce at the orchestration layer, not inside the module.
**Detection:** Grep for license-related conditionals outside the license service. Multiple database tables tracking activation state.
**Phase relevance:** Phase 2 (marketplace/module system). The license service must exist before the first licensable module.
---
### Pitfall 9: Desktop Wrapper (Electron/Tauri) Divergence from Web
**What goes wrong:** The desktop wrapper introduces platform-specific behavior (file system access, window management, notifications) that diverges from the web version. Bugs exist only in the wrapper. Features work in-browser but not in the desktop app.
**Why it happens:** Teams treat the wrapper as "just a browser window" but it isn't — WebView quirks, different security contexts, and OS integration create a separate test surface.
**Prevention:**
- Keep the desktop wrapper as thin as possible — it should be a shell around the same web app, not a fork.
- Use Tauri over Electron for smaller binary size and better security defaults (Tessera is already considering this).
- Run the same URL in the wrapper that the browser uses (point to localhost or the deployed server).
- Do NOT add desktop-only features to the web codebase — keep OS integration in the wrapper layer only.
- Accept that the desktop wrapper is a Phase 4+ concern — do not build it until the web platform is stable.
**Detection:** Features that work in Chrome but fail in the desktop app. Desktop-specific code paths in the shared frontend codebase.
**Phase relevance:** Phase 4+ (desktop client). Do not start this until the web platform is feature-complete and stable.
---
### Pitfall 10: Tenant Context Lost in Async Operations
**What goes wrong:** Background jobs, event handlers, and scheduled tasks lose the tenant context that was present during the original HTTP request. Jobs process data without tenant scoping, or worse, process one tenant's job with another tenant's context.
**Why it happens:** HTTP middleware sets tenant context on the request. But when a job is enqueued, the worker that picks it up has no request context. If the job payload doesn't explicitly include tenant_id, the worker either fails or defaults to a wrong/global context.
**Consequences:** Data corruption across tenants. Background jobs that silently operate on wrong tenant's data. Difficult to reproduce because it depends on job scheduling order.
**Prevention:**
- Every job payload MUST include `tenant_id` as a required field — enforce via TypeScript types or schema validation.
- Worker initialization must set the tenant context (including database RLS session variable) before processing any job.
- Never rely on ambient/global state for tenant identification in async contexts.
- Add assertion checks: if a job's tenant_id doesn't match the database session's tenant, abort with an error.
**Detection:** Jobs that succeed without a tenant_id in their payload. Log analysis showing jobs processing data from multiple tenants in a single execution.
**Phase relevance:** Phase 2 (background processing). Must be enforced from the first background job.
---
## Minor Pitfalls
### Pitfall 11: Docker Volume Permission Mismatches
**What goes wrong:** Containers run as non-root (correctly) but mounted volumes have root ownership from the host. The application cannot write to its data directory, causing silent failures or crashes on startup.
**Prevention:** Explicitly set user/group in Dockerfile. Use named volumes instead of bind mounts in production. Set permissions in an entrypoint script. Test with `docker-compose up` from a fresh state.
**Phase relevance:** Phase 1 (Docker setup).
---
### Pitfall 12: Over-Engineering the Module Communication Protocol
**What goes wrong:** Teams design complex message buses, event sourcing, or gRPC streaming for module communication when simple REST calls would suffice. The protocol becomes harder to debug than the business logic.
**Prevention:** Start with synchronous REST between core and modules. Add async messaging only when you have a proven need (e.g., long-running tasks). Keep the protocol debuggable with standard HTTP tools (curl, Postman).
**Phase relevance:** Phase 2 (module system). Start simple, evolve based on real needs.
---
### Pitfall 13: Gitea Automation Tight Coupling
**What goes wrong:** The platform's deployment pipeline is so tightly integrated with Gitea that changing version control systems or CI tools requires rewriting the deployment process.
**Prevention:** Abstract the VCS integration behind an interface. Use standard Git operations rather than Gitea-specific APIs where possible. Keep CI/CD configuration in standard formats (Dockerfiles, compose files) rather than Gitea-specific workflows exclusively.
**Phase relevance:** Phase 3+ (CI/CD integration). Design the abstraction before implementing Gitea-specific hooks.
---
### Pitfall 14: Theme System Without Design Tokens
**What goes wrong:** Light/dark theme is implemented with scattered CSS variables or conditional classes. Adding a third theme or adjusting the color palette requires touching hundreds of files.
**Prevention:** Use a design token system from day one — a single source of truth for colors, spacing, typography. Themes are just alternative token sets. Use CSS custom properties at the `:root` level with a theme class toggle.
**Phase relevance:** Phase 1 (frontend scaffolding). Design tokens must be established before the first component is styled.
---
## Phase-Specific Warnings
| Phase Topic | Likely Pitfall | Mitigation |
|-------------|---------------|------------|
| Core architecture | Tenant isolation not enforced at DB level | Implement PostgreSQL RLS from day one |
| Core architecture | Flat Docker network | Design network segmentation in initial docker-compose.yml |
| Core architecture | i18n deferred | Set up translation infrastructure with first component |
| Authentication | LDAP as auth instead of directory sync | Build auth abstraction layer; LDAP is one provider |
| Module system | No versioned API contract | Define module SDK with semantic versioning before first module |
| Module system | In-process module loading | Each module = own container from the start |
| Module system | License checks scattered | Centralized LicenseService as single gate |
| Dashboard | Widget state write amplification | Debounced persistence, JSON column, proven grid library |
| Background jobs | Tenant context lost in workers | Mandatory tenant_id in all job payloads |
| Desktop client | Built too early, diverges from web | Defer until web is stable; keep wrapper minimal |
| Deployment | Volume permissions break non-root containers | Named volumes, entrypoint permission scripts |
| VCS integration | Gitea-specific tight coupling | Abstract behind interface, use standard Git ops |
## Sources
- [Multi-Tenant SaaS Architecture: What Nobody Tells You Before You Build](https://dev.to/actinode/multi-tenant-saas-architecture-what-nobody-tells-you-before-you-build-a4h) - Confidence: HIGH
- [Designing Multi-Tenant SaaS Architecture: Mistakes to Avoid](https://www.saasadviser.co/blog/multi-tenant-saas-architecture-mistakes-best-practices) - Confidence: MEDIUM
- [Multi-Tenant Row-Level Security in PostgreSQL: A Production Pattern](https://dev.to/uaslimcreate/building-multi-tenant-row-level-security-in-postgresql-a-production-pattern-4n2k) - Confidence: HIGH
- [Docker Network Isolation Pitfalls](https://hexshift.medium.com/docker-network-isolation-pitfalls-that-put-your-applications-at-risk-b60356a14033) - Confidence: MEDIUM
- [The LDAP Authentication Anti-Pattern](https://blog.lithnet.io/2018/03/the-ldap-authentication-anti-pattern.html) - Confidence: HIGH
- [Plugin Versioning Best Practices](https://devactivity.com/insights/mastering-plugin-versioning-ensuring-compatibility-for-your-software-measurement-tool/) - Confidence: MEDIUM
- [Feature Toggles (Martin Fowler)](https://martinfowler.com/articles/feature-toggles.html) - Confidence: HIGH
- [Common Technical Challenges in i18n](https://activeloc.com/blog/i18n-technical-challenges/) - Confidence: MEDIUM
- [Electron vs. Tauri](https://www.dolthub.com/blog/2025-11-13-electron-vs-tauri/) - Confidence: MEDIUM
- [Building Interactive Dashboards with React Grid Layout](https://www.ilert.com/blog/building-interactive-dashboards-why-react-grid-layout-was-our-best-choice) - Confidence: MEDIUM
- [AWS: Multi-tenant data isolation with PostgreSQL Row Level Security](https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/) - Confidence: HIGH
+203
View File
@@ -0,0 +1,203 @@
# Technology Stack
**Project:** Tessera - Modular Portal Platform with Marketplace
**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.
### 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 |
### 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 |
## 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)
```
## 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
- [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
+165
View File
@@ -0,0 +1,165 @@
# Project Research Summary
**Project:** Tessera - Modular Portal Platform with Marketplace
**Domain:** Multi-tenant SaaS portal with module marketplace, configurable dashboard, desktop wrapper
**Researched:** 2026-06-18
**Confidence:** HIGH
## Executive Summary
Tessera is a modular portal platform where tenants activate licensed workflow modules from a marketplace, interact through a configurable drag-and-drop dashboard, and optionally use a lightweight desktop wrapper. The expert consensus for this type of product is a modular monolith backend (NestJS) with PostgreSQL Row-Level Security for tenant isolation, a Next.js frontend shell that lazy-loads module UIs on demand, and Keycloak for identity management. This stack avoids premature microservice complexity while maintaining clean extraction boundaries.
The recommended approach is to build foundation-first: Docker infrastructure with network segmentation, database schema with RLS policies, and the frontend shell with i18n and theming baked in from day one. Authentication and tenant context middleware come next, followed by the module system with a versioned API contract, then marketplace and dashboard features. The desktop wrapper (Tauri) is a late-stage concern that wraps the already-working web app.
The primary risks are tenant data leakage (mitigated by RLS at the database level, not application-level filtering), module API instability (mitigated by a versioned SDK contract from the start), and i18n/theme retrofitting pain (mitigated by establishing both before the first UI component). Docker network segmentation is a day-one infrastructure requirement to prevent lateral movement between containers.
## Key Findings
### Recommended Stack
A pnpm + Turborepo monorepo with three apps (Next.js 16 frontend, NestJS 11 backend, Tauri 2 desktop) and shared packages for types, UI components, config, and database schema.
**Core technologies:**
- **Next.js 16 + React 19**: App Router, Server Components, Turbopack, multi-tenant middleware support
- **NestJS 11**: Modular architecture maps directly to Tessera's module system; DI, guards, interceptors
- **PostgreSQL 16 + Prisma 7**: RLS for multi-tenancy, Prisma Client Extensions for tenant context, pure TS runtime in v7
- **Keycloak 26.6**: Native multi-tenancy via realms, LDAP federation, eliminates custom auth
- **Tauri 2**: 5MB installer vs Electron's 150MB; thin wrapper around the web app
- **Tailwind 4 + shadcn/ui**: Utility-first styling with owned component library, trivial dark mode
- **react-grid-layout 2.2**: TypeScript rewrite with hooks API for drag-and-drop dashboard
- **Traefik 3**: Docker-native reverse proxy with auto-SSL and label-based config
### Expected Features
**Must have (table stakes):**
- Authentication with RBAC and multi-tenancy with data isolation
- Sidebar navigation with module categories
- Module activation/deactivation and admin-managed licensing per tenant
- i18n (DE + EN), light/dark theme, responsive layout
- Basic dashboard with widgets, session management, error handling/loading states
- User profile and settings, search/filter in module lists
**Should have (differentiators):**
- Drag-and-drop dashboard with resizable widgets
- Widget marketplace/gallery
- Module hot-activation without restart
- Per-tenant branding (logo + accent color)
- Module-provided dashboard widgets
- Keyboard shortcuts and command palette
- LDAP/AD directory sync
**Defer (v2+):**
- LDAP integration (manual user creation suffices initially)
- Desktop wrapper (browser works fine first)
- Notification center, admin impersonation, onboarding wizard
### Architecture Approach
Modular monolith: single deployable NestJS backend with domain-separated modules (auth, tenant, marketplace, dashboard, module loader) communicating via in-process calls. Frontend is a React shell that lazy-loads module UIs via dynamic imports based on tenant activation state. Shared PostgreSQL database with RLS enforces tenant isolation at the database level. Modules follow a standard interface contract (TesseraModule) for uniform discovery and lifecycle management.
**Major components:**
1. **Frontend Shell** -- routing, sidebar, theme, i18n; lazy-loads module UIs
2. **API Gateway (Traefik)** -- reverse proxy, rate limiting, tenant header injection
3. **Auth Module** -- Keycloak integration, JWT, session management
4. **Tenant Module** -- context resolution, RLS activation per request
5. **Module Loader** -- plugin lifecycle: discover, validate, activate, deactivate
6. **Marketplace Service** -- module catalog, licensing, activation per tenant
7. **Dashboard Engine** -- widget grid, layout persistence, module-contributed widgets
### Critical Pitfalls
1. **Tenant data leaks** -- Enforce PostgreSQL RLS from day one; never rely on application-level WHERE clauses alone
2. **Module API contract instability** -- Define a versioned @tessera/sdk with semver before shipping the first module
3. **Flat Docker network** -- Segment into frontend-net, backend-net, data-net from the initial docker-compose.yml
4. **i18n retrofitting** -- Every string through t('key') from the first component; design for German text length
5. **LDAP as direct auth** -- Use Keycloak as identity layer; LDAP for directory sync only, never raw bind for auth
## Implications for Roadmap
### Phase 1: Foundation and Infrastructure
**Rationale:** Everything depends on the database, Docker setup, backend skeleton, and frontend shell. RLS, network segmentation, i18n, and theming must exist before any feature code.
**Delivers:** Running Docker Compose stack with PostgreSQL (RLS enabled), NestJS skeleton, Next.js shell with routing/layout/sidebar frame, i18n infrastructure, theme/design tokens, Traefik reverse proxy, segmented Docker networks.
**Addresses:** Responsive layout, theme system, i18n framework, error handling/loading states (table stakes infrastructure)
**Avoids:** Pitfalls 1 (tenant isolation), 4 (flat Docker network), 6 (i18n retrofitting), 14 (theme without design tokens), 11 (volume permissions)
### Phase 2: Authentication, Tenancy, and User Management
**Rationale:** Module activation requires knowing who the user is and which tenant they belong to. Auth is the prerequisite for everything else.
**Delivers:** Keycloak integration, login/logout, JWT-based sessions, RBAC, tenant context middleware setting RLS per request, user management CRUD, user profile/settings.
**Addresses:** Authentication, RBAC, multi-tenancy, session management, user profile (table stakes)
**Avoids:** Pitfalls 5 (LDAP as auth), 10 (tenant context lost in async)
### Phase 3: Module System and First Module
**Rationale:** The module system must exist before the marketplace can manage it. Building the first real module (Domaincheck) validates the entire architecture.
**Delivers:** Module interface contract (@tessera/sdk), database-driven module registry, module loader with activation/deactivation, lazy-loading frontend integration, Domaincheck module as proof of concept.
**Addresses:** Module activation/deactivation, sidebar navigation reflecting active modules (table stakes); module dependency declaration (differentiator)
**Avoids:** Pitfalls 2 (API contract instability), 3 (monolithic module loading), 8 (scattered licensing), 12 (over-engineered module communication)
### Phase 4: Marketplace and Dashboard
**Rationale:** Both depend on the module system but are largely independent of each other. Marketplace manages module catalog and licensing; dashboard provides the user home with widgets.
**Delivers:** Marketplace UI (browse, activate, manage licenses), licensing service, dashboard with react-grid-layout, core widgets (clock, search, notes, calendar), drag-and-drop with layout persistence, module-provided widgets.
**Addresses:** Marketplace UI with licensing (table stakes); drag-and-drop dashboard, widget gallery, module-provided widgets (differentiators)
**Avoids:** Pitfall 7 (widget state explosion -- debounced persistence, JSON column)
### Phase 5: Polish, Desktop, and Advanced Features
**Rationale:** Enhancement layer on a stable web platform. Desktop wrapper requires stable web app. LDAP and advanced features add value but are not launch-blocking.
**Delivers:** Tauri desktop wrapper, LDAP/AD directory sync via Keycloak, per-tenant branding, keyboard shortcuts/command palette, Gitea CI/CD integration.
**Addresses:** Desktop wrapper, LDAP integration, per-tenant branding, command palette (differentiators)
**Avoids:** Pitfall 9 (desktop divergence -- keep wrapper thin), 13 (Gitea tight coupling)
### Phase Ordering Rationale
- Phases follow strict dependency chains: infrastructure -> auth/tenancy -> modules -> marketplace/dashboard -> polish
- i18n and theming are in Phase 1 (not Phase 5) because retrofitting is a known moderate pitfall
- Module system before marketplace because the marketplace manages modules -- the registry must exist first
- Dashboard is grouped with marketplace (Phase 4) because both depend on the module system and can partially parallelize
- Desktop wrapper is last because it wraps an already-working web app and building it earlier risks divergence
### Research Flags
Phases likely needing deeper research during planning:
- **Phase 2:** Keycloak realm-per-tenant configuration and NestJS integration patterns; RLS session variable management with Prisma Client Extensions
- **Phase 3:** Module interface contract design; lazy-loading strategy for module UIs with Next.js App Router
- **Phase 4:** react-grid-layout integration with module-provided widgets; layout persistence schema design
Phases with standard patterns (skip research-phase):
- **Phase 1:** Docker Compose, NestJS/Next.js scaffolding, Tailwind/shadcn setup -- well-documented, established patterns
- **Phase 5:** Tauri wrapper is a thin shell; LDAP sync via Keycloak is documented
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | All technologies are mature, well-documented, and version-verified. Clear rationale for each choice with alternatives considered. |
| Features | HIGH | Feature landscape is well-defined with clear table stakes vs differentiators. Dependency chain is explicit. |
| Architecture | HIGH | Modular monolith with RLS is a proven pattern for multi-tenant SaaS. Multiple authoritative sources confirm the approach. |
| Pitfalls | HIGH | Critical pitfalls sourced from production incident reports and authoritative guides. Prevention strategies are concrete. |
**Overall confidence:** HIGH
### Gaps to Address
- **Prisma + RLS integration specifics:** Prisma Client Extensions for setting session variables per request need validation during Phase 2 implementation. The pattern is documented but not battle-tested at scale with Prisma 7.
- **Module hot-activation with Next.js App Router:** Lazy-loading module UIs via dynamic imports is standard React, but integrating with Next.js App Router file-based routing may require a custom module route convention. Needs Phase 3 research.
- **Keycloak realm-per-tenant vs single-realm with groups:** Research assumes realm-per-tenant mapping but single-realm with tenant claims may be simpler for the initial scale. Decide during Phase 2 planning.
- **react-grid-layout v2 maturity:** The TypeScript rewrite (v2.2) is recent. Fallback to v1 /legacy API if v2 hooks have issues.
## Sources
### Primary (HIGH confidence)
- [Next.js 16 Docs](https://nextjs.org/docs) -- App Router, Server Components, middleware
- [NestJS Documentation](https://docs.nestjs.com/) -- Modules, guards, interceptors
- [Prisma ORM 7](https://www.prisma.io/blog/announcing-prisma-orm-7-0-0) -- Pure TS runtime, Client Extensions
- [PostgreSQL RLS](https://aws.amazon.com/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/) -- Multi-tenant isolation
- [Keycloak 26.6](https://www.keycloak.org/) -- Identity provider, realm multi-tenancy
- [Tauri 2.0](https://v2.tauri.app/) -- Desktop wrapper
- [Martin Fowler: Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) -- Module activation as feature gating
### Secondary (MEDIUM confidence)
- [Multi-Tenant SaaS Architecture guides](https://apipilot.com/developing-a-multi-tenant-saas-application-the-2026-architecture-guide/) -- Architecture patterns
- [Node.js Plugin Architecture](https://medium.com/codeelevation/node-js-plugin-architecture-build-your-own-plugin-system-with-es-modules-5b9a5df19884) -- Module system design
- [Docker Network Isolation](https://hexshift.medium.com/docker-network-isolation-pitfalls-that-put-your-applications-at-risk-b60356a14033) -- Network segmentation
- [LDAP Authentication Anti-Pattern](https://blog.lithnet.io/2018/03/the-ldap-authentication-anti-pattern.html) -- Auth layer design
---
*Research completed: 2026-06-18*
*Ready for roadmap: yes*