# 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//` 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 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//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; onDeactivate?: (tenantId: string) => Promise; } ``` ### 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 }>; configSchema?: JSONSchema; // Optional widget settings } export interface WidgetProps { config: Record; 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/)