Module System
Modules are self-contained units of functionality in Fullfinity.
Module Structure
Section titled “Module Structure”modules/my_module/├── __init__.py # Package marker (optional but recommended)├── manifest.yaml # Module metadata├── models/ # Model definitions│ ├── __init__.py # MUST list every model: `from . import my_model`│ └── my_model.py├── views/ # UI definitions (.yaml)│ ├── views.yaml # UiView definitions│ ├── actions.yaml # Window actions│ └── menus.yaml # Menu items├── data/ # Seed data (.yaml)│ └── stages.yaml├── security/ # Access control (.yaml)│ └── security.yaml├── templates/ # All templates (.yaml, .jinja)│ ├── report_layout.jinja│ └── section_templates.yaml├── routes/ # API endpoints (optional)│ ├── __init__.py│ └── my_routes.py└── static/ # Static assets (optional) └── js/Folder Purposes
Section titled “Folder Purposes”| Folder | Purpose | File Types |
|---|---|---|
views/ | UI definitions | UiView, WindowAction, UiMenu |
data/ | Seed/reference data | Any model (stages, currencies, categories) |
security/ | Access control | Group, ModelAccess, RecordRule |
templates/ | All templates | WebsiteSectionTemplate, ReportLayout, WebPage |
routes/ | API endpoints | Python controllers |
static/ | Frontend assets | JS, CSS, images |
Manifest File
Section titled “Manifest File”The manifest.yaml defines module metadata:
name: CRMidentifier: crmcategory: module_category_crmdescription: Manage leads and pipelinedependencies:- core- meetingsicon: FilterFields
Section titled “Fields”| Field | Type | Description |
|---|---|---|
name | string | Display name |
identifier | string | Unique identifier (snake_case) |
category | string | Module category for grouping (must be a valid ModuleCategory identifier) |
description | string | Short description |
dependencies | array | Required modules |
icon | string | Lucide icon name (without prefix) |
application | boolean | Marks a standalone, user-facing app (appears in the Apps browser) |
implicit | boolean | Auto-install once dependencies are present (the bridge pattern) |
version | string | Free-text version label (informational only — not used for upgrade ordering) |
The module’s display color is computed deterministically from its name at load time; a
colormanifest key is ignored. There are noauthor,website, orlicensekeys either. See Module Manifest for the full field reference.
Dependencies
Section titled “Dependencies”Modules declare their dependencies:
dependencies:- core- contacts- Dependencies are installed automatically
- Circular dependencies are not allowed
coreis always required (implicitly)
Adding a dependency to an already-installed module
Section titled “Adding a dependency to an already-installed module”When you extend an existing, already-installed module so it now depends on another module —
e.g. helpdesk starts depending on timesheet because it references that module’s model —
just run ./fullfinity-server -db <db> -u all. The upgrade discovers the new dependency
from the current manifests, installs it (and provisions its tables) before updating the
dependents, and replays any data migrations.
You do not need to install the new dependency by hand first: the runtime composes the transitive dependency closure of the installed modules’ current on-disk manifests, so a newly-declared dependency’s models load and cross-module references resolve even before its module row exists. (In steady state — every dependency already installed — this is a no-op.)
Module Loading
Section titled “Module Loading”1. Discovery
Section titled “1. Discovery”On startup, all module directories are scanned:
modules = discover_modules("./modules")# Returns: [ModuleInfo(name="core", ...), ModuleInfo(name="crm", ...)]2. Dependency Resolution
Section titled “2. Dependency Resolution”Modules are sorted by dependencies:
# crm depends on core → core loads firstsorted_modules = topological_sort(modules)3. Loading
Section titled “3. Loading”For each module:
- Import the
models/package — running itsmodels/__init__.py, which explicitly lists every model file viafrom . import <file>lines - Load data files (views, menus, security) from
views/,security/,data/ - Import routes from
routes/directory
Loading conventions
Section titled “Loading conventions”| Folder | File Type | Purpose |
|---|---|---|
models/ | *.py (listed in __init__.py) | Model definitions |
views/ | *.yaml | UI views, actions, menus |
data/ | *.yaml | Seed/default data |
security/ | *.yaml | Access rules and groups |
templates/ | *.yaml | Template definitions |
routes/ | *.py | API endpoints |
:::tip Excluding Files
Python files starting with _ are skipped by the route importer. Use this for:
- Helper modules not meant to be a route file:
_helpers.py - Test utilities:
_test_helpers.py
(Model files are not discovered by name — they load only via the explicit
from . import <file> lines in models/__init__.py. A _-prefixed model helper is
simply omitted from that list.)
:::
File References in YAML
Section titled “File References in YAML”Template files (.jinja, .html, .xml, etc.) can be referenced from YAML:
- data_type: WebsiteSectionTemplate identifier: hero_centered template: hero_centered.jinjaThe loader automatically detects file references based on extension and loads the content.
Models
Section titled “Models”Models are defined in the models/ directory. A model file loads only if it is
listed in models/__init__.py — importing the models package runs its __init__.py,
and the route importer stops at the package (it does not descend into the directory).
A .py file dropped into models/ without a matching from . import <file> line silently
never loads (no registration, no __inherit__/_selection_add extension).
# models/__init__.py — the authoritative, greppable list (keep it alphabetical)from . import leadfrom fullfinity.engine.base import *
class Lead(Model): _verbose_name = "Lead"
name = Char(max_length=255, required=True) email = Char(max_length=255) stage = ManyToOne("CrmStage", related_name="leads") probability = Float(default=10.0):::note Add a model = two edits
Create models/<name>.py and add from . import <name> to models/__init__.py.
There is no manifest models: key, and no by-name auto-discovery.
:::
Views are defined in YAML files in views/:
- data_type: UiView identifier: lead_form_view type: Form model: Lead arch: [...]Actions
Section titled “Actions”Window actions connect views to models. modes is a comma-separated string of view
types; views is the registered list of view records:
- data_type: WindowAction identifier: leads_action name: Leads model: Lead modes: Kanban, List, Form default_view: lead_kanban_viewMenus define navigation:
- data_type: UiMenu identifier: crm_leads_menu name: Leads parent: crm_menu action: leads_action sequence: 10Security
Section titled “Security”Security rules are defined in security/security.yaml:
- data_type: Group identifier: crm_user name: User category: CRM- data_type: ModelAccess identifier: lead_access model: Lead read_perm: true write_perm: true create_perm: true delete_perm: falseInitial Data
Section titled “Initial Data”Seed data can be placed in data/:
- data_type: CrmStage identifier: stage_new name: New sequence: 10 apply_once: true- data_type: CrmStage identifier: stage_qualified name: Qualified sequence: 20 apply_once: trueapply_once Flag
Section titled “apply_once Flag”Controls whether records are updated on module update:
| Value | Behavior |
|---|---|
true | Only create if not exists (preserves user customizations) |
false (default) | Always create/overwrite on update |
Use apply_once: true for user-customizable seed data (stages, categories, currencies).
Omit or set false for system records that must stay in sync (cron jobs).
Module Installation
Section titled “Module Installation”When a module is installed:
- Database Migration - Tables created for new models
- Data Loading - Views, actions, menus, security loaded
- Dependencies - All dependencies installed first
Module Extension
Section titled “Module Extension”Modules can extend other modules:
Extending Models
Section titled “Extending Models”# In your_module/models/contact_ext.py# (remember to add `from . import contact_ext` to your_module/models/__init__.py)from fullfinity.engine.base import *
class ContactExtension(Model): __inherit__ = "Contact" # Extends Contact model
twitter_handle = Char(max_length=100) linkedin_url = Char(max_length=255)Extending Views
Section titled “Extending Views”- data_type: UiView identifier: contact_twitter_extension type: Form model: Contact inherited_view: contact_form_view arch: operations: - action: add target: field: email position: after value: type: field name: twitter_handle properties: widget: TextInputBest Practices
Section titled “Best Practices”- Keep modules focused - One domain per module
- Minimize dependencies - Only depend on what you need
- Use semantic view targeting - Never use index-based targets
- Use
_prefix for non-route Python helpers - Files like_helpers.pyare skipped by the route importer (model files load only viamodels/__init__.py, so just omit them from that list) - Use identifiers - All data should have unique identifiers
Next Steps
Section titled “Next Steps”- Creating Modules - Step-by-step guide
- View Inheritance - Extending views