Files
orion/docs/development/creating-modules.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
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>
2026-02-14 16:46:56 +01:00

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.py with ModuleDefinition
  • 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/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!