Landing Architecture
In a simple application, deciding where to send a user after they log in is trivial: you redirect them to /dashboard.
However, in an enterprise application built on Framework M, users have diverse roles, diverse contexts, and access to entirely different modules. A Warehouse Worker needs to land on the Barcode Scanner, an Accountant needs to land on the General Ledger, and an external Customer might need to land on a self-service portal.
To solve this without hardcoding routing logic into the authentication handlers, Framework M implements a Policy-Driven Landing Architecture.
The 5-Step Resolution Hierarchy
Instead of a single redirect rule, the core LandingService acts as an orchestrator. It iterates through an injected sequence of LandingProtocol adapters, stopping at the first one that successfully resolves a route.
The standard hierarchy evaluates in this exact order:
- Manual Override: Has an admin explicitly forced this specific user to a certain page? (e.g., forcing a password reset or terms of service acceptance).
- Feature Entitlement: Does the user's current active tenant or subscription mandate a specific starting experience?
- User Preference: Did the user manually pin a specific page as their default home page?
- Policy-Driven (ABAC): Based on the user's roles and permissions, what is the most appropriate default page for their persona?
- System Fallback: If all else fails, where does the system send them? (Usually a generic
/app/home).
By modelling this as a priority queue, we guarantee that critical system overrides (like security prompts) always preempt user preferences.
Ports and Adapters in Action
The Landing Architecture perfectly illustrates Framework M's Hexagonal (Ports & Adapters) design.
The Core Port: LandingProtocol
The core domain defines a single, simple interface:
async def resolve(self, context: UserContext) -> LandingDescriptor | None:
...
The core LandingService simply iterates through a list of these interfaces. It has absolutely no idea if the underlying route is coming from a database, a Redis cache, or a TOML file.
The Grouper: CompositeLandingAdapter
Why do we need a Composite adapter if the LandingService already loops through a list?
In reality, step 3 ("User Preference") might require checking a fast Redis cache first, and if that misses, querying a slow PostgreSQL table. If we put both of those checks directly into the core LandingService, we are leaking infrastructure details into our pure business logic.
Instead, we bundle the RedisPreferenceAdapter and SqlPreferenceAdapter inside a CompositeLandingAdapter. To the core orchestrator, it looks like a single, clean step.
The Engine: PermissionAwareLandingAdapter
Step 4 ("Policy-Driven") is where the real power of the framework shines.
Instead of building a massive if/else block checking for specific roles (if "Accountant" in roles:), we use the PermissionAwareLandingAdapter. This adapter takes a list of LandingCandidate configurations and delegates directly to the system's existing PermissionProtocol.
# Example Landing Candidates
[
{"route": "/app/admin", "required_doctype": "SystemSettings", "required_action": "read"},
{"route": "/app/wms", "required_doctype": "Warehouse", "required_action": "read"},
{"route": "/app/home"} # Fallback
]
When a user logs in, the adapter iterates through these candidates from top to bottom. It asks the Permission service: "Is this user authorized to read SystemSettings?" If yes, they go to /app/admin. If no, it checks the next candidate.
This means your routing logic automatically scales with your Attribute-Based Access Control (ABAC) graph without writing any custom routing code.
Integration with Zero-Touch Provisioning
Where do these LandingCandidate configurations come from?
In Framework M, they are stored on the SystemSettings singleton DocType.
- Start Indie: When you first deploy your app, the database is empty. The
DefaultsServiceseamlessly readsframework_config.tomlto create a "Virtual Singleton" of theSystemSettings, passing your declarative candidates into the Landing Adapter. Your app works out-of-the-box. - Scale Enterprise: Later, an administrator logs into the Desk UI, goes to System Settings, and adds a new Landing Candidate for a newly hired role. The moment they click "Save", it writes to the database. From then on, the framework reads from the database instead of the TOML file.
This achieves full GitOps compatibility for developers, while preserving dynamic UI configurability for system administrators.