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>
15 KiB
Creating Modules
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.
Quick Start (5 Minutes)
Creating a new module requires zero changes to main.py, registry.py, or any other framework file.
Step 1: Create Directory Structure
# 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
# app/modules/mymodule/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
mymodule_module = ModuleDefinition(
code="mymodule",
name="My Module",
description="Description of what this module does",
version="1.0.0",
# 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.VENDOR: ["mymodule"],
},
# 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",
)
Step 3: Create Routes (Auto-Discovered)
# app/modules/mymodule/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'
@router.get("")
def get_mymodule_data(current_user=Depends(get_current_vendor_api)):
return {"message": "Hello from mymodule"}
# 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
router = APIRouter() # MUST be named 'router'
@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},
)
Step 4: Done!
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:
# 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:
templates.TemplateResponse("mymodule/vendor/index.html", {...})
Static File URLs
Static files are mounted at /static/modules/{module_code}/:
<!-- In template -->
<script src="{{ url_for('mymodule_static', path='vendor/js/mymodule.js') }}"></script>
Translation Keys
Translations are namespaced under the module code:
// locales/en.json
{
"mymodule": {
"title": "My Module",
"description": "Module description"
}
}
Use in templates:
{{ _('mymodule.title') }}
Module Classification
Core Modules
Always enabled, cannot be disabled.
ModuleDefinition(
code="mymodule",
is_core=True,
# ...
)
Optional Modules (Default)
Can be enabled/disabled per platform.
ModuleDefinition(
code="mymodule",
is_core=False,
is_internal=False,
# ...
)
Internal Modules
Admin-only tools, not visible to vendors.
ModuleDefinition(
code="mymodule",
is_internal=True,
# ...
)
Module Dependencies
Declare dependencies in the definition:
ModuleDefinition(
code="orders",
requires=["payments"], # Must have payments enabled
# ...
)
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
Services Pattern
# app/modules/mymodule/services/mymodule_service.py
from sqlalchemy.orm import Session
class MyModuleService:
def get_data(self, db: Session, vendor_id: int):
# Business logic here
pass
# Singleton instance
mymodule_service = MyModuleService()
# app/modules/mymodule/services/__init__.py
from app.modules.mymodule.services.mymodule_service import (
mymodule_service,
MyModuleService,
)
__all__ = ["mymodule_service", "MyModuleService"]
Exceptions Pattern
# 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.
# 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:
# 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:
from app.modules.mymodule.config import config
timeout = config.api_timeout
Environment variables:
# .env
MYMODULE_API_TIMEOUT=60
MYMODULE_MAX_RETRIES=5
MYMODULE_ENABLE_FEATURE_X=true
Background Tasks
# app/modules/mymodule/tasks/__init__.py
from app.modules.mymodule.tasks.background import process_data
__all__ = ["process_data"]
# 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
{module_code}_{sequence}_{description}.py
Example: mymodule_001_create_tables.py
Creating the Migrations Directory
# 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
# 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 = "mymodule_001"
down_revision = None
branch_labels = ("mymodule",)
def upgrade() -> None:
op.create_table(
"mymodule_items",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
sa.Column("name", sa.String(200), nullable=False),
)
def downgrade() -> None:
op.drop_table("mymodule_items")
Running Module Migrations
Module migrations are automatically discovered. Run all migrations:
alembic upgrade head
The alembic/env.py automatically discovers module migrations via app/modules/migrations.py.
Note on Existing Modules
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.
New modules should create their own migrations/versions/ directory.
Validation
Run the architecture validator to check your module:
python scripts/validate_architecture.py
Validation Rules
| 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 with structure
- Create
definition.pywith ModuleDefinition - Set
is_self_contained=True - Create
__init__.pyin all directories - Create
exceptions.py - Create routes with
routervariable - Create templates with namespace prefix
- Create locales (en, de, fr, lu)
- Create
config.pyif module needs environment settings (optional) - Create
migrations/versions/with__init__.pyfiles if module has database tables - Run
python scripts/validate_architecture.py - Test routes are accessible
No Framework Changes Needed
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 overview
- Menu Management - Sidebar integration
- Architecture Rules - Validation rules