Bounded Contexts vs. Field-Level Security
In traditional monolithic ERP systems, it is common to see massive database tables with hundreds of columns (e.g., an Item table containing everything from weight to tax_code to valuation_rate).
Because all data lives in one place, these systems are forced to invent Field-Level Security (FLS) to dynamically mask sensitive columns (like valuation_rate) from unauthorized users (like warehouse workers) during reads, and to strip out unauthorized updates during generic doc.save() operations.
FLS is notoriously confusing, difficult to audit, and a massive source of performance overhead.
Framework M and Business M completely reject Field-Level Security.
We use Domain-Driven Design (DDD) and Bounded Contexts to natively eliminate the need for FLS.
The Golden Rule of Schema Design
Section titled “The Golden Rule of Schema Design”If you find yourself needing to hide a specific field from a specific role, that is your domain telling you that the field does not belong on that DocType.
It belongs in a separate Bounded Context (Extension Table) owned exclusively by the macroservice that cares about it.
1. Secure Reads via Bounded Contexts
Section titled “1. Secure Reads via Bounded Contexts”When extending Master Data (like Customer, Supplier, or Item), you must choose between two distinct patterns based on the sensitivity of the data.
Option A: Metadata Decorators (Public Extensions)
Section titled “Option A: Metadata Decorators (Public Extensions)”Used for non-sensitive, globally visible extensions.
If the WMS macroservice needs to track the physical volume of an Item, it can safely inject that property into the core Item DocType dynamically during bootstrap.
# In wms/bootstrap.pyregistry.register_property("Item", "volume_cbm", float, persistence="json")Security Impact: Because this is injected into the base Item, anyone who has permission to read an Item will see volume_cbm. No field-level masking is applied.
Option B: Extension Tables / Profiles (Sensitive Extensions)
Section titled “Option B: Extension Tables / Profiles (Sensitive Extensions)”Used for sensitive or highly specialized data.
If the Finance macroservice needs to track a Customer’s tax_id or default_income_account, it does not inject it into the base Customer DocType. Instead, Finance creates a completely separate DocType (e.g., FinanceCustomerProfile) that links back to the core Customer.
# In finance/doctypes/finance_customer_profile/doctype.pyclass FinanceCustomerProfile(DocType): customer: str = Field(description="Linked Customer ID") tax_id: str | None = Field(default=None, description="Tax ID (e.g., GSTIN)") default_income_account: str | None = Field( default=None, description="Default Income Account" )
class Meta: permissions = {"read": ["Finance Manager"], "write": ["Finance Manager"]}Security Impact: Because the sensitive fields are physically isolated in a different DocType owned by the finance module, standard document-level Role-Based Access Control (RBAC) naturally hides the data from unauthorized users like warehouse operators.
2. Secure Writes via Task-Based APIs (CQRS)
Section titled “2. Secure Writes via Task-Based APIs (CQRS)”Monoliths rely on generic doc.save() endpoints that update the entire document at once. This forces the backend to guess the user’s intent and strip out unauthorized field modifications.
Business M utilizes Command Query Responsibility Segregation (CQRS). Instead of generic saves, we expose specific intents/commands via RPC over NATS or REST.
- To change an item’s weight, the UI calls a specific endpoint:
rpc.wms.item.update_dimensions. - To change an item’s price, the UI calls:
rpc.finance.item.update_price.
How this eliminates Write FLS:
Section titled “How this eliminates Write FLS:”By building task-based APIs, we map standard RBAC permissions to the RPC handlers themselves.
If a user calls update_dimensions, the system only accepts weight and dimensions parameters. There is physically no way for a malicious request to sneak in an update to valuation_rate, eliminating the need to inspect and mask the payload field-by-field.
Beyond CRUD: The Limiter Workflow
Section titled “Beyond CRUD: The Limiter Workflow”For highly concurrent, sensitive data like Credit Limits, Budgets, or Inventory Balances, we abandon CRUD entirely.
We do not store current_balance as static fields on a profile table that get “bumped” up and down via SQL UPDATEs (which causes severe lock contention and race conditions). Instead, we use dedicated high-speed engines (like book-keeper or the m_imprest limiter). The UI dispatches intent events (e.g., rpc.finance.limit.consume), which append immutable records to refill or consume limiter accounts.
Summary
Section titled “Summary”- Never use Field-Level Security.
- If data is sensitive, put it in an Extension Table (
Option B) and restrict access to the entire table. - If data is public, inject it via the MetadataDecoratorRegistry (
Option A). - Use Task-Based RPCs to handle updates, rather than generic
doc.save()payloads.