Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19 KiB
Module System
ArchiTools internal architecture reference -- module registry, loader, and lifecycle.
Overview
ArchiTools is composed of discrete modules, each representing a self-contained functional unit (e.g., Devize, Oferte, Pontaj). Modules are registered in a central registry, gated behind feature flags, lazily loaded at runtime, and isolated from one another. This design provides three guarantees:
- Zero bundle cost for disabled modules -- they are never imported.
- Runtime safety -- a crash in one module cannot propagate to another.
- Clear ownership boundaries -- each module owns its types, storage, and UI.
Module Definition Model
Every module exposes a config.ts at its root that exports a ModuleConfig object. This is the module's identity card -- it tells the platform everything it needs to know to register, gate, load, and display the module.
ModuleConfig Interface
// src/core/module-registry/types.ts
type ModuleCategory = 'operations' | 'generators' | 'management' | 'tools' | 'ai';
type Visibility = 'all' | 'admin' | 'internal';
interface ModuleConfig {
id: string; // unique kebab-case identifier, e.g. "devize-generator"
name: string; // Romanian display name, e.g. "Generator Devize"
description: string; // Romanian description shown in module catalog
icon: string; // Lucide icon name, e.g. "file-spreadsheet"
route: string; // base route path, e.g. "/devize"
category: ModuleCategory; // sidebar grouping
featureFlag: string; // corresponding key in the feature flag system
visibility: Visibility; // audience restriction
version: string; // semver, e.g. "1.0.0"
dependencies?: string[]; // other module IDs this depends on
storageNamespace: string; // isolated localStorage/IndexedDB prefix
navOrder: number; // sidebar sort order within its category
tags?: string[]; // capability tags for search/filtering
}
Field Contracts
| Field | Constraint |
|---|---|
id |
Must be unique across all modules. Kebab-case. Used as the key in the registry map. |
route |
Must start with /. Must not collide with another module's route. |
featureFlag |
Must have a matching entry in src/config/flags.ts. |
storageNamespace |
Must be unique. Convention: architools.[module-id]. |
navOrder |
Lower numbers appear first. Modules in the same category are sorted by this value. |
dependencies |
If specified, the platform will refuse to load this module unless all listed module IDs are also enabled and loaded. |
Example
// src/modules/devize-generator/config.ts
import { ModuleConfig } from '@/core/module-registry/types';
export const registraturaConfig: ModuleConfig = {
id: 'registratura',
name: 'Registratură',
description: 'Registru de corespondență multi-firmă cu urmărire documente.',
icon: 'book-open',
route: '/registratura',
category: 'operations',
featureFlag: 'module.registratura',
visibility: 'all',
version: '1.0.0',
dependencies: [],
storageNamespace: 'architools.registratura',
navOrder: 10,
tags: ['registry', 'documents', 'correspondence'],
};
Module Directory Structure
Every module lives under src/modules/ and follows a strict directory convention:
src/modules/[module-name]/
components/ # React components specific to this module
hooks/ # Custom hooks encapsulating business logic
services/ # Data services (CRUD, transformations) using the storage abstraction
types.ts # Module-specific TypeScript types and interfaces
config.ts # ModuleConfig export (the module's identity)
index.ts # Public API barrel export
Rules
components/-- Only UI components consumed within this module. If a component is needed by multiple modules, it belongs insrc/shared/components/common/.hooks/-- Business logic hooks. Must not contain UI rendering. Must not directly calllocalStorageorIndexedDB-- use the storage abstraction via services.services/-- All data access goes through services. Services consume the platform's storage abstraction layer (src/core/storage/) and must scope all keys understorageNamespace.types.ts-- All TypeScript types that are internal to this module. Shared types go insrc/core/*/types.ts.config.ts-- Exports exactly oneModuleConfigobject. No side effects.index.ts-- The barrel export. This is the only file that other parts of the system (the registry, the loader) may import from. It must exportconfigand the lazy-loadable root component.
Barrel Export Convention
// src/modules/registratura/index.ts
export { registraturaConfig as config } from './config';
export { default as RegistraturaModule } from './components/RegistraturaModule';
The root component (RegistraturaModule) is what gets lazy-loaded and rendered at the module's route.
Module Registry
The central registry lives at src/config/modules.ts. It imports every module's config and builds a lookup map.
// src/config/modules.ts
import { registraturaConfig } from '@/modules/registratura/config';
import { emailSignatureConfig } from '@/modules/email-signature/config';
import { wordXmlConfig } from '@/modules/word-xml/config';
import { promptGeneratorConfig } from '@/modules/prompt-generator/config';
// ... all module configs
import type { ModuleConfig } from '@/core/module-registry/types';
const moduleConfigs: ModuleConfig[] = [
registraturaConfig,
emailSignatureConfig,
wordXmlConfig,
promptGeneratorConfig,
// ... add new modules here
];
/** Map of module ID -> ModuleConfig for O(1) lookups */
export const moduleRegistry: Map<string, ModuleConfig> = new Map(
moduleConfigs.map((config) => [config.id, config])
);
/** All registered configs, sorted by category then navOrder */
export const allModules: ModuleConfig[] = [...moduleConfigs].sort((a, b) => {
if (a.category !== b.category) return a.category.localeCompare(b.category);
return a.navOrder - b.navOrder;
});
/** Get configs for a specific category */
export function getModulesByCategory(category: ModuleConfig['category']): ModuleConfig[] {
return allModules.filter((m) => m.category === category);
}
/** Validate that all module IDs are unique. Called once at startup in dev mode. */
export function validateRegistry(): void {
const ids = moduleConfigs.map((m) => m.id);
const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
if (duplicates.length > 0) {
throw new Error(`Duplicate module IDs detected: ${duplicates.join(', ')}`);
}
const routes = moduleConfigs.map((m) => m.route);
const duplicateRoutes = routes.filter((r, i) => routes.indexOf(r) !== i);
if (duplicateRoutes.length > 0) {
throw new Error(`Duplicate module routes detected: ${duplicateRoutes.join(', ')}`);
}
}
The registry is a static, build-time artifact. It does not perform any dynamic discovery -- every module must be explicitly imported. This is intentional: it guarantees that the dependency graph is fully visible to the bundler for tree-shaking and code-splitting.
Module Loader
The loader is responsible for conditionally importing and rendering a module based on its feature flag status.
Lazy Loading Strategy
Each module's root component is wrapped in React.lazy(). The import only fires when the component is actually rendered, which only happens when the feature flag is enabled.
// src/core/module-registry/loader.ts
import React, { Suspense } from 'react';
import type { ModuleConfig } from '@/core/module-registry/types';
import { ModuleErrorBoundary } from '@/shared/components/common/module-error-boundary';
import { ModuleSkeleton } from '@/shared/components/common/module-skeleton';
/** Registry of lazy component factories, keyed by module ID */
const lazyComponents: Record<string, React.LazyExoticComponent<React.ComponentType>> = {
'registratura': React.lazy(() => import('@/modules/registratura')),
'email-signature': React.lazy(() => import('@/modules/email-signature')),
'word-xml': React.lazy(() => import('@/modules/word-xml')),
'prompt-generator': React.lazy(() => import('@/modules/prompt-generator')),
// ... add new modules here
};
interface ModuleLoaderProps {
config: ModuleConfig;
}
export function ModuleLoader({ config }: ModuleLoaderProps) {
const LazyComponent = lazyComponents[config.id];
if (!LazyComponent) {
return <ModuleNotFound moduleId={config.id} />;
}
return (
<ModuleErrorBoundary moduleId={config.id} moduleName={config.name}>
<Suspense fallback={<ModuleSkeleton moduleName={config.name} />}>
<LazyComponent />
</Suspense>
</ModuleErrorBoundary>
);
}
Key Mechanics
React.lazy()accepts a function that returns a dynamicimport(). The import is only executed the first time the component renders.Suspenseshows a skeleton loader while the chunk downloads and parses.ModuleErrorBoundarycatches any rendering error within the module and displays a localized error state without crashing the entire application.
Why Not Automatic Discovery?
We intentionally avoid filesystem-based auto-discovery (e.g., import.meta.glob) because:
- It defeats tree-shaking: the bundler must include every discovered module.
- It hides the dependency graph from code review.
- It makes the build non-deterministic if modules are added/removed from the filesystem.
Explicit registration is a small amount of boilerplate that pays for itself in predictability.
Navigation Auto-Discovery
The sidebar reads directly from the module registry to build its navigation tree. No separate nav configuration file exists.
// src/components/navigation/Sidebar.tsx (simplified)
import { allModules } from '@/config/modules';
import { useFeatureFlag } from '@/core/feature-flags';
import { groupBy } from '@/shared/lib/utils';
const CATEGORY_LABELS: Record<string, string> = {
operations: 'Operatiuni',
generators: 'Generatoare',
management: 'Management',
tools: 'Instrumente',
ai: 'AI & Automatizari',
};
export function Sidebar() {
const grouped = groupBy(allModules, (m) => m.category);
return (
<nav>
{Object.entries(CATEGORY_LABELS).map(([category, label]) => {
const modules = grouped[category] ?? [];
const visibleModules = modules.filter((m) => {
const flagEnabled = useFeatureFlag(m.featureFlag);
return flagEnabled;
});
if (visibleModules.length === 0) return null;
return (
<SidebarGroup key={category} label={label}>
{visibleModules.map((m) => (
<SidebarItem key={m.id} icon={m.icon} href={m.route}>
{m.name}
</SidebarItem>
))}
</SidebarGroup>
);
})}
</nav>
);
}
This means:
- Adding a module to the registry and enabling its flag automatically adds it to the sidebar.
- Disabling a flag automatically removes the module from navigation.
navOrdercontrols position within each category group.visibilitycan be checked against the current user's role for further filtering.
Module Isolation Rules
Modules must be isolated from one another. This is enforced by convention and code review.
What Is Allowed
| From | To | Allowed? |
|---|---|---|
| Module A component | Module A hook | Yes |
| Module A service | src/lib/storage/ |
Yes |
| Module A component | src/components/shared/ |
Yes |
| Module A hook | src/lib/* (platform utilities) |
Yes |
| Module A | Module B (any file) | No |
What Is Not Allowed
- Direct imports between modules. Module A must never
import { something } from '@/modules/module-b/...'. If two modules need to share data, they do so through a shared service insrc/lib/or through an event bus. - Shared mutable state. No module may write to a global store that another module reads, unless that store is a platform-level shared service (e.g., user context, notification system).
- Cross-module storage access. Each module's
storageNamespaceis its own territory. Module A must never read or write keys prefixed with Module B's namespace.
Enforcement
- ESLint rule (planned): a custom
no-restricted-importspattern that forbids@/modules/*/imports from within a different module directory. - Code review: any PR that introduces a cross-module import must be flagged.
Module Lifecycle
A module goes through the following stages from registration to active use:
1. Registration
Module config is imported into src/config/modules.ts.
Module appears in the registry map.
2. Feature Flag Check
At render time, the navigation system and route handler check
the module's featureFlag via useFeatureFlag(). If disabled,
the module is invisible and its code is never loaded.
3. Lazy Load
When the user navigates to the module's route (and the flag is
enabled), React.lazy() triggers the dynamic import(). The
bundler fetches the module's code chunk over the network.
4. Suspense Boundary
While the chunk loads, the Suspense fallback (ModuleSkeleton)
is displayed.
5. Error Boundary Setup
The ModuleErrorBoundary wraps the module. If any error occurs
during mount or subsequent renders, the boundary catches it.
6. Mount
The module's root component mounts. It may initialize hooks,
call services, and read from its storage namespace.
7. Storage Namespace Init
On first use, the module's services initialize their storage
keys under the module's storageNamespace prefix. This happens
lazily -- no storage is allocated until the module actually
writes data.
8. Active
The module is fully interactive. It manages its own state
and communicates with the platform only through shared services.
9. Unmount
When the user navigates away, the module unmounts normally via
React's unmount lifecycle. Cleanup happens in useEffect
return functions as usual.
Diagram
Registration ──> Flag Check ──┬──> [disabled] ──> Not loaded, not visible
│
└──> [enabled] ──> Lazy Load ──> Suspense ──> Mount ──> Active
│
└──> [error] ──> Error Boundary UI
Error Boundaries
Every module is wrapped in a ModuleErrorBoundary. This is a React error boundary class component that:
- Catches JavaScript errors in the module's component tree.
- Logs the error with the module's ID for diagnostics.
- Renders a fallback UI with the module name and a retry button.
- Does not crash the shell (sidebar, header, other modules remain functional).
// src/components/ModuleErrorBoundary.tsx
import React from 'react';
interface Props {
moduleId: string;
moduleName: string;
children: React.ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ModuleErrorBoundary extends React.Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(
`[ModuleError] ${this.props.moduleId}:`,
error,
errorInfo.componentStack
);
// Future: send to error reporting service
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<h2 className="text-lg font-semibold text-destructive">
Eroare in modulul “{this.props.moduleName}”
</h2>
<p className="mt-2 text-sm text-muted-foreground">
A aparut o eroare neasteptata. Incercati din nou sau contactati echipa tehnica.
</p>
<button
onClick={this.handleRetry}
className="mt-4 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground"
>
Reincearca
</button>
</div>
);
}
return this.props.children;
}
}
How to Add a New Module
Step-by-step checklist for adding a module to ArchiTools. For full code examples, see MODULE-DEVELOPMENT.md.
1. Create the directory structure
mkdir -p src/modules/[module-name]/{components,hooks,services}
touch src/modules/[module-name]/{types.ts,config.ts,index.ts}
2. Write config.ts
Export a ModuleConfig with a unique id, route, featureFlag, and storageNamespace.
3. Implement the module
Build types, services, hooks, and components inside the module directory.
4. Create index.ts barrel export
Export config and the default root component.
5. Register in src/config/modules.ts
Import the config and add it to the moduleConfigs array.
6. Register the lazy component in src/lib/module-loader.ts
Add a React.lazy(() => import(...)) entry keyed by the module ID.
7. Add a feature flag in src/config/flags.ts
Create a flag with the key matching the module's featureFlag field.
8. Create the route
Add src/app/(modules)/[module-name]/page.tsx that renders the ModuleLoader.
9. Test
- Verify the module appears in the sidebar when the flag is enabled.
- Verify the module is completely absent (no network requests for its chunk) when the flag is disabled.
- Verify the error boundary catches a thrown error gracefully.
- Verify storage operations use the correct namespace prefix.
Appendix: Module Category Descriptions
| Category | Key | Intended Use |
|---|---|---|
| Operațiuni | operations |
Day-to-day operational tools (registratura, password vault) |
| Generatoare | generators |
Document/artifact generators (email signature, Word XML, Word templates) |
| Management | management |
Registries and asset management (IT inventory, address book, digital signatures) |
| Instrumente | tools |
Utility tools (mini utilities, tag manager) |
| AI & Automatizări | ai |
AI-powered features (prompt generator, AI chat) |