Skip to content

Module System

Modules are self-contained units of functionality in Fullfinity.

modules/my_module/
├── __init__.py # Package marker (optional but recommended)
├── manifest.yaml # Module metadata (manifest.json also accepted)
├── 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/
FolderPurposeFile Types
views/UI definitionsUiView, WindowAction, UiMenu
data/Seed/reference dataAny model (stages, currencies, categories)
security/Access controlGroup, ModelAccess, RecordRule
templates/All templatesWebsiteSectionTemplate, ReportLayout, WebPage
routes/API endpointsPython controllers
static/Frontend assetsJS, CSS, images

The manifest.yaml defines module metadata (a manifest.json is also accepted — the loader looks for manifest.yaml first, then manifest.json):

name: CRM
identifier: crm
category: module_category_crm
description: Manage leads and pipeline
dependencies:
- core
- meetings
icon: Filter
FieldTypeDescription
namestringDisplay name
identifierstringUnique identifier (snake_case)
categorystringModule category for grouping
descriptionstringShort description
dependenciesarrayRequired modules
iconstringTabler icon name
colorstringTheme color (hex)
versionstringSemantic version

Modules declare their dependencies:

{
"dependencies": ["core", "contacts"]
}
  • Dependencies are installed automatically
  • Circular dependencies are not allowed
  • core is always required (implicitly)

On startup, all module directories are scanned:

modules = discover_modules("./modules")
# Returns: [ModuleInfo(name="core", ...), ModuleInfo(name="crm", ...)]

Modules are sorted by dependencies:

# crm depends on core → core loads first
sorted_modules = topological_sort(modules)

For each module:

  1. Import the models/ package — running its models/__init__.py, which explicitly lists every model file via from . import <file> lines
  2. Load data files (views, menus, security) from views/, security/, data/
  3. Import routes from routes/ directory
FolderFile TypePurpose
models/*.py (listed in __init__.py)Model definitions
views/*.yamlUI views, actions, menus
data/*.yamlSeed/default data
security/*.yamlAccess rules and groups
templates/*.yamlTemplate definitions
routes/*.pyAPI 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.) :::

Template files (.jinja, .html, .xml, etc.) can be referenced from YAML:

- data_type: WebsiteSectionTemplate
identifier: hero_centered
template: hero_centered.jinja

The loader automatically detects file references based on extension and loads the content.

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 lead
models/lead.py
from 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: [...]

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_view

Menus define navigation:

- data_type: UiMenu
identifier: crm_leads_menu
name: Leads
parent: crm_menu
action: leads_action
sequence: 10

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: false

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: true

Controls whether records are updated on module update:

ValueBehavior
trueOnly 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).

When a module is installed:

  1. Database Migration - Tables created for new models
  2. Data Loading - Views, actions, menus, security loaded
  3. Dependencies - All dependencies installed first

Modules can extend other modules:

# 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)
- 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: TextInput
  1. Keep modules focused - One domain per module
  2. Minimize dependencies - Only depend on what you need
  3. Use semantic view targeting - Never use index-based targets
  4. Use _ prefix for non-route Python helpers - Files like _helpers.py are skipped by the route importer (model files load only via models/__init__.py, so just omit them from that list)
  5. Use identifiers - All data should have unique identifiers