Files
orion/docs/architecture/module-system.md
Samir Boulahtit 967f08e4ba feat: add module definition completeness validation and permissions
Add new validation rules MOD-020 to MOD-023 for module definition
completeness and standardize permissions across all modules.

Changes:
- Add MOD-020: Module definitions must have required attributes
- Add MOD-021: Modules with menus should have features
- Add MOD-022: Feature modules should have permissions
- Add MOD-023: Modules with routers should use get_*_with_routers pattern

Module permissions added:
- analytics: view, export, manage_dashboards
- billing: view_tiers, manage_tiers, view_subscriptions, manage_subscriptions, view_invoices
- cart: view, manage
- checkout: view_settings, manage_settings
- cms: view_pages, manage_pages, view_media, manage_media, manage_themes
- loyalty: view_programs, manage_programs, view_rewards, manage_rewards
- marketplace: view_integration, manage_integration, sync_products
- messaging: view_messages, send_messages, manage_templates
- payments: view_gateways, manage_gateways, view_transactions

Module improvements:
- Complete cart module with features and permissions
- Complete checkout module with features and permissions
- Add features to catalog module
- Add version to cms module
- Fix loyalty platform_router attachment
- Add path definitions to payments module
- Remove empty scheduled_tasks from dev_tools module

Documentation:
- Update module-system.md with new validation rules
- Update architecture-rules.md with MOD-020 to MOD-023

Tests:
- Add unit tests for module definition completeness
- Add tests for permission structure validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 18:23:04 +01:00

988 lines
34 KiB
Markdown

# Module System Architecture
The Wizamart platform uses a **plug-and-play modular architecture** where modules are fully self-contained and automatically discovered. Simply create a module directory with the required structure, and the framework handles registration, routing, and resource loading automatically.
## Key Features
- **Auto-Discovery**: Modules are automatically discovered from `app/modules/*/definition.py`
- **Zero Configuration**: No changes to `main.py`, `registry.py`, or other framework files needed
- **Self-Contained**: Each module owns its routes, services, models, templates, and translations
- **Hot-Pluggable**: Add or remove modules by simply adding/removing directories
## Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ FRAMEWORK LAYER │
│ (Infrastructure that modules depend on - not modules themselves) │
│ │
│ Config │ Database │ Auth │ Permissions │ Observability │ Celery │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ AUTO-DISCOVERED MODULE LAYER │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CORE MODULES (Always Enabled) │ │
│ │ core │ tenancy │ cms │ customers │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ OPTIONAL MODULES (Per-Platform) │ │
│ │ payments │ billing │ inventory │ orders │ marketplace │ ...│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ INTERNAL MODULES (Admin Only) │ │
│ │ dev-tools │ monitoring │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## Auto-Discovery System
All module components are automatically discovered by the framework:
| Component | Discovery Location | Auto-Loaded By |
|-----------|-------------------|----------------|
| **Registry** | `*/definition.py` | `app/modules/discovery.py` |
| **Configuration** | `*/config.py` | `app/modules/config.py` |
| **API Routes** | `*/routes/api/*.py` | `app/modules/routes.py` |
| **Page Routes** | `*/routes/pages/*.py` | `app/modules/routes.py` |
| **Tasks** | `*/tasks/__init__.py` | `app/modules/tasks.py` |
| **Templates** | `*/templates/` | `app/templates_config.py` |
| **Static Files** | `*/static/` | `main.py` |
| **Locales** | `*/locales/*.json` | `app/utils/i18n.py` |
| **Migrations** | `*/migrations/versions/` | `app/modules/migrations.py` |
### Creating a New Module (Zero Framework Changes)
```bash
# 1. Create module directory
mkdir -p app/modules/mymodule/{routes/{api,pages},services,models,schemas,templates/mymodule/vendor,static/vendor/js,locales,tasks}
# 2. Create required files
touch app/modules/mymodule/__init__.py
touch app/modules/mymodule/definition.py
touch app/modules/mymodule/exceptions.py
# 3. That's it! The framework auto-discovers and registers everything.
```
## Three-Tier Classification
### Core Modules (5)
Core modules are **always enabled** and cannot be disabled. They provide fundamental platform functionality.
| Module | Description | Key Features | Permissions |
|--------|-------------|--------------|-------------|
| `contracts` | Cross-module protocols and interfaces | Service protocols, type-safe interfaces | - |
| `core` | Dashboard, settings, profile | Basic platform operation | 5 |
| `cms` | Content pages, media library, themes | Content management | 5 |
| `customers` | Customer database, profiles, segmentation | Customer data management | 4 |
| `tenancy` | Platform, company, vendor, admin user management | Multi-tenant infrastructure | 4 |
### Optional Modules (11)
Optional modules can be **enabled or disabled per platform**. They provide additional functionality that may not be needed by all platforms.
| Module | Dependencies | Description | Permissions |
|--------|--------------|-------------|-------------|
| `analytics` | - | Reports, dashboards | 3 |
| `billing` | `payments` | Platform subscriptions, vendor invoices | 5 |
| `cart` | `inventory` | Shopping cart management, session-based carts | 2 |
| `catalog` | `inventory` | Customer-facing product browsing | 6 |
| `checkout` | `cart`, `orders`, `payments`, `customers` | Cart-to-order conversion, checkout flow | 2 |
| `inventory` | - | Stock management, locations | 3 |
| `loyalty` | `customers` | Stamp/points loyalty programs, wallet integration | 4 |
| `marketplace` | `inventory` | Letzshop integration | 3 |
| `messaging` | - | Messages, notifications | 3 |
| `orders` | `payments` | Order management, customer checkout | 4 |
| `payments` | - | Payment gateway integrations (Stripe, PayPal, etc.) | 3 |
### Internal Modules (2)
Internal modules are **admin-only tools** not exposed to customers or vendors.
| Module | Description |
|--------|-------------|
| `dev-tools` | Component library, icon browser |
| `monitoring` | Logs, background tasks, Flower, Grafana integration |
## Self-Contained Module Structure
Every module follows this standardized structure:
```
app/modules/analytics/
├── __init__.py # Module package marker
├── definition.py # ModuleDefinition (REQUIRED for auto-discovery)
├── config.py # Environment config (auto-discovered)
├── exceptions.py # Module-specific exceptions
├── routes/
│ ├── __init__.py
│ ├── api/ # API endpoints (auto-discovered)
│ │ ├── __init__.py
│ │ ├── admin.py # Must export: router = APIRouter()
│ │ └── vendor.py # Must export: router = APIRouter()
│ └── pages/ # HTML page routes (auto-discovered)
│ ├── __init__.py
│ └── vendor.py # Must export: router = APIRouter()
├── services/
│ ├── __init__.py
│ └── stats_service.py
├── models/
│ ├── __init__.py
│ └── report.py
├── schemas/
│ ├── __init__.py
│ └── stats.py
├── templates/ # Auto-discovered by Jinja2
│ └── analytics/
│ └── vendor/
│ └── analytics.html
├── static/ # Auto-mounted at /static/modules/analytics/
│ ├── admin/js/ # Admin-facing JS for this module
│ ├── vendor/js/ # Vendor-facing JS for this module
│ │ └── analytics.js
│ └── shared/js/ # Shared JS (used by both admin and vendor)
├── locales/ # Auto-loaded translations
│ ├── en.json
│ ├── de.json
│ ├── fr.json
│ └── lu.json
├── tasks/ # Auto-discovered by Celery
│ ├── __init__.py # REQUIRED for Celery discovery
│ └── reports.py
└── migrations/ # Auto-discovered by Alembic
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
└── analytics_001_create_reports.py
```
## Module Definition
Each module must have a `definition.py` with a `ModuleDefinition` instance:
```python
# app/modules/analytics/definition.py
from app.modules.base import ModuleDefinition, PermissionDefinition
from app.modules.enums import FrontendType
analytics_module = ModuleDefinition(
# Identity
code="analytics",
name="Analytics & Reporting",
description="Dashboard analytics, custom reports, and data exports.",
version="1.0.0",
# Classification (determines tier)
is_core=False, # Set True for core modules
is_internal=False, # Set True for admin-only modules
# Dependencies
requires=[], # List other module codes this depends on
# Features (for tier-based gating)
features=[
"basic_reports",
"analytics_dashboard",
"custom_reports",
],
# Module-driven permissions (RBAC)
permissions=[
PermissionDefinition(
id="analytics.view",
label_key="analytics.permissions.view",
description_key="analytics.permissions.view_desc",
category="analytics",
),
PermissionDefinition(
id="analytics.export",
label_key="analytics.permissions.export",
description_key="analytics.permissions.export_desc",
category="analytics",
),
],
# Menu items per frontend
menu_items={
FrontendType.ADMIN: [], # Analytics uses dashboard
FrontendType.VENDOR: ["analytics"],
},
# Self-contained module configuration
is_self_contained=True,
services_path="app.modules.analytics.services",
models_path="app.modules.analytics.models",
schemas_path="app.modules.analytics.schemas",
exceptions_path="app.modules.analytics.exceptions",
templates_path="templates",
locales_path="locales",
)
```
### ModuleDefinition Fields
| Field | Type | Description |
|-------|------|-------------|
| `code` | `str` | Unique identifier (e.g., "billing") |
| `name` | `str` | Display name |
| `description` | `str` | What the module provides |
| `version` | `str` | Semantic version (default: "1.0.0") |
| `requires` | `list[str]` | Module codes this depends on |
| `features` | `list[str]` | Feature codes for tier gating |
| `permissions` | `list[PermissionDefinition]` | RBAC permission definitions |
| `menu_items` | `dict` | Menu items per frontend type |
| `is_core` | `bool` | Cannot be disabled if True |
| `is_internal` | `bool` | Admin-only if True |
| `is_self_contained` | `bool` | Uses self-contained structure |
## Route Auto-Discovery
Routes in `routes/api/` and `routes/pages/` are automatically discovered and registered.
### API Routes (`routes/api/vendor.py`)
```python
# app/modules/analytics/routes/api/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import get_current_vendor_api, get_db
router = APIRouter() # MUST be named 'router' for auto-discovery
@router.get("")
def get_analytics(
current_user = Depends(get_current_vendor_api),
db = Depends(get_db),
):
"""Get vendor analytics."""
pass
```
**Auto-registered at:** `/api/v1/vendor/analytics`
### Page Routes (`routes/pages/vendor.py`)
```python
# app/modules/analytics/routes/pages/vendor.py
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
router = APIRouter() # MUST be named 'router' for auto-discovery
@router.get("/{vendor_code}/analytics", response_class=HTMLResponse)
async def analytics_page(request: Request, vendor_code: str):
"""Render analytics page."""
pass
```
**Auto-registered at:** `/vendor/{vendor_code}/analytics`
## Framework Layer
The Framework Layer provides infrastructure that modules depend on. These are **not modules** - they're always available and cannot be disabled.
| Component | Location | Purpose |
|-----------|----------|---------|
| Config | `app/core/config.py` | Settings management |
| Database | `app/core/database.py` | SQLAlchemy sessions |
| Logging | `app/core/logging.py` | Structured logging |
| Permissions | `app/core/permissions.py` | RBAC definitions |
| Feature Gate | `app/core/feature_gate.py` | Tier-based access |
| Celery | `app/core/celery_config.py` | Task queue |
| Observability | `app/core/observability.py` | Health checks, metrics, Sentry |
| Auth Middleware | `middleware/auth.py` | JWT authentication |
| Context Middleware | `middleware/platform_context.py` | Multi-tenancy |
| Dependencies | `app/api/deps.py` | FastAPI DI |
| Base Exceptions | `app/exceptions/base.py` | Exception hierarchy |
## Module Dependencies
Modules can depend on other modules. When enabling a module, its dependencies are automatically enabled.
```
payments
↙ ↘
billing orders ←──┐
↑ │
inventory │ │
↓ cart │
marketplace ↘ │
checkout
```
**Dependency Rules:**
1. Core modules cannot depend on optional modules
2. Enabling a module auto-enables its dependencies
3. Disabling a module auto-disables modules that depend on it
4. Circular dependencies are not allowed
## Module Registry
The registry auto-discovers all modules:
```python
from app.modules.registry import (
MODULES, # All modules (auto-discovered)
CORE_MODULES, # Core only
OPTIONAL_MODULES, # Optional only
INTERNAL_MODULES, # Internal only
get_module,
get_core_module_codes,
get_module_tier,
)
# Get a specific module
billing = get_module("billing")
# Check module tier
tier = get_module_tier("billing") # Returns "optional"
# Get all core module codes
core_codes = get_core_module_codes() # {"core", "tenancy", "cms", "customers"}
```
## Module Service
The `ModuleService` manages module enablement per platform:
```python
from app.modules.service import module_service
# Check if module is enabled
if module_service.is_module_enabled(db, platform_id, "billing"):
pass
# Get all enabled modules for a platform
modules = module_service.get_platform_modules(db, platform_id)
# Enable a module (auto-enables dependencies)
module_service.enable_module(db, platform_id, "billing", user_id=current_user.id)
# Disable a module (auto-disables dependents)
module_service.disable_module(db, platform_id, "billing", user_id=current_user.id)
```
## Module Static Files
Each module can have its own static assets (JavaScript, CSS, images) in the `static/` directory. These are automatically mounted at `/static/modules/{module_name}/`.
### Static File Structure
```
app/modules/{module}/static/
├── admin/js/ # Admin pages for this module
├── vendor/js/ # Vendor pages for this module
├── shared/js/ # Shared across admin/vendor (e.g., feature-store.js)
└── shop/js/ # Shop pages (if module has storefront UI)
```
### Referencing in Templates
Use the `{module}_static` URL name:
```html
<!-- Module-specific JS -->
<script src="{{ url_for('orders_static', path='vendor/js/orders.js') }}"></script>
<script src="{{ url_for('billing_static', path='shared/js/feature-store.js') }}"></script>
```
### Module vs. Platform Static Files
| Put in Module | Put in Platform (`static/`) |
|---------------|----------------------------|
| Module-specific features | Platform-level admin (dashboard, login, platforms, vendors) |
| Order management → `orders` module | Vendor core (profile, settings, team) |
| Product catalog → `catalog` module | Shared utilities (api-client, utils, icons) |
| Billing/subscriptions → `billing` module | Admin user management |
| Analytics dashboards → `analytics` module | Platform user management |
**Key distinction:** Platform users (admin-users.js, users.js) manage internal platform access. Shop customers (customers.js in customers module) are end-users who purchase from vendors.
See [Frontend Structure](frontend-structure.md) for detailed JS file organization.
## Module Configuration
Modules can have environment-based configuration using Pydantic Settings. The `config.py` file is auto-discovered by `app/modules/config.py`.
```python
# app/modules/marketplace/config.py
from pydantic import Field
from pydantic_settings import BaseSettings
class MarketplaceConfig(BaseSettings):
"""Configuration for marketplace module."""
# Settings loaded from env vars with MARKETPLACE_ prefix
api_timeout: int = Field(default=30, description="API timeout in seconds")
batch_size: int = Field(default=100, description="Import batch size")
max_retries: int = Field(default=3, description="Max retry attempts")
model_config = {"env_prefix": "MARKETPLACE_"}
# Export for auto-discovery
config_class = MarketplaceConfig
config = MarketplaceConfig()
```
**Usage:**
```python
# Direct import
from app.modules.marketplace.config import config
timeout = config.api_timeout
# Via discovery
from app.modules.config import get_module_config
config = get_module_config("marketplace")
```
**Environment variables:**
```bash
MARKETPLACE_API_TIMEOUT=60
MARKETPLACE_BATCH_SIZE=500
```
## Module Migrations
Each module owns its database migrations in the `migrations/versions/` directory. Alembic auto-discovers these via `app/modules/migrations.py`.
### Migration Structure
```
app/modules/cms/migrations/
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
├── cms_001_create_content_pages.py
├── cms_002_add_sections.py
└── cms_003_add_media_library.py
```
### Naming Convention
```
{module_code}_{sequence}_{description}.py
```
### Migration Template
```python
# app/modules/cms/migrations/versions/cms_001_create_content_pages.py
"""Create content_pages table.
Revision ID: cms_001
Create Date: 2026-01-28
"""
from alembic import op
import sqlalchemy as sa
revision = "cms_001"
down_revision = None
branch_labels = ("cms",) # Module-specific branch
def upgrade() -> None:
op.create_table(
"content_pages",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
sa.Column("slug", sa.String(100), nullable=False),
sa.Column("title", sa.String(200), nullable=False),
)
def downgrade() -> None:
op.drop_table("content_pages")
```
### Running Migrations
Module migrations are automatically discovered:
```bash
# Run all migrations (core + modules)
alembic upgrade head
# View migration history
alembic history
```
### Current State
Currently, all migrations reside in central `alembic/versions/`. The module-specific directories are in place for:
- **New modules**: Should create migrations in their own `migrations/versions/`
- **Future reorganization**: Existing migrations will be moved to modules pre-production
## Entity Auto-Discovery Reference
This section details the auto-discovery requirements for each entity type. **All entities must be in modules** - legacy locations are deprecated and will trigger architecture validation errors.
### Routes
Routes define API and page endpoints. They are auto-discovered from module directories.
| Type | Location | Discovery | Router Name |
|------|----------|-----------|-------------|
| Admin API | `routes/api/admin.py` | `app/modules/routes.py` | `admin_router` |
| Vendor API | `routes/api/vendor.py` | `app/modules/routes.py` | `vendor_router` |
| Storefront API | `routes/api/storefront.py` | `app/modules/routes.py` | `router` |
| Admin Pages | `routes/pages/admin.py` | `app/modules/routes.py` | `admin_router` |
| Vendor Pages | `routes/pages/vendor.py` | `app/modules/routes.py` | `vendor_router` |
**Structure:**
```
app/modules/{module}/routes/
├── __init__.py
├── api/
│ ├── __init__.py
│ ├── admin.py # Must export admin_router
│ ├── vendor.py # Must export vendor_router
│ ├── storefront.py # Must export router (public storefront)
│ └── admin_{feature}.py # Sub-routers aggregated in admin.py
└── pages/
├── __init__.py
└── vendor.py # Must export vendor_router
```
**Example - Aggregating Sub-Routers:**
```python
# app/modules/billing/routes/api/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
vendor_router = APIRouter(
prefix="/billing",
dependencies=[Depends(require_module_access("billing"))],
)
# Aggregate sub-routers
from .vendor_checkout import vendor_checkout_router
from .vendor_usage import vendor_usage_router
vendor_router.include_router(vendor_checkout_router)
vendor_router.include_router(vendor_usage_router)
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/api/v1/vendor/*.py` - Move to module `routes/api/vendor.py`
- `app/api/v1/admin/*.py` - Move to module `routes/api/admin.py`
---
### Services
Services contain business logic. They are not auto-discovered but should be in modules for organization.
| Location | Import Pattern |
|----------|----------------|
| `services/*.py` | `from app.modules.{module}.services import service_name` |
| `services/__init__.py` | Re-exports all public services |
**Structure:**
```
app/modules/{module}/services/
├── __init__.py # Re-exports: from .order_service import order_service
├── order_service.py # OrderService class + order_service singleton
└── fulfillment_service.py # Related services
```
**Example:**
```python
# app/modules/orders/services/order_service.py
from sqlalchemy.orm import Session
from app.modules.orders.models import Order
class OrderService:
def get_order(self, db: Session, order_id: int) -> Order:
return db.query(Order).filter(Order.id == order_id).first()
order_service = OrderService()
# app/modules/orders/services/__init__.py
from .order_service import order_service, OrderService
__all__ = ["order_service", "OrderService"]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/services/*.py` - Move to module `services/`
- `app/services/{module}/` - Move to `app/modules/{module}/services/`
---
### Models
Database models (SQLAlchemy). Currently in `models/database/`, migrating to modules.
| Location | Base Class | Discovery |
|----------|------------|-----------|
| `models/*.py` | `Base` from `models.base` | Alembic autogenerate |
**Structure:**
```
app/modules/{module}/models/
├── __init__.py # Re-exports: from .order import Order, OrderItem
├── order.py # Order model
└── order_item.py # Related models
```
**Example:**
```python
# app/modules/orders/models/order.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from models.base import Base, TimestampMixin
class Order(Base, TimestampMixin):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
status = Column(String(50), default="pending")
items = relationship("OrderItem", back_populates="order")
```
**Legacy Locations (being migrated):**
- `models/database/*.py` - Core models remain here, domain models move to modules
---
### Schemas
Pydantic schemas for request/response validation.
| Location | Base Class | Usage |
|----------|------------|-------|
| `schemas/*.py` | `BaseModel` from Pydantic | API routes, validation |
**Structure:**
```
app/modules/{module}/schemas/
├── __init__.py # Re-exports all schemas
├── order.py # Order request/response schemas
└── order_item.py # Related schemas
```
**Example:**
```python
# app/modules/orders/schemas/order.py
from pydantic import BaseModel
from datetime import datetime
class OrderResponse(BaseModel):
id: int
vendor_id: int
status: str
created_at: datetime
class Config:
from_attributes = True
class OrderCreateRequest(BaseModel):
customer_id: int
items: list[OrderItemRequest]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `models/schema/*.py` - Move to module `schemas/`
---
### Tasks (Celery)
Background tasks are auto-discovered by Celery from module `tasks/` directories.
| Location | Discovery | Registration |
|----------|-----------|--------------|
| `tasks/*.py` | `app/modules/tasks.py` | Celery autodiscover |
**Structure:**
```
app/modules/{module}/tasks/
├── __init__.py # REQUIRED - imports task functions
├── import_tasks.py # Task definitions
└── export_tasks.py # Related tasks
```
**Example:**
```python
# app/modules/marketplace/tasks/import_tasks.py
from celery import shared_task
from app.core.database import SessionLocal
@shared_task(bind=True)
def process_import(self, job_id: int, vendor_id: int):
db = SessionLocal()
try:
# Process import
pass
finally:
db.close()
# app/modules/marketplace/tasks/__init__.py
from .import_tasks import process_import
from .export_tasks import export_products
__all__ = ["process_import", "export_products"]
```
**Legacy Locations (DEPRECATED - will cause errors):**
- `app/tasks/*.py` - Move to module `tasks/`
---
### Exceptions
Module-specific exceptions inherit from `WizamartException`.
| Location | Base Class | Usage |
|----------|------------|-------|
| `exceptions.py` | `WizamartException` | Domain errors |
**Structure:**
```
app/modules/{module}/
└── exceptions.py # All module exceptions
```
**Example:**
```python
# app/modules/orders/exceptions.py
from app.exceptions import WizamartException
class OrderException(WizamartException):
"""Base exception for orders module."""
pass
class OrderNotFoundError(OrderException):
"""Order not found."""
def __init__(self, order_id: int):
super().__init__(f"Order {order_id} not found")
self.order_id = order_id
class OrderAlreadyFulfilledError(OrderException):
"""Order has already been fulfilled."""
pass
```
---
### Templates
Jinja2 templates are auto-discovered from module `templates/` directories. The template loader searches `app/templates/` first (for shared templates), then each module's `templates/` directory.
| Location | URL Pattern | Discovery |
|----------|-------------|-----------|
| `templates/{module}/vendor/*.html` | `/vendor/{vendor}/...` | Jinja2 loader |
| `templates/{module}/admin/*.html` | `/admin/...` | Jinja2 loader |
| `templates/{module}/storefront/*.html` | `/storefront/...` | Jinja2 loader |
| `templates/{module}/public/*.html` | `/...` (platform pages) | Jinja2 loader |
**Module Template Structure:**
```
app/modules/{module}/templates/
└── {module}/
├── admin/
│ ├── list.html
│ └── partials/ # Module-specific partials
│ └── my-partial.html
├── vendor/
│ ├── index.html
│ └── detail.html
├── storefront/ # Customer-facing shop pages
│ └── products.html
└── public/ # Platform marketing pages
└── pricing.html
```
**Template Reference:**
```python
# In route
return templates.TemplateResponse(
request=request,
name="{module}/vendor/index.html",
context={"items": items}
)
```
**Shared Templates (in `app/templates/`):**
Some templates remain in `app/templates/` because they are used across all modules:
| Directory | Contents | Purpose |
|-----------|----------|---------|
| `admin/base.html` | Admin layout | Parent template all admin pages extend |
| `vendor/base.html` | Vendor layout | Parent template all vendor pages extend |
| `storefront/base.html` | Shop layout | Parent template all storefront pages extend |
| `platform/base.html` | Public layout | Parent template all public pages extend |
| `admin/errors/` | Error pages | HTTP error templates (404, 500, etc.) |
| `vendor/errors/` | Error pages | HTTP error templates for vendor |
| `storefront/errors/` | Error pages | HTTP error templates for storefront |
| `admin/partials/` | Shared partials | Header, sidebar used across admin |
| `vendor/partials/` | Shared partials | Header, sidebar used across vendor |
| `shared/macros/` | Jinja2 macros | Reusable UI components (buttons, forms, tables) |
| `shared/includes/` | Includes | Common HTML snippets |
| `invoices/` | PDF templates | Invoice PDF generation |
These shared templates provide the "framework" that module templates build upon. Module templates extend base layouts and import shared macros.
---
### Static Files
JavaScript, CSS, and images are auto-mounted from module `static/` directories.
| Location | URL | Discovery |
|----------|-----|-----------|
| `static/vendor/js/*.js` | `/static/modules/{module}/vendor/js/*.js` | `main.py` |
| `static/admin/js/*.js` | `/static/modules/{module}/admin/js/*.js` | `main.py` |
**Structure:**
```
app/modules/{module}/static/
├── vendor/js/
│ └── {module}.js
├── admin/js/
│ └── {module}.js
└── shared/js/
└── common.js
```
**Template Reference:**
```html
<script src="{{ url_for('{module}_static', path='vendor/js/{module}.js') }}"></script>
```
---
### Locales (i18n)
Translation files are auto-discovered from module `locales/` directories.
| Location | Format | Discovery |
|----------|--------|-----------|
| `locales/*.json` | JSON key-value | `app/utils/i18n.py` |
**Structure:**
```
app/modules/{module}/locales/
├── en.json
├── de.json
├── fr.json
└── lb.json
```
**Example:**
```json
{
"orders.title": "Orders",
"orders.status.pending": "Pending",
"orders.status.fulfilled": "Fulfilled"
}
```
**Usage:**
```python
from app.utils.i18n import t
message = t("orders.title", locale="en") # "Orders"
```
---
### Configuration
Module-specific environment configuration.
| Location | Base Class | Discovery |
|----------|------------|-----------|
| `config.py` | `BaseSettings` | `app/modules/config.py` |
**Example:**
```python
# app/modules/marketplace/config.py
from pydantic_settings import BaseSettings
class MarketplaceConfig(BaseSettings):
api_timeout: int = 30
batch_size: int = 100
model_config = {"env_prefix": "MARKETPLACE_"}
config = MarketplaceConfig()
```
**Environment Variables:**
```bash
MARKETPLACE_API_TIMEOUT=60
MARKETPLACE_BATCH_SIZE=500
```
## Architecture Validation Rules
The architecture validator (`scripts/validate_architecture.py`) enforces module structure:
| Rule | Severity | Description |
|------|----------|-------------|
| MOD-001 | ERROR | Self-contained modules must have required directories |
| MOD-002 | WARNING | Services must contain actual code, not re-exports |
| MOD-003 | WARNING | Schemas must contain actual code, not re-exports |
| MOD-004 | WARNING | Routes must import from module, not legacy locations |
| MOD-005 | WARNING | Modules with UI must have templates and static |
| MOD-006 | INFO | Modules should have locales for i18n |
| MOD-007 | ERROR | Definition paths must match directory structure |
| MOD-008 | WARNING | Self-contained modules must have exceptions.py |
| MOD-009 | ERROR | Modules must have definition.py for auto-discovery |
| MOD-010 | WARNING | Route files must export `router` variable |
| MOD-011 | WARNING | Tasks directory must have `__init__.py` |
| MOD-012 | INFO | Locales should have all language files |
| MOD-013 | INFO | config.py should export `config` or `config_class` |
| MOD-014 | WARNING | Migrations must follow naming convention |
| MOD-015 | WARNING | Migrations directory must have `__init__.py` files |
| MOD-016 | ERROR | Routes must be in modules, not `app/api/v1/` |
| MOD-017 | ERROR | Services must be in modules, not `app/services/` |
| MOD-018 | ERROR | Tasks must be in modules, not `app/tasks/` |
| MOD-019 | ERROR | Schemas must be in modules, not `models/schema/` |
| MOD-020 | WARNING | Module definition must have required attributes (code, name, description, version, features) |
| MOD-021 | WARNING | Modules with menus should have features defined |
| MOD-022 | INFO | Feature modules should have permissions (unless internal or storefront-only) |
| MOD-023 | INFO | Modules with routers should use `get_*_with_routers` pattern |
Run validation:
```bash
python scripts/validate_architecture.py
```
## Best Practices
### Do
- Keep modules focused on a single domain
- Use `requires` for hard dependencies
- Provide `health_check` for critical modules
- Use events for cross-module communication
- Follow the standard directory structure
- Export `router` variable in route files
- Include all supported languages in locales
### Don't
- Create circular dependencies
- Make core modules depend on optional modules
- Put framework-level code in modules
- Skip migration naming conventions
- Forget `__init__.py` in tasks directory
- Manually register modules in registry.py (use auto-discovery)
## Related Documentation
- [Creating Modules](../development/creating-modules.md) - Step-by-step guide
- [Menu Management](menu-management.md) - Sidebar configuration
- [Observability](observability.md) - Health checks integration
- [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access