Architecture
App Studio spans TypeScript frontend packages and a C# backend service layer. All data access goes through AIExtension.Service — never direct database queries.
System Architecture Overview
App Studio is composed of two major layers:
- Frontend — TypeScript SPA packages running in BizFirstAiStudio, providing the builder and the published app runtime
- Backend — C# ASP.NET Core API controllers backed by
AIExtension.Service, providing the data and execution layer
App Studio has no DbContext and no Repository classes. Every read and write operation goes through IAppStudioService in the AIExtension.Service layer. This is a deliberate architectural constraint that enforces multi-tenant isolation, caching, and audit logging.
Frontend Package Structure
The App Studio frontend is built as composable TypeScript packages within the BizFirstAiStudio monorepo. Key functional areas:
| Area | Responsibility | Key Files |
|---|---|---|
| Builder Shell | Three-panel layout, toolbar, keyboard shortcuts | BuilderLayout.tsx, BuilderToolbar.tsx |
| App Tree | Left panel tree view — AppPages, Panes, Widgets hierarchy | AppTree.tsx, AppTreeStore.ts |
| Canvas | Live preview pane — renders the selected AppPage | Canvas.tsx, CanvasRenderer.ts |
| Properties Editor | Right panel — context-sensitive property forms | PropertiesPanel.tsx, PropertyForm.tsx |
| Widget Registry | Catalog of available widget types and their schemas | WidgetRegistry.ts, WidgetDefinition.ts |
| Token Resolver | Evaluates {{ }} expressions at render time | TokenResolver.ts, AppVariable.ts |
| Action Executor | Runs actions triggered by widget events | ActionExecutor.ts, ActionConfig.ts |
| Layout Engine | Breakpoint-aware grid/flex layout calculation | LayoutEngine.ts, BreakpointConfig.ts |
| Navigation | Sidebar config, URL routing, route-to-variable wiring | NavigationConfig.ts, RouteConfig.ts |
| Permissions | Role-based visibility rule evaluation | PermissionsConfig.ts, VisibilityRuleEvaluator.ts |
| Custom JS Sandbox | Isolated JS execution environment | CustomJSSandbox.ts, SandboxedJSExecutor.ts |
| Export/Import | App bundle serialization and deserialization | AppExporter.ts, AppImporter.ts |
Backend Service Layer
The C# backend for App Studio is organized under mvc-server/AppStudio/ in the BizFirstPayrollV3 solution:
| Component | Responsibility |
|---|---|
IAppStudioService | Service contract — all App Studio backend operations |
AppStudioDataService | Implementation in AIExtension — handles caching, multi-tenancy, audit |
AppController | REST API endpoints for app CRUD, publish, export, import |
WidgetService | Widget definition storage and retrieval |
AppExportService | Builds the JSON export bundle |
AppImportService | Validates and imports a JSON bundle |
AppAccessChecker | Server-side enforcement of app-level role permissions |
WorkflowTriggerActionHandler | Executes trigger-workflow actions via Flow Studio API |
Data Flow
// Builder save flow
Builder (TypeScript)
└─► PUT /api/apps/{id}
└─► AppController.SaveApp()
└─► IAppStudioService.SaveAppAsync()
└─► AIExtension.AppStudioDataService
├─► Validate multi-tenant isolation
├─► Write to AIExtension tables
├─► Invalidate Redis cache
└─► Write audit log entry
// Runtime data fetch flow
Published App (TypeScript runtime)
└─► GET /api/apps/{id}/data?source=GetLeads
└─► AppController.GetData()
└─► IAppStudioService.GetGridDataAsync(query)
└─► AIExtension.AppStudioDataService
├─► Check Redis cache
├─► Query backing data (via platform services)
└─► Return typed result rows
Multi-Tenant Isolation
App Studio enforces tenant isolation at the service layer — not in the controller. Every IAppStudioService method receives the current tenantId from the authenticated request context. Apps are always scoped to a tenant — a user from Tenant A cannot access apps from Tenant B.
Caching Strategy
App definitions (the JSON config of an app) are cached in Redis by AppStudioDataService:
- Cache key:
appstudio:app:{tenantId}:{appId} - TTL: configurable (default 30 minutes)
- Invalidation: on every
SaveApporPublishAppcall - Widget definitions are cached separately with a longer TTL