Virtual State & Defaults
In many enterprise frameworks, configuring an application for the first time is a tedious process. You spin up the database, run a heavy "database seeding" script, and hope it populates the configuration tables correctly before the app can boot.
Framework M takes a different approach: Zero-Touch Provisioning.
By utilizing the DefaultsService and the concept of "Virtual State", an application can boot and run perfectly using declarative configuration files (like TOML), smoothly transitioning to database-driven state only when an administrator explicitly changes a setting in the UI.
The Trap: Seeding vs. Defaults
A classic architectural trap is conflating "defaults" with "seeding". They serve two fundamentally different purposes in Framework M:
- Seeding (State Initialization): A write operation. It physically inserts rows into the database (e.g., standard
CurrencyorCountryrecords) because other tables require strict foreign-key relationships to them. - Defaults (Runtime Fallbacks): A read operation. It evaluates at runtime. If a requested value doesn't exist in the database, the framework serves a fallback value in-memory. It does not write to the database.
Framework M's DefaultsService handles the latter, operating through two distinct patterns depending on the DocType.
Pattern 1: Virtual Singletons
For Singleton DocTypes (like SystemSettings), there is only ever one record.
If a user has never opened the System Settings page and clicked "Save", the database table is completely empty. Instead of crashing or returning None, the SinglesManager intercepts the database miss and orchestrates a Virtual Singleton.
How it works at runtime:
- The application calls
singles_manager.get(SystemSettings). - The manager checks the database and finds nothing.
- It delegates to the
DefaultsService, which checks the DocType'sMeta.defaults_file(defaulting toframework_config.toml). - The service reads the TOML file, extracts the relevant configuration, and instantiates the
SystemSettingsPydantic model in-memory. - The application receives a fully hydrated, strictly typed configuration object without a single database row existing.
Start Indie, Scale Enterprise
This is the core of Zero-Touch Provisioning. As a developer, you ship a framework_config.toml file with your app. Your app works instantly on the first boot.
Later, when a system administrator logs into the Desk UI and changes a setting, the framework writes an actual row to the database. From that moment forward, the SinglesManager will find the database row, bypass the DefaultsService, and use the admin's UI configuration instead!
Pattern 2: Record-Level Field Defaults
For standard, multi-record DocTypes (like Employee or Invoice), the DefaultsService solves a different enterprise problem: dynamic, environment-specific fallbacks.
Normally, if you want a default value for a field, you hardcode it in the schema:
class Employee(BaseDocType):
department: str = Field(default="General")
leave_days: int = Field(default=20)
But what if you deploy this HR app to two different companies? Company A defaults to 20 days of leave, and Company B defaults to 15. Hardcoding forces you to branch your code.
The Configuration Extraction
Instead of hardcoding, you declare a defaults_file in the Meta class:
class Employee(BaseDocType):
department: str
leave_days: int
class Meta:
defaults_file = "apps/hr/config/employee_defaults.toml"
When a user clicks "New Employee" and submits { "name": "Alice" }, the DefaultsService intercepts the payload before it reaches the repository. It reads the TOML defaults and merges them with the user's input (user input always wins ties).
The resulting hydrated payload is then saved to the database:
name: "Alice" (from user)department: "General" (from TOML)leave_days: 15 (from TOML)
Order of Precedence
If you keep Field(default=20) on the Pydantic model alongside the defaults_file, the framework resolves conflicts in this exact order:
- User Input (Highest priority - wins all ties)
- TOML Configuration (
defaults_file- environment specific overrides) - Pydantic Schema Default (
Field(default=...)- the ultimate code-level fallback)
Why this is powerful:
- Zero-Code Customization: A system administrator can change the default behavior for all future records just by editing a TOML file.
- Clean Domain Models: Your Pydantic models stay pure. They define types and constraints, while the TOML file defines the business context.
- Multi-Tenancy: In a SaaS environment, the
DefaultsProtocolcan dynamically load tenant-specific configurations, allowing every tenant to have their own default field behaviors without touching the core schema.
The Single Source of Truth
To utilize these features, you simply declare defaults_file in your DocType.Meta. The framework's MetaRegistry automatically whitelists this property, exposing it securely to the frontend and internal services.