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
├── 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:

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 (must be a valid ModuleCategory identifier)
descriptionstringShort description
dependenciesarrayRequired modules
iconstringLucide icon name (without prefix)
applicationbooleanMarks a standalone, user-facing app (appears in the Apps browser)
implicitbooleanAuto-install once dependencies are present (the bridge pattern)
versionstringFree-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 color manifest key is ignored. There are no author, website, or license keys either. See Module Manifest for the full field reference.

Modules declare their dependencies:

dependencies:
- core
- contacts
  • Dependencies are installed automatically
  • Circular dependencies are not allowed
  • core is 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.)

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