feat: add module config and migrations auto-discovery infrastructure

Add self-contained configuration and migrations support for modules:

Config auto-discovery (app/modules/config.py):
- Modules can have config.py with Pydantic Settings
- Environment variables prefixed with MODULE_NAME_
- Auto-discovered via get_module_config()

Migrations auto-discovery:
- Each module has migrations/versions/ directory
- Alembic discovers module migrations automatically
- Naming convention: {module}_{seq}_{description}.py

New architecture rules (MOD-013 to MOD-015):
- MOD-013: config.py should export config/config_class
- MOD-014: Migrations must follow naming convention
- MOD-015: Migrations directory must have __init__.py

Created for all 11 self-contained modules:
- config.py placeholder files
- migrations/ directories with __init__.py files

Added core and tenancy module definitions for completeness.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 22:19:41 +01:00
parent eb47daec8b
commit 2466dfd7ed
43 changed files with 2338 additions and 581 deletions

View File

@@ -1,305 +1,437 @@
# Creating Modules
This guide explains how to create new modules for the Wizamart platform, including both simple wrapper modules and self-contained modules with their own services, models, and migrations.
This guide explains how to create new **plug-and-play modules** for the Wizamart platform. Modules are fully self-contained and automatically discovered - no changes to framework files required.
## Module Types
## Quick Start (5 Minutes)
| Type | Complexity | Use Case |
|------|------------|----------|
| **Wrapper Module** | Simple | Groups existing routes and features under a toggleable module |
| **Self-Contained Module** | Complex | Full isolation with own services, models, templates, migrations |
Creating a new module requires **zero changes** to `main.py`, `registry.py`, or any other framework file.
## Quick Start: Wrapper Module
### Step 1: Create Directory Structure
For a simple module that wraps existing functionality:
```bash
# Create module with all directories
MODULE_NAME=mymodule
mkdir -p app/modules/$MODULE_NAME/{routes/{api,pages},services,models,schemas,templates/$MODULE_NAME/vendor,static/vendor/js,locales,tasks}
# Create required files
touch app/modules/$MODULE_NAME/__init__.py
touch app/modules/$MODULE_NAME/definition.py
touch app/modules/$MODULE_NAME/exceptions.py
touch app/modules/$MODULE_NAME/routes/__init__.py
touch app/modules/$MODULE_NAME/routes/api/__init__.py
touch app/modules/$MODULE_NAME/routes/pages/__init__.py
touch app/modules/$MODULE_NAME/services/__init__.py
touch app/modules/$MODULE_NAME/models/__init__.py
touch app/modules/$MODULE_NAME/schemas/__init__.py
touch app/modules/$MODULE_NAME/tasks/__init__.py
```
### Step 2: Create Module Definition
```python
# app/modules/analytics/definition.py
# app/modules/mymodule/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
analytics_module = ModuleDefinition(
code="analytics",
name="Analytics & Reports",
description="Business analytics, reports, and dashboards",
mymodule_module = ModuleDefinition(
code="mymodule",
name="My Module",
description="Description of what this module does",
version="1.0.0",
features=[
"analytics_dashboard",
"sales_reports",
"customer_insights",
],
# Classification
is_core=False, # True = always enabled
is_internal=False, # True = admin-only
is_self_contained=True,
# Features for tier-based gating
features=["mymodule_feature"],
# Menu items
menu_items={
FrontendType.ADMIN: ["analytics-dashboard", "reports"],
FrontendType.VENDOR: ["analytics", "sales-reports"],
FrontendType.VENDOR: ["mymodule"],
},
is_core=False,
is_internal=False,
# Paths (for self-contained modules)
services_path="app.modules.mymodule.services",
models_path="app.modules.mymodule.models",
schemas_path="app.modules.mymodule.schemas",
exceptions_path="app.modules.mymodule.exceptions",
templates_path="templates",
locales_path="locales",
)
```
## Module Definition Fields
### Step 3: Create Routes (Auto-Discovered)
### Required Fields
```python
# app/modules/mymodule/routes/api/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import get_current_vendor_api, get_db
| Field | Type | Description |
|-------|------|-------------|
| `code` | str | Unique identifier (e.g., "billing", "analytics") |
| `name` | str | Display name (e.g., "Billing & Subscriptions") |
router = APIRouter() # MUST be named 'router'
### Optional Fields
@router.get("")
def get_mymodule_data(current_user=Depends(get_current_vendor_api)):
return {"message": "Hello from mymodule"}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `description` | str | "" | What the module provides |
| `version` | str | "1.0.0" | Semantic version |
| `requires` | list[str] | [] | Module codes this depends on |
| `features` | list[str] | [] | Feature codes for tier gating |
| `menu_items` | dict | {} | Menu items per frontend type |
| `permissions` | list[str] | [] | Permission codes defined |
| `is_core` | bool | False | Cannot be disabled if True |
| `is_internal` | bool | False | Admin-only if True |
```python
# app/modules/mymodule/routes/pages/vendor.py
from fastapi import APIRouter, Request, Path
from fastapi.responses import HTMLResponse
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db, Depends
from app.templates_config import templates
### Configuration Fields
router = APIRouter() # MUST be named 'router'
| Field | Type | Description |
|-------|------|-------------|
| `config_schema` | type[BaseModel] | Pydantic model for validation |
| `default_config` | dict | Default configuration values |
@router.get("/{vendor_code}/mymodule", response_class=HTMLResponse)
async def mymodule_page(
request: Request,
vendor_code: str = Path(...),
current_user=Depends(get_current_vendor_from_cookie_or_header),
db=Depends(get_db),
):
return templates.TemplateResponse(
"mymodule/vendor/index.html",
{"request": request, "vendor_code": vendor_code},
)
```
### Lifecycle Hooks
### Step 4: Done!
| Field | Signature | Description |
|-------|-----------|-------------|
| `on_enable` | (platform_id: int) -> None | Called when enabled |
| `on_disable` | (platform_id: int) -> None | Called when disabled |
| `on_startup` | () -> None | Called on app startup |
| `health_check` | () -> dict | Returns health status |
That's it! The framework automatically:
- Discovers and registers the module
- Mounts API routes at `/api/v1/vendor/mymodule`
- Mounts page routes at `/vendor/{code}/mymodule`
- Loads templates from `templates/`
- Mounts static files at `/static/modules/mymodule/`
- Loads translations from `locales/`
## Complete Module Structure
```
app/modules/mymodule/
├── __init__.py # Package marker
├── definition.py # ModuleDefinition (REQUIRED)
├── config.py # Environment config (optional, auto-discovered)
├── exceptions.py # Module exceptions
├── routes/ # Auto-discovered routes
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── admin.py # /api/v1/admin/mymodule
│ │ └── vendor.py # /api/v1/vendor/mymodule
│ └── pages/
│ ├── __init__.py
│ ├── admin.py # /admin/mymodule
│ └── vendor.py # /vendor/{code}/mymodule
├── services/ # Business logic
│ ├── __init__.py
│ └── mymodule_service.py
├── models/ # SQLAlchemy models
│ ├── __init__.py
│ └── mymodel.py
├── schemas/ # Pydantic schemas
│ ├── __init__.py
│ └── myschema.py
├── templates/ # Jinja2 templates (auto-loaded)
│ └── mymodule/
│ ├── admin/
│ │ └── index.html
│ └── vendor/
│ └── index.html
├── static/ # Static files (auto-mounted)
│ ├── admin/
│ │ └── js/
│ │ └── mymodule.js
│ └── vendor/
│ └── js/
│ └── mymodule.js
├── locales/ # Translations (auto-loaded)
│ ├── en.json
│ ├── de.json
│ ├── fr.json
│ └── lu.json
├── tasks/ # Celery tasks (auto-discovered)
│ ├── __init__.py # REQUIRED for discovery
│ └── background_tasks.py
└── migrations/ # Alembic migrations (auto-discovered)
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
└── mymodule_001_initial.py
```
## Auto-Discovery Details
### What Gets Auto-Discovered
| Component | Location | Discovered By | Notes |
|-----------|----------|---------------|-------|
| Module Definition | `definition.py` | `app/modules/discovery.py` | Must export ModuleDefinition |
| Configuration | `config.py` | `app/modules/config.py` | Must export `config` or `config_class` |
| API Routes | `routes/api/*.py` | `app/modules/routes.py` | Must export `router` |
| Page Routes | `routes/pages/*.py` | `app/modules/routes.py` | Must export `router` |
| Templates | `templates/` | `app/templates_config.py` | Use namespace prefix |
| Static Files | `static/` | `main.py` | Mounted at `/static/modules/{code}/` |
| Locales | `locales/*.json` | `app/utils/i18n.py` | Keyed by module code |
| Tasks | `tasks/` | `app/modules/tasks.py` | Needs `__init__.py` |
| Migrations | `migrations/versions/` | `app/modules/migrations.py` | Use naming convention |
### Route File Requirements
Route files MUST export a `router` variable:
```python
# CORRECT - will be discovered
router = APIRouter()
@router.get("/")
def my_endpoint():
pass
# WRONG - won't be discovered
my_router = APIRouter() # Variable not named 'router'
```
### Template Namespacing
Templates must be namespaced under the module code:
```
templates/
└── mymodule/ # Module code as namespace
└── vendor/
└── index.html
```
Reference in code:
```python
templates.TemplateResponse("mymodule/vendor/index.html", {...})
```
### Static File URLs
Static files are mounted at `/static/modules/{module_code}/`:
```html
<!-- In template -->
<script src="{{ url_for('mymodule_static', path='vendor/js/mymodule.js') }}"></script>
```
### Translation Keys
Translations are namespaced under the module code:
```json
// locales/en.json
{
"mymodule": {
"title": "My Module",
"description": "Module description"
}
}
```
Use in templates:
```html
{{ _('mymodule.title') }}
```
## Module Classification
### Core Modules
Cannot be disabled. Use for essential platform functionality.
Always enabled, cannot be disabled.
```python
core_module = ModuleDefinition(
code="tenancy",
name="Platform Tenancy",
is_core=True, # Cannot be disabled
ModuleDefinition(
code="mymodule",
is_core=True,
# ...
)
```
### Optional Modules
### Optional Modules (Default)
Can be enabled/disabled per platform.
```python
billing_module = ModuleDefinition(
code="billing",
name="Billing",
is_core=False, # Can be toggled
ModuleDefinition(
code="mymodule",
is_core=False,
is_internal=False,
# ...
)
```
### Internal Modules
Admin-only tools not visible to customers/vendors.
Admin-only tools, not visible to vendors.
```python
devtools_module = ModuleDefinition(
code="dev-tools",
name="Developer Tools",
is_internal=True, # Admin-only
ModuleDefinition(
code="mymodule",
is_internal=True,
# ...
)
```
## Module Dependencies
Declare dependencies using the `requires` field:
Declare dependencies in the definition:
```python
# orders module requires payments
orders_module = ModuleDefinition(
ModuleDefinition(
code="orders",
name="Orders",
requires=["payments"], # Must have payments enabled
# ...
)
```
**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
**Rules:**
- Enabling a module auto-enables its dependencies
- Disabling a module auto-disables modules that depend on it
- Core modules cannot depend on optional modules
## Self-Contained Module Structure
For modules with their own services, models, and templates:
```
app/modules/cms/
├── __init__.py
├── definition.py # ModuleDefinition
├── config.py # Configuration schema (optional)
├── exceptions.py # Module-specific exceptions
├── routes/
│ ├── __init__.py
│ ├── admin.py # Admin API routes
│ └── vendor.py # Vendor API routes
├── services/
│ ├── __init__.py
│ └── content_service.py
├── models/
│ ├── __init__.py
│ └── content_page.py
├── schemas/
│ ├── __init__.py
│ └── content.py
├── templates/
│ ├── admin/
│ └── vendor/
├── migrations/
│ └── versions/
│ ├── cms_001_create_content_pages.py
│ └── cms_002_add_seo_fields.py
└── locales/
├── en.json
└── fr.json
```
### Self-Contained Definition
## Services Pattern
```python
# app/modules/cms/definition.py
from pydantic import BaseModel, Field
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
class CMSConfig(BaseModel):
"""CMS module configuration."""
max_pages_per_vendor: int = Field(default=100, ge=1, le=1000)
enable_seo: bool = True
default_language: str = "en"
cms_module = ModuleDefinition(
# Identity
code="cms",
name="Content Management",
description="Content pages, media library, and themes",
version="1.0.0",
# Classification
is_core=True,
is_self_contained=True,
# Features
features=[
"content_pages",
"media_library",
"vendor_themes",
],
# Menu items
menu_items={
FrontendType.ADMIN: ["content-pages", "vendor-themes"],
FrontendType.VENDOR: ["content-pages", "media"],
},
# Configuration
config_schema=CMSConfig,
default_config={
"max_pages_per_vendor": 100,
"enable_seo": True,
},
# Self-contained paths
services_path="app.modules.cms.services",
models_path="app.modules.cms.models",
schemas_path="app.modules.cms.schemas",
templates_path="templates",
exceptions_path="app.modules.cms.exceptions",
locales_path="locales",
migrations_path="migrations",
# Lifecycle
health_check=lambda: {"status": "healthy"},
)
```
## Creating Module Routes
### Admin Routes
```python
# app/modules/payments/routes/admin.py
from fastapi import APIRouter, Depends
# app/modules/mymodule/services/mymodule_service.py
from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_admin_user
admin_router = APIRouter(prefix="/api/admin/payments", tags=["Admin - Payments"])
class MyModuleService:
def get_data(self, db: Session, vendor_id: int):
# Business logic here
pass
@admin_router.get("/gateways")
async def list_gateways(
db: Session = Depends(get_db),
current_user = Depends(get_current_admin_user),
):
"""List configured payment gateways."""
# Implementation
pass
# Singleton instance
mymodule_service = MyModuleService()
```
### Vendor Routes
```python
# app/modules/payments/routes/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import get_db, get_current_vendor_user
vendor_router = APIRouter(prefix="/api/vendor/payments", tags=["Vendor - Payments"])
@vendor_router.get("/methods")
async def list_payment_methods(
db: Session = Depends(get_db),
current_user = Depends(get_current_vendor_user),
):
"""List vendor's stored payment methods."""
pass
```
### Lazy Router Loading
To avoid circular imports, use lazy loading:
```python
# app/modules/payments/definition.py
def _get_admin_router():
"""Lazy import to avoid circular imports."""
from app.modules.payments.routes.admin import admin_router
return admin_router
def _get_vendor_router():
from app.modules.payments.routes.vendor import vendor_router
return vendor_router
payments_module = ModuleDefinition(
code="payments",
# ...
# app/modules/mymodule/services/__init__.py
from app.modules.mymodule.services.mymodule_service import (
mymodule_service,
MyModuleService,
)
def get_payments_module_with_routers():
"""Get module with routers attached."""
payments_module.admin_router = _get_admin_router()
payments_module.vendor_router = _get_vendor_router()
return payments_module
__all__ = ["mymodule_service", "MyModuleService"]
```
## Module Migrations
## Exceptions Pattern
```python
# app/modules/mymodule/exceptions.py
from app.exceptions import WizamartException
class MyModuleException(WizamartException):
"""Base exception for mymodule."""
pass
class MyModuleNotFoundError(MyModuleException):
"""Resource not found."""
def __init__(self, resource_id: int):
super().__init__(f"Resource {resource_id} not found")
```
## Configuration Pattern
Modules can have their own environment-based configuration using Pydantic Settings.
```python
# app/modules/mymodule/config.py
from pydantic import Field
from pydantic_settings import BaseSettings
class MyModuleConfig(BaseSettings):
"""Configuration for mymodule."""
# Settings are loaded from environment with MYMODULE_ prefix
api_timeout: int = Field(default=30, description="API timeout in seconds")
max_retries: int = Field(default=3, description="Maximum retry attempts")
batch_size: int = Field(default=100, description="Batch processing size")
enable_feature_x: bool = Field(default=False, description="Enable feature X")
model_config = {"env_prefix": "MYMODULE_"}
# Export for auto-discovery
config_class = MyModuleConfig
config = MyModuleConfig()
```
Access configuration in your code:
```python
# In services or routes
from app.modules.config import get_module_config
mymodule_config = get_module_config("mymodule")
timeout = mymodule_config.api_timeout
```
Or import directly:
```python
from app.modules.mymodule.config import config
timeout = config.api_timeout
```
Environment variables:
```bash
# .env
MYMODULE_API_TIMEOUT=60
MYMODULE_MAX_RETRIES=5
MYMODULE_ENABLE_FEATURE_X=true
```
## Background Tasks
```python
# app/modules/mymodule/tasks/__init__.py
from app.modules.mymodule.tasks.background import process_data
__all__ = ["process_data"]
```
```python
# app/modules/mymodule/tasks/background.py
from app.modules.task_base import DatabaseTask
from celery_config import celery_app
@celery_app.task(base=DatabaseTask, bind=True)
def process_data(self, data_id: int):
"""Process data in background."""
db = self.get_db()
# Use db session
pass
```
## Migrations
Modules can have their own migrations that are auto-discovered by Alembic.
### Directory Structure
```
app/modules/mymodule/migrations/
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
├── mymodule_001_initial.py
└── mymodule_002_add_feature.py
```
### Naming Convention
@@ -307,177 +439,121 @@ def get_payments_module_with_routers():
{module_code}_{sequence}_{description}.py
```
Examples:
- `cms_001_create_content_pages.py`
- `cms_002_add_seo_fields.py`
- `billing_001_create_subscriptions.py`
Example: `mymodule_001_create_tables.py`
### Creating the Migrations Directory
```bash
# Create migrations directory structure
mkdir -p app/modules/mymodule/migrations/versions
# Create required __init__.py files
echo '"""Module migrations."""' > app/modules/mymodule/migrations/__init__.py
echo '"""Migration versions."""' > app/modules/mymodule/migrations/versions/__init__.py
```
### Migration Template
```python
# app/modules/cms/migrations/versions/cms_001_create_content_pages.py
"""Create content pages table.
Revision ID: cms_001
Revises:
Create Date: 2026-01-27
# app/modules/mymodule/migrations/versions/mymodule_001_create_tables.py
"""Create mymodule tables.
Revision ID: mymodule_001
Create Date: 2026-01-28
"""
from alembic import op
import sqlalchemy as sa
revision = "cms_001"
down_revision = None # Or previous module migration
branch_labels = ("cms",)
depends_on = None
revision = "mymodule_001"
down_revision = None
branch_labels = ("mymodule",)
def upgrade() -> None:
op.create_table(
"cms_content_pages",
"mymodule_items",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("slug", sa.String(200), nullable=False),
sa.Column("content", sa.Text()),
sa.Column("created_at", sa.DateTime(timezone=True)),
sa.Column("updated_at", sa.DateTime(timezone=True)),
sa.Column("name", sa.String(200), nullable=False),
)
def downgrade() -> None:
op.drop_table("cms_content_pages")
op.drop_table("mymodule_items")
```
## Module Configuration
### Running Module Migrations
### Defining Configuration Schema
Module migrations are automatically discovered. Run all migrations:
```python
from pydantic import BaseModel, Field
class BillingConfig(BaseModel):
"""Billing module configuration."""
stripe_mode: Literal["test", "live"] = "test"
trial_days: int = Field(default=14, ge=0, le=365)
enable_invoices: bool = True
invoice_prefix: str = Field(default="INV-", max_length=10)
```bash
alembic upgrade head
```
### Using Configuration
The `alembic/env.py` automatically discovers module migrations via `app/modules/migrations.py`.
```python
from app.modules.service import module_service
### Note on Existing Modules
# Get module config for platform
config = module_service.get_module_config(db, platform_id, "billing")
# Returns: {"stripe_mode": "test", "trial_days": 14, ...}
Existing modules (CMS, billing, marketplace, etc.) currently have their migrations in the central `alembic/versions/` directory. Moving these to module-specific directories is an optional migration task that can be done incrementally.
# Set module config
module_service.set_module_config(
db, platform_id, "billing",
{"trial_days": 30}
)
**New modules should create their own `migrations/versions/` directory.**
## Validation
Run the architecture validator to check your module:
```bash
python scripts/validate_architecture.py
```
## Health Checks
### Validation Rules
### Defining Health Check
```python
def check_billing_health() -> dict:
"""Check billing service dependencies."""
issues = []
# Check Stripe
try:
stripe.Account.retrieve()
except stripe.AuthenticationError:
issues.append("Stripe authentication failed")
if issues:
return {
"status": "unhealthy",
"message": "; ".join(issues),
}
return {
"status": "healthy",
"details": {"stripe": "connected"},
}
billing_module = ModuleDefinition(
code="billing",
health_check=check_billing_health,
# ...
)
```
## Registering the Module
### Add to Registry
```python
# app/modules/registry.py
from app.modules.analytics.definition import analytics_module
OPTIONAL_MODULES: dict[str, ModuleDefinition] = {
# ... existing modules
"analytics": analytics_module,
}
MODULES = {**CORE_MODULES, **OPTIONAL_MODULES, **INTERNAL_MODULES}
```
## Module Events
Subscribe to module lifecycle events:
```python
from app.modules.events import module_event_bus, ModuleEvent, ModuleEventData
@module_event_bus.subscribe(ModuleEvent.ENABLED)
def on_analytics_enabled(data: ModuleEventData):
if data.module_code == "analytics":
# Initialize analytics for this platform
setup_analytics_dashboard(data.platform_id)
@module_event_bus.subscribe(ModuleEvent.DISABLED)
def on_analytics_disabled(data: ModuleEventData):
if data.module_code == "analytics":
# Cleanup
cleanup_analytics_data(data.platform_id)
```
| Rule | What It Checks |
|------|----------------|
| MOD-001 | Required directories exist |
| MOD-002 | Services contain actual code |
| MOD-003 | Schemas contain actual code |
| MOD-004 | Routes import from module |
| MOD-005 | Templates and static exist |
| MOD-006 | Locales directory exists |
| MOD-007 | Definition paths are valid |
| MOD-008 | exceptions.py exists |
| MOD-009 | definition.py exists |
| MOD-010 | Routes export `router` |
| MOD-011 | Tasks has `__init__.py` |
| MOD-012 | All locale files exist |
| MOD-013 | config.py exports config |
| MOD-014 | Migrations follow naming convention |
| MOD-015 | Migrations have `__init__.py` |
## Checklist
### New Module Checklist
- [ ] Create module directory: `app/modules/{code}/`
- [ ] Create module directory with structure
- [ ] Create `definition.py` with ModuleDefinition
- [ ] Add module to appropriate dict in `registry.py`
- [ ] Create routes if needed (admin.py, vendor.py)
- [ ] Register menu items in menu registry
- [ ] Create migrations if adding database tables
- [ ] Add health check if module has dependencies
- [ ] Document features and configuration options
- [ ] Write tests for module functionality
- [ ] Set `is_self_contained=True`
- [ ] Create `__init__.py` in all directories
- [ ] Create `exceptions.py`
- [ ] Create routes with `router` variable
- [ ] Create templates with namespace prefix
- [ ] Create locales (en, de, fr, lu)
- [ ] Create `config.py` if module needs environment settings (optional)
- [ ] Create `migrations/versions/` with `__init__.py` files if module has database tables
- [ ] Run `python scripts/validate_architecture.py`
- [ ] Test routes are accessible
### Self-Contained Module Checklist
### No Framework Changes Needed
- [ ] All items from basic checklist
- [ ] Create `services/` directory with business logic
- [ ] Create `models/` directory with SQLAlchemy models
- [ ] Create `schemas/` directory with Pydantic schemas
- [ ] Create `exceptions.py` for module-specific errors
- [ ] Create `config.py` with Pydantic config model
- [ ] Set up `migrations/versions/` directory
- [ ] Create `templates/` if module has UI
- [ ] Create `locales/` if module needs translations
- [ ] Set `is_self_contained=True` and path attributes
You do NOT need to:
- Edit `main.py`
- Edit `registry.py`
- Edit any configuration files
- Register routes manually
- Import the module anywhere
The framework discovers everything automatically!
## Related Documentation
- [Module System](../architecture/module-system.md) - Architecture overview
- [Menu Management](../architecture/menu-management.md) - Menu item integration
- [Database Migrations](migration/database-migrations.md) - Migration guide
- [Menu Management](../architecture/menu-management.md) - Sidebar integration
- [Architecture Rules](architecture-rules.md) - Validation rules