16c2d5b6af
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>
321 lines
16 KiB
Markdown
321 lines
16 KiB
Markdown
# 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/)
|