Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
Creating Modules
This guide explains how to create new plug-and-play modules for the Orion 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/store,static/store/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.STORE: ["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/store.py
from fastapi import APIRouter, Depends
from app.api.deps import get_current_store_api, get_db
router = APIRouter() # MUST be named 'router'
@router.get("")
def get_mymodule_data(current_user=Depends(get_current_store_api)):
return {"message": "Hello from mymodule"}
# app/modules/mymodule/routes/pages/store.py
from fastapi import APIRouter, Request, Path
from fastapi.responses import HTMLResponse
from app.api.deps import get_current_store_from_cookie_or_header, get_db, Depends
from app.templates_config import templates
router = APIRouter() # MUST be named 'router'
@router.get("/{store_code}/mymodule", response_class=HTMLResponse)
async def mymodule_page(
request: Request,
store_code: str = Path(...),
current_user=Depends(get_current_store_from_cookie_or_header),
db=Depends(get_db),
):
return templates.TemplateResponse(
"mymodule/store/index.html",
{"request": request, "store_code": store_code},
)
Step 4: Done!
That's it! The framework automatically:
- Discovers and registers the module
- Mounts API routes at
/api/v1/store/mymodule - Mounts page routes at
/store/{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
│ │ └── store.py # /api/v1/store/mymodule
│ └── pages/
│ ├── __init__.py
│ ├── admin.py # /admin/mymodule
│ └── store.py # /store/{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
│ └── store/
│ └── index.html
│
├── static/ # Static files (auto-mounted)
│ ├── admin/
│ │ └── js/
│ │ └── mymodule.js
│ └── store/
│ └── 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
└── store/
└── index.html
Reference in code:
templates.TemplateResponse("mymodule/store/index.html", {...})
Static File URLs
Static files are mounted at /static/modules/{module_code}/:
<!-- In template -->
<script src="{{ url_for('mymodule_static', path='store/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 stores.
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, store_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 OrionException
class MyModuleException(OrionException):
"""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("store_id", sa.Integer(), sa.ForeignKey("stores.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/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/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