The No-Pages Model
App Studio apps are not built from static HTML pages. They are composed from AppPages, Panes, and Widgets — a dynamic hierarchy that is configured, not coded.
Traditional Pages vs. App Studio's Model
In a traditional web application, a "page" is an HTML document with a URL. Each page is coded separately, with its own markup, styles, and scripts. Navigation sends the browser to a new document.
App Studio takes a different approach. The entire application is a single-page application (SPA) with a structured content model:
| Traditional Web App | App Studio |
|---|---|
| HTML files for each page | AppPage records stored in the app definition |
| CSS and layout per page | Pane layout configured per breakpoint in the builder |
| JavaScript per page | Widget actions and optional sandboxed Custom JS |
| Backend routes for each page | URL routing configured declaratively per AppPage |
| Navigation = browser load | Navigation = content area swap within the SPA shell |
| Deploy = publish files | Deploy = publish app definition (JSON stored in service layer) |
The App Hierarchy
Every App Studio application is structured as a four-level hierarchy:
App
The top-level container. Holds global settings: name, icon, permissions (which roles can access the app), navigation configuration, and published status.
AppPage
A named content area with a URL route pattern (e.g., /leads/:id). This is what the sidebar navigation item points to. An App can have many AppPages.
Pane
A layout container within an AppPage. Each Pane has its own grid or flex layout, configurable per breakpoint (Desktop/Tablet/Mobile). Panes organize the visual structure of a page.
Widget
A placed UI component inside a Pane. Widgets have a type (DataGrid, Chart, Form, Button, etc.), configuration properties, data bindings, and actions. A Pane can hold many Widgets.
What This Means in Practice
No File System Artifacts
When you build an App Studio application, you do not create HTML files, TypeScript components, or CSS stylesheets. Everything is stored as structured configuration in the App Studio data model, persisted through AIExtension.Service.
Content Areas, Not Page Loads
When a user navigates from the "Leads List" AppPage to the "Lead Detail" AppPage, the browser URL changes (e.g., from /app/crm/leads to /app/crm/leads/123) but the SPA shell does not reload. The app tree swaps the active AppPage's Panes and Widgets into the content area — instant navigation, no server round-trips for the shell.
Modals Are Content Areas Too
Modal dialogs in App Studio are not separate HTML dialogs — they reference a Pane configured as a modal. The open-modal action loads that Pane's Widgets into an overlay layer. This means modal content is just as configurable as any other Pane.
App Studio "pages" are not HTML pages. The URL changes when navigating between AppPages, but there is no server round-trip to load a new document. If you inspect the network tab, you will see data API calls — not page loads.
URL Routing in the No-Pages Model
Each AppPage has a URL route pattern. URL parameters in the route become app variables automatically:
// AppPage: Lead Detail
// Route: /app/crm/leads/:id
// In any widget on this AppPage, use:
{{ route.id }} // resolves to the actual :id from the URL
// Example: DataGrid bound to entity id
dataSource: "GetLeadById"
params: { id: "{{ route.id }}" }
This makes deep linking work naturally. A user can share /app/crm/leads/456 with a colleague — the app will load and navigate directly to lead 456's detail AppPage.
App Definition as Data
Because the app is configuration (not code), it can be exported as a JSON bundle, version-controlled, and imported into another tenant or environment. This is the foundation of App Studio's environment promotion workflow (dev → staging → production).
// Simplified app definition JSON
{
"appId": "crm-app",
"name": "CRM Portal",
"pages": [
{
"pageId": "leads-list",
"title": "Leads",
"route": "/leads",
"panes": [
{
"paneId": "main",
"layout": { "type": "grid", "columns": 12 },
"widgets": [
{
"widgetId": "leads-grid",
"type": "DataGrid",
"config": {
"dataSource": "GetLeads",
"columns": ["name", "status", "owner"]
}
}
]
}
]
}
]
}