Architecture
This document provides a detailed overview of the App Store for Intune architecture.
System architecture
Section titled “System architecture”┌────────────────────────────────────────────────────────────────────────┐│ Azure Cloud ││ ││ ┌──────────────┐ ┌──────────────┐ ││ │ Entra ID │◄────────┤ Frontend │ ││ │ │ │ (React SPA) │ ││ └──────┬───────┘ └──────┬───────┘ ││ │ │ ││ │ │ HTTPS/JWT ││ │ │ ││ │ ┌──────▼───────┐ ┌──────────────┐ ││ │ │ API Layer │◄─────┤ Azure Bot │ ││ │ │ (ASP.NET 8) │ │ (Teams Bot) │ ││ │ └──────┬───────┘ └──────┬───────┘ ││ │ │ │ ││ │ ┌───────────┬───────┼──────────┬──────────┘ ││ │ │ │ │ │ ││ │ ┌─▼────────┐ │ ┌─────▼────┐ ┌───▼──────┐ ┌──────────┐ ││ └──► Graph API │ │ │ SQL │ │ Azure │ │ App │ ││ │ │ │ │ Database │ │ Storage │ │ Insights │ ││ └─────┬─────┘ │ └──────────┘ └──────────┘ └──────────┘ ││ │ │ ││ ┌─────▼─────┐ │ ┌──────────────┐ ││ │ Intune │ └──► Key Vault │ ││ │ & Groups │ │ (Secrets) │ ││ └───────────┘ └──────────────┘ │└────────────────────────────────────────────────────────────────────────┘Components
Section titled “Components”1. Frontend (React SPA)
Section titled “1. Frontend (React SPA)”Technology stack:
- React 18
- TypeScript
- Azure MSAL for authentication
- Axios for HTTP requests
- React Router for navigation
Key features:
- Single-page application for optimal performance
- Microsoft Entra ID authentication with SSO
- Responsive design for desktop and mobile
- Real-time status updates for app requests
Key files:
src/AppRequestPortal.Web/src/App.tsxsrc/AppRequestPortal.Web/src/authConfig.tssrc/AppRequestPortal.Web/src/services/apiClient.ts
2. Backend API (ASP.NET Core)
Section titled “2. Backend API (ASP.NET Core)”Technology stack:
- ASP.NET Core 8.0
- Entity Framework Core
- Microsoft.Identity.Web
- Microsoft Graph SDK
Architecture pattern: Clean Architecture / Layered Architecture
Layers:
-
API Layer (
AppRequestPortal.API)- Controllers
- Authentication/Authorization
- Middleware
- API endpoints
-
Core Layer (
AppRequestPortal.Core)- Domain models
- Business interfaces
- Business logic (services)
-
Infrastructure Layer (
AppRequestPortal.Infrastructure)- Data access (Entity Framework)
- External service integrations (Graph API)
- Repository implementations
3. Database (Azure SQL)
Section titled “3. Database (Azure SQL)”The database is managed by Entity Framework Core with automatic migrations applied on startup. The schema contains 19 tables organized into the following categories:
Table summary
Section titled “Table summary”| Table | Purpose | Type |
|---|---|---|
Apps | Intune app catalog synced from Microsoft Intune | Core |
AppRequests | User app requests with status tracking | Core |
AppApprovers | Per-app approver assignments | Core |
AuditLogs | Complete audit trail of all portal actions | Core |
ApprovalWorkflows | Per-app approval workflow configuration | Approval |
ApprovalStages | Ordered stages within a workflow | Approval |
WorkflowConditions | Conditional logic per approval stage | Approval |
RequestApprovals | Per-request approval decisions at each stage | Approval |
PortalSettings | Global portal configuration (singleton) | Config |
BrandingSettings | Portal branding/theming (singleton) | Config |
LicenseInfo | PowerStacks license status (singleton) | Config |
VendorLicenseAcceptances | Vendor license agreement acceptance (singleton) | Config |
CategorySettings | Custom category colors and icons | Config |
TermsOfService | Terms of Service versions | Compliance |
TermsAcceptances | User acceptance records for TOS | Compliance |
PackagingJobs | WinGet-to-Intune packaging pipeline jobs | Packaging |
WingetPackageCache | Cached WinGet package catalog | Packaging |
AppVersionHistories | App version tracking for updates | Updates |
BotConversationReferences | Teams bot conversation references for proactive messaging | Notifications |
Core tables
Section titled “Core tables”-- Apps: Intune application catalogCREATE TABLE Apps ( Id NVARCHAR(450) PRIMARY KEY, IntuneAppId NVARCHAR(255) NOT NULL UNIQUE, DisplayName NVARCHAR(255) NOT NULL, Description NVARCHAR(MAX), Publisher NVARCHAR(255), Version NVARCHAR(50), Architecture NVARCHAR(MAX), -- x64, x86, arm64 Locale NVARCHAR(MAX), -- e.g. en-US IconUrl NVARCHAR(MAX), IconBase64 NVARCHAR(MAX), IconContentType NVARCHAR(MAX), Category NVARCHAR(100), Cost DECIMAL(18,2), IsVisible BIT NOT NULL DEFAULT 1, RequiresApproval BIT NOT NULL DEFAULT 0, IsFeatured BIT NOT NULL DEFAULT 0, AssignmentType INT NOT NULL DEFAULT 0, -- 0=User, 1=Device TargetGroupId NVARCHAR(100), TargetGroupName NVARCHAR(255), AzureADGroupId NVARCHAR(MAX), AzureADGroupName NVARCHAR(MAX), IntuneAppType NVARCHAR(MAX), -- e.g. win32LobApp Platform INT NOT NULL DEFAULT 0, -- 0=Unknown, 1=Windows, 2=iOS, 3=Android, 4=macOS IsSupportedAppType BIT NOT NULL DEFAULT 0, IntuneAssignmentId NVARCHAR(MAX), FilterId NVARCHAR(MAX), FilterName NVARCHAR(MAX), FilterType INT NOT NULL DEFAULT 0, InstallBehavior NVARCHAR(MAX) DEFAULT 'System', DeviceRestartBehavior NVARCHAR(MAX) DEFAULT 'BasedOnReturnCode', EndUserNotification NVARCHAR(MAX) DEFAULT 'ShowAll', AllowAvailableUninstall BIT NOT NULL DEFAULT 1, DeliveryOptimizationPriority INT NOT NULL DEFAULT 0, RestartGracePeriodEnabled BIT NOT NULL DEFAULT 1, RestartGracePeriodMinutes INT NOT NULL DEFAULT 1440, RestartCountdownMinutes INT NOT NULL DEFAULT 15, AllowSnoozeRestart BIT NOT NULL DEFAULT 1, SnoozeDurationMinutes INT NOT NULL DEFAULT 240, -- Mobile app fields IsVppApp BIT NOT NULL DEFAULT 0, VppTokenId NVARCHAR(MAX), VppLicenseCount INT, VppUsedLicenseCount INT, BundleId NVARCHAR(MAX), MinimumOsVersion NVARCHAR(MAX), StoreUrl NVARCHAR(MAX), -- Update tracking SourceWingetPackageId NVARCHAR(255), LatestAvailableVersion NVARCHAR(MAX), LastUpdateCheckDate DATETIME2, UpdateAvailable BIT NOT NULL DEFAULT 0, -- Acknowledgment RequiresAcknowledgment BIT NOT NULL DEFAULT 0, AcknowledgmentText NVARCHAR(MAX), CreatedDate DATETIME2 NOT NULL, LastSyncDate DATETIME2 NOT NULL);
-- AppRequests: User app requestsCREATE TABLE AppRequests ( Id NVARCHAR(450) PRIMARY KEY, AppId NVARCHAR(450) NOT NULL, UserId NVARCHAR(450) NOT NULL, UserEmail NVARCHAR(255) NOT NULL, UserDisplayName NVARCHAR(255) NOT NULL, DeviceId NVARCHAR(MAX), DeviceName NVARCHAR(MAX), Status INT NOT NULL DEFAULT 0, -- 0=Pending, 1=Approved, 2=Rejected, 3=Completed, 4=Cancelled Justification NVARCHAR(1000), RequestedDate DATETIME2 NOT NULL, ReviewedDate DATETIME2, ReviewedBy NVARCHAR(MAX), ReviewerEmail NVARCHAR(MAX), ReviewComments NVARCHAR(1000), RejectedReason NVARCHAR(MAX), CompletedDate DATETIME2, CurrentApprovalStage INT NOT NULL DEFAULT 0, LastReminderSentDate DATETIME2, ReminderCount INT NOT NULL DEFAULT 0, -- On-behalf-of fields RequestedOnBehalfOfUserId NVARCHAR(MAX), RequestedOnBehalfOfEmail NVARCHAR(MAX), RequestedOnBehalfOfDisplayName NVARCHAR(MAX), -- Install status tracking InstallStatus INT NOT NULL DEFAULT 0, InstallStatusLastChecked DATETIME2, InstallStatusErrorMessage NVARCHAR(MAX), IntuneAssignmentId NVARCHAR(MAX), -- Escalation LastEscalationDate DATETIME2, EscalationCount INT NOT NULL DEFAULT 0, -- Actionable emails ActionToken NVARCHAR(MAX), FOREIGN KEY (AppId) REFERENCES Apps(Id) ON DELETE RESTRICT);
-- AppApprovers: Per-app approver assignmentsCREATE TABLE AppApprovers ( Id NVARCHAR(450) PRIMARY KEY, AppId NVARCHAR(450) NOT NULL, UserId NVARCHAR(450) NOT NULL, UserEmail NVARCHAR(255) NOT NULL, UserDisplayName NVARCHAR(255) NOT NULL, ApproverType INT NOT NULL DEFAULT 0, -- 0=Designated, 1=Manager CreatedDate DATETIME2 NOT NULL, FOREIGN KEY (AppId) REFERENCES Apps(Id) ON DELETE CASCADE, UNIQUE (AppId, UserId));
-- AuditLogs: Complete audit trailCREATE TABLE AuditLogs ( Id NVARCHAR(450) PRIMARY KEY, UserId NVARCHAR(450) NOT NULL, UserEmail NVARCHAR(255) NOT NULL, Action NVARCHAR(100) NOT NULL, EntityType NVARCHAR(100) NOT NULL, EntityId NVARCHAR(450) NOT NULL, Details NVARCHAR(MAX), -- JSON payload Timestamp DATETIME2 NOT NULL, IpAddress NVARCHAR(50) NOT NULL);Approval workflow tables
Section titled “Approval workflow tables”-- ApprovalWorkflows: Per-app workflow configurationCREATE TABLE ApprovalWorkflows ( Id NVARCHAR(450) PRIMARY KEY, AppId NVARCHAR(450) NOT NULL UNIQUE, -- One workflow per app RequireManagerApproval BIT NOT NULL DEFAULT 0, WorkflowType INT NOT NULL DEFAULT 0, -- 0=Pooled, 1=Linear CreatedDate DATETIME2 NOT NULL, ModifiedDate DATETIME2, ModifiedBy NVARCHAR(255), FOREIGN KEY (AppId) REFERENCES Apps(Id) ON DELETE CASCADE);
-- ApprovalStages: Ordered stages within a workflowCREATE TABLE ApprovalStages ( Id NVARCHAR(450) PRIMARY KEY, WorkflowId NVARCHAR(450) NOT NULL, StageOrder INT NOT NULL, -- 1-based ordering -- Linear workflow fields ApproverUserId NVARCHAR(MAX), ApproverEmail NVARCHAR(255), ApproverDisplayName NVARCHAR(255), -- Pooled workflow fields ApproverGroupId NVARCHAR(MAX), ApproverGroupName NVARCHAR(255), -- Conditional logic HasConditions BIT NOT NULL DEFAULT 0, ConditionDescription NVARCHAR(500), CreatedDate DATETIME2 NOT NULL, FOREIGN KEY (WorkflowId) REFERENCES ApprovalWorkflows(Id) ON DELETE CASCADE);
-- WorkflowConditions: Conditional logic per approval stageCREATE TABLE WorkflowConditions ( Id NVARCHAR(450) PRIMARY KEY, StageId NVARCHAR(100) NOT NULL, Type NVARCHAR(50) NOT NULL DEFAULT 'Always', -- Always, Cost, Category, Platform, Publisher, Department Operator NVARCHAR(50) NOT NULL DEFAULT 'Equals', -- Equals, NotEquals, GreaterThan, etc. Value NVARCHAR(500), Value2 NVARCHAR(500), -- For range conditions LogicalOperator NVARCHAR(10) DEFAULT 'And', -- And, Or FOREIGN KEY (StageId) REFERENCES ApprovalStages(Id) ON DELETE CASCADE);
-- RequestApprovals: Per-request approval decisions at each stageCREATE TABLE RequestApprovals ( Id NVARCHAR(450) PRIMARY KEY, RequestId NVARCHAR(450) NOT NULL, StageOrder INT NOT NULL, -- 0=manager, 1+=workflow stages StageType INT NOT NULL, -- 0=Manager, 1=Linear, 2=Pooled Status INT NOT NULL DEFAULT 0, -- 0=Pending, 1=Approved, 2=Rejected, 3=Skipped ApprovedByUserId NVARCHAR(MAX), ApprovedByEmail NVARCHAR(255), ApprovedByDisplayName NVARCHAR(255), DecisionDate DATETIME2, Comments NVARCHAR(1000), -- Pooled stage fields GroupId NVARCHAR(MAX), GroupName NVARCHAR(255), -- Linear stage fields ExpectedApproverUserId NVARCHAR(MAX), ExpectedApproverEmail NVARCHAR(255), CreatedDate DATETIME2 NOT NULL, FOREIGN KEY (RequestId) REFERENCES AppRequests(Id) ON DELETE CASCADE);Configuration tables (singletons)
Section titled “Configuration tables (singletons)”-- PortalSettings: Global portal configuration (always Id=1)CREATE TABLE PortalSettings ( Id INT PRIMARY KEY, -- Always 1 -- Email notification settings EmailSendAsUserId NVARCHAR(100), EmailFromAddress NVARCHAR(255), EmailPortalUrl NVARCHAR(500), EmailNotificationsEnabled BIT NOT NULL DEFAULT 0, EmailNotifyOnSubmitted BIT NOT NULL DEFAULT 1, EmailNotifyOnApproved BIT NOT NULL DEFAULT 1, EmailNotifyOnRejected BIT NOT NULL DEFAULT 1, EmailNotifyOnApprovalRequired BIT NOT NULL DEFAULT 1, EmailNotifyOnInstalled BIT NOT NULL DEFAULT 1, EmailNotifyOnAppPublished BIT NOT NULL DEFAULT 1, -- Actionable email settings ActionableEmailsEnabled BIT NOT NULL DEFAULT 1, ActionableEmailApiBaseUrl NVARCHAR(MAX), ActionableEmailOriginatorId NVARCHAR(MAX), -- Teams Bot notification settings TeamsBotEnabled BIT NOT NULL DEFAULT 0, TeamsBotAppId NVARCHAR(MAX), TeamsBotNotifyOnApprovalRequired BIT NOT NULL DEFAULT 1, TeamsBotNotifyOnApproved BIT NOT NULL DEFAULT 1, TeamsBotNotifyOnRejected BIT NOT NULL DEFAULT 1, TeamsBotNotifyOnInstalled BIT NOT NULL DEFAULT 1, TeamsBotNotifyOnAppPublished BIT NOT NULL DEFAULT 1, -- Approval reminders ApprovalRemindersEnabled BIT NOT NULL DEFAULT 0, ReminderIntervalDays INT NOT NULL DEFAULT 2, MaxReminderCount INT NOT NULL DEFAULT 3, -- Group authorization AdminGroupId NVARCHAR(100), AdminGroupName NVARCHAR(255), ApproverGroupId NVARCHAR(100), ApproverGroupName NVARCHAR(255), UserAccessGroupId NVARCHAR(MAX), UserAccessGroupName NVARCHAR(MAX), RequestOnBehalfGroupId NVARCHAR(MAX), RequestOnBehalfGroupName NVARCHAR(MAX), -- General settings RequireManagerApproval BIT NOT NULL DEFAULT 1, AutoCreateGroups BIT NOT NULL DEFAULT 1, GroupNamePrefix NVARCHAR(MAX) DEFAULT 'AppStore-', -- Display settings MaxFeaturedApps INT NOT NULL DEFAULT 0, DarkModeEnabled BIT NOT NULL DEFAULT 0, HeroAppId NVARCHAR(MAX), -- WinGet settings WingetRepoUrl NVARCHAR(MAX), GitHubPersonalAccessToken NVARCHAR(MAX), WingetUpdateCheckEnabled BIT NOT NULL DEFAULT 0, WingetUpdateCheckIntervalHours INT NOT NULL DEFAULT 24, WingetUpdateNotificationsEnabled BIT NOT NULL DEFAULT 0, -- Auto-update settings AutoCheckUpdates BIT NOT NULL DEFAULT 1, ShowUpdateNotification BIT NOT NULL DEFAULT 1, ReleaseChannel NVARCHAR(MAX) DEFAULT 'Latest', -- Reports & ROI settings HelpDeskCostPerTicket DECIMAL(18,2) NOT NULL DEFAULT 22.00, CurrencyCode NVARCHAR(10) DEFAULT 'USD', CurrencySymbol NVARCHAR(10) DEFAULT '$', -- SLA settings SlaTrackingEnabled BIT NOT NULL DEFAULT 0, SlaTargetApprovalHours INT NOT NULL DEFAULT 24, SlaWarningThresholdHours INT NOT NULL DEFAULT 18, SlaSendBreachAlerts BIT NOT NULL DEFAULT 1, SlaSendWarningAlerts BIT NOT NULL DEFAULT 1, SlaBusinessHoursOnly BIT NOT NULL DEFAULT 0, SlaBusinessDayStartHour INT NOT NULL DEFAULT 9, SlaBusinessDayEndHour INT NOT NULL DEFAULT 17, -- Request escalation RequestEscalationEnabled BIT NOT NULL DEFAULT 0, EscalationThresholdHours INT NOT NULL DEFAULT 48, EscalationRecipientEmail NVARCHAR(MAX), EscalationRecipientGroupId NVARCHAR(MAX), EscalationRecipientGroupName NVARCHAR(MAX), -- Company info CompanyName NVARCHAR(MAX), CompanyLogoBase64 NVARCHAR(MAX), CompanyLogoContentType NVARCHAR(MAX), SupportEmail NVARCHAR(MAX), SupportPhone NVARCHAR(MAX), -- Intune sync LastIntuneSyncDate DATETIME2, -- Metadata LastModified DATETIME2 NOT NULL, ModifiedBy NVARCHAR(255));
-- BrandingSettings: Portal theming (always Id=1)CREATE TABLE BrandingSettings ( Id INT PRIMARY KEY, -- Always 1 LogoBase64 NVARCHAR(MAX), LogoContentType NVARCHAR(50) DEFAULT 'image/webp', FaviconBase64 NVARCHAR(MAX), FaviconContentType NVARCHAR(50), PrimaryColor NVARCHAR(20) DEFAULT '#0078d4', PrimaryColorHover NVARCHAR(20) DEFAULT '#106ebe', SecondaryColor NVARCHAR(20) DEFAULT '#6c757d', HeaderTextColor NVARCHAR(20) DEFAULT '#ffffff', BackgroundColor NVARCHAR(20) DEFAULT '#f5f5f5', CardBackgroundColor NVARCHAR(20) DEFAULT '#ffffff', PortalTitle NVARCHAR(100) DEFAULT 'App Store for Intune', Tagline NVARCHAR(MAX), WelcomeMessage NVARCHAR(500), FooterText NVARCHAR(500), SupportContactInfo NVARCHAR(500), LastModified DATETIME2 NOT NULL, ModifiedBy NVARCHAR(255));
-- LicenseInfo: PowerStacks license status (always Id=1)CREATE TABLE LicenseInfo ( Id INT PRIMARY KEY, -- Always 1 Status NVARCHAR(50) DEFAULT 'unknown', Enabled BIT NOT NULL DEFAULT 0, LicenseType NVARCHAR(50), StartDate DATETIME2, EndDate DATETIME2, LicensedDeviceCount INT, CurrentDeviceCount INT NOT NULL DEFAULT 0, AuthorizedTenants NVARCHAR(MAX), -- JSON array TenantId NVARCHAR(100), IsTenantAuthorized BIT NOT NULL DEFAULT 0, IsWithinDeviceLimit BIT NOT NULL DEFAULT 1, LastValidated DATETIME2, LastDeviceCountUpdate DATETIME2, ErrorMessage NVARCHAR(500), ApiKey NVARCHAR(MAX));
-- VendorLicenseAcceptances: Vendor license agreement (always Id=1)CREATE TABLE VendorLicenseAcceptances ( Id INT PRIMARY KEY, -- Always 1 AcceptedVersion NVARCHAR(20) NOT NULL, AcceptedByUserId NVARCHAR(100) NOT NULL, AcceptedByEmail NVARCHAR(255) NOT NULL, AcceptedByDisplayName NVARCHAR(255), AcceptedDate DATETIME2 NOT NULL, IpAddress NVARCHAR(50), UserAgent NVARCHAR(500), TenantId NVARCHAR(100));
-- CategorySettings: Custom category colors and iconsCREATE TABLE CategorySettings ( Id INT PRIMARY KEY IDENTITY, CategoryName NVARCHAR(100) NOT NULL UNIQUE, Color NVARCHAR(20), -- Hex e.g. '#0078d4' Icon NVARCHAR(255));Compliance tables
Section titled “Compliance tables”-- TermsOfService: TOS versionsCREATE TABLE TermsOfService ( Id NVARCHAR(450) PRIMARY KEY, Version NVARCHAR(20) NOT NULL DEFAULT '1.0', Title NVARCHAR(255) NOT NULL, Content NVARCHAR(MAX) NOT NULL, ChangesSummary NVARCHAR(1000), IsActive BIT NOT NULL DEFAULT 0, IsRequired BIT NOT NULL DEFAULT 1, CreatedDate DATETIME2 NOT NULL, PublishedDate DATETIME2, CreatedBy NVARCHAR(255), ModifiedBy NVARCHAR(255), ModifiedDate DATETIME2);
-- TermsAcceptances: User acceptance recordsCREATE TABLE TermsAcceptances ( Id NVARCHAR(450) PRIMARY KEY, TermsOfServiceId NVARCHAR(450) NOT NULL, UserId NVARCHAR(100) NOT NULL, UserEmail NVARCHAR(255) NOT NULL, UserDisplayName NVARCHAR(255), AcceptedDate DATETIME2 NOT NULL, IpAddress NVARCHAR(50), UserAgent NVARCHAR(500), FOREIGN KEY (TermsOfServiceId) REFERENCES TermsOfService(Id) ON DELETE CASCADE, UNIQUE (TermsOfServiceId, UserId));Packaging & update tables
Section titled “Packaging & update tables”-- PackagingJobs: WinGet-to-Intune packaging pipelineCREATE TABLE PackagingJobs ( Id INT PRIMARY KEY IDENTITY, JobId NVARCHAR(50) NOT NULL UNIQUE, WingetPackageId NVARCHAR(255) NOT NULL, PackageName NVARCHAR(255) NOT NULL, PackageVersion NVARCHAR(50), Publisher NVARCHAR(255), Description NVARCHAR(MAX), IconBase64 NVARCHAR(MAX), Status NVARCHAR(50) NOT NULL DEFAULT 'Pending', IntunewinBlobUrl NVARCHAR(500), IntuneAppId NVARCHAR(100), ErrorMessage NVARCHAR(MAX), CreatedBy NVARCHAR(255) NOT NULL, CreatedAt DATETIME2 NOT NULL, CompletedAt DATETIME2, StatusMessage NVARCHAR(500), PackageSize BIGINT, Architecture NVARCHAR(MAX), Locale NVARCHAR(MAX), InstallCommand NVARCHAR(1000), UninstallCommand NVARCHAR(1000), SilentInstallSwitch NVARCHAR(MAX), AppsAndFeaturesEntriesJson NVARCHAR(MAX), UsePsadt BIT NOT NULL DEFAULT 1);
-- WingetPackageCache: Cached WinGet catalogCREATE TABLE WingetPackageCache ( Id INT PRIMARY KEY IDENTITY, PackageId NVARCHAR(MAX) NOT NULL, Name NVARCHAR(MAX) NOT NULL, Publisher NVARCHAR(MAX) NOT NULL, LatestVersion NVARCHAR(MAX) NOT NULL, Description NVARCHAR(MAX), Homepage NVARCHAR(MAX), License NVARCHAR(MAX), IconUrl NVARCHAR(MAX), TagsJson NVARCHAR(MAX), -- JSON array InstallersJson NVARCHAR(MAX), -- JSON array CreatedAt DATETIME2 NOT NULL, LastUpdated DATETIME2 NOT NULL, ExpiresAt DATETIME2 NOT NULL, PopularityRank INT, Category NVARCHAR(MAX));
-- AppVersionHistories: Version tracking for app updatesCREATE TABLE AppVersionHistories ( Id NVARCHAR(450) PRIMARY KEY, AppId NVARCHAR(100) NOT NULL, Version NVARCHAR(50) NOT NULL, RecordedAt DATETIME2 NOT NULL, UpdatedByUserId NVARCHAR(100), UpdatedByUserName NVARCHAR(255), WingetPackageId NVARCHAR(255), InstallerUrl NVARCHAR(1000), InstallerHash NVARCHAR(100), -- SHA256 IntuneAppId NVARCHAR(100), ReleaseNotes NVARCHAR(2000), IsRollbackAvailable BIT NOT NULL DEFAULT 1, InstallerSizeBytes BIGINT, Status NVARCHAR(50) DEFAULT 'Archived', -- Current, Archived, RolledBack FOREIGN KEY (AppId) REFERENCES Apps(Id) ON DELETE CASCADE, UNIQUE (AppId, Version));Notification tables
Section titled “Notification tables”-- BotConversationReferences: Teams bot proactive messaging referencesCREATE TABLE BotConversationReferences ( Id INT PRIMARY KEY IDENTITY, UserId NVARCHAR(100) NOT NULL UNIQUE, -- Microsoft Entra ID object ID UserEmail NVARCHAR(255), ConversationId NVARCHAR(255) NOT NULL, ServiceUrl NVARCHAR(500) NOT NULL, ConversationReferenceJson NVARCHAR(MAX) NOT NULL, InstalledDate DATETIME2 NOT NULL, LastActivityDate DATETIME2 NOT NULL);4. Microsoft Graph API integration
Section titled “4. Microsoft Graph API integration”Purpose:
- Retrieve apps from Intune
- Manage Microsoft Entra ID groups
- Add/remove users and devices from groups
- Get user information and manager hierarchy
Permissions required:
DeviceManagementApps.Read.AllDeviceManagementApps.ReadWrite.AllDeviceManagementConfiguration.Read.All(for the assignment-filter picker in ring deployment settings)DeviceManagementManagedDevices.Read.All(for device count)Group.ReadWrite.AllUser.Read.AllDirectory.Read.AllMail.Send(optional, for email notifications)
Key operations:
// Get Intune appsGET /deviceAppManagement/mobileApps
// Create Microsoft Entra ID groupPOST /groups
// Add user to groupPOST /groups/{group-id}/members/$ref
// Get user's managerGET /users/{user-id}/manager
// Get user's devicesGET /users/{user-id}/managedDevices5. Azure Key Vault
Section titled “5. Azure Key Vault”Purpose: Secure storage for application secrets
Secrets stored:
AzureAdClientSecret: Microsoft Entra ID client secretSqlConnectionString: SQL Database connection stringStorageConnectionString: Azure Storage connection string
Access: App Service Managed Identity with Get and List permissions only. Secrets are referenced via Key Vault references in App Settings (e.g., @Microsoft.KeyVault(SecretUri=...)).
6. Azure Storage account
Section titled “6. Azure Storage account”Purpose: Queue and blob storage for the cloud packaging pipeline
Containers/queues:
packaging-jobsqueue: Job messages for the packaging agentintunewin-packagesblob container: Packaged.intunewinfiles and PSADT templates
7. Azure Bot (optional)
Section titled “7. Azure Bot (optional)”Purpose: Send personal Teams notifications to users via Bot Framework proactive messaging
Configuration:
- Reuses the Backend API Microsoft Entra ID app registration (same Client ID/Secret)
- Messaging endpoint:
https://{app-url}/api/messages - SingleTenant, Teams channel enabled
- No additional Microsoft Graph permissions required
Key files:
src/AppRequestPortal.API/Bot/AppRequestBot.cs: Handles install/uninstall eventssrc/AppRequestPortal.API/Controllers/BotController.cs: Bot messaging endpointsrc/AppRequestPortal.Infrastructure/Services/TeamsBotService.cs: Proactive notification service
Data flow
Section titled “Data flow”User request flow
Section titled “User request flow”1. User browses apps Frontend → API → Database → Apps list
2. User submits request Frontend → API → Database (save request) → Email service (notify approvers) → Teams bot (notify users)
3. Approver reviews request Frontend → API → Database (update status) → Email service (notify requester) → Teams bot (notify users)
4. System processes approved request API → Graph API (create/get Microsoft Entra ID group) → Graph API (add user/device to group) → Intune (app deployment triggered automatically) → Database (update request status) → Email service (notify requester) → Teams bot (notify users)Notification services
Section titled “Notification services”The portal uses two notification channels:
| Service | Purpose | Technology |
|---|---|---|
| EmailNotificationService | Direct user notifications | Microsoft Graph Mail.Send API |
| TeamsBotService | Personal Teams notifications | Bot Framework proactive messaging with Adaptive Cards |
Both services are optional and can be enabled/disabled independently in the Admin Communications tab. The Teams Bot sends personal 1:1 messages to users via stored conversation references (the bot must be pre-installed for users via Teams Admin Center setup policies).
App sync flow
Section titled “App sync flow”1. Admin triggers sync Frontend → API → Graph API (get Intune apps) → Graph API (get managed device count, last 30 days) → PowerStacks License API (validate license) → Database (update apps, device count, license info)
2. Scheduled sync (background job) Background Service → Graph API (get Intune apps) → Graph API (get managed device count) → PowerStacks License API (validate license) → Database (update apps, device count, license info)Security architecture
Section titled “Security architecture”Authentication flow
Section titled “Authentication flow”1. User visits frontend2. MSAL redirects to Microsoft Entra ID sign-in3. User authenticates4. Microsoft Entra ID returns ID token and access token5. Frontend stores tokens in session storage6. Frontend includes access token in API requests7. API validates JWT token8. API authorizes based on user roles/claimsAuthorization levels
Section titled “Authorization levels”- User: Can browse apps and submit requests
- Approver: Can approve/reject requests for assigned apps
- Admin: Can manage apps, approvers, and view all requests
Security features
Section titled “Security features”- HTTPS Only: All communication encrypted in transit
- JWT Authentication: Stateless authentication with Microsoft Entra ID
- RBAC: Role-based access control for granular permissions
- Conditional Access: Integration with Microsoft Entra ID Conditional Access policies
- Managed Identity: Service-to-service authentication without secrets
- Key Vault: Secure storage of secrets and certificates
- Audit Logging: Complete audit trail of all actions
Scalability considerations
Section titled “Scalability considerations”Horizontal scaling
Section titled “Horizontal scaling”- App Services: Scale out to multiple instances
- Database: Use Azure SQL elastic pools or scale up tiers
- Frontend: Served via CDN for global distribution
Caching strategy
Section titled “Caching strategy”// Cache app list (1 hour TTL)[ResponseCache(Duration = 3600)]public async Task<IActionResult> GetApps()
// Cache user's devices (30 minutes TTL)[ResponseCache(Duration = 1800, VaryByQueryKeys = new[] { "userId" })]public async Task<IActionResult> GetUserDevices(string userId)Performance optimizations
Section titled “Performance optimizations”- Database indexing: Indexes on frequently queried columns
- Connection pooling: Efficient database connection management
- Async/await: Non-blocking I/O operations
- Lazy loading: Load data only when needed
- Pagination: Limit result sets for large queries
Monitoring and observability
Section titled “Monitoring and observability”Application Insights integration
Section titled “Application Insights integration”// Custom telemetrytelemetryClient.TrackEvent("AppRequestApproved", new Dictionary<string, string>{ { "AppId", appId }, { "UserId", userId }, { "ProcessingTime", processingTime.ToString() }});
// Dependency trackingtelemetryClient.TrackDependency("GraphAPI", "GetIntuneApps", startTime, duration, success);Key metrics
Section titled “Key metrics”- Request rate and response times
- Failure rates and error types
- Graph API call latency
- Database query performance
- User authentication success rate
- App request approval time
Logging
Section titled “Logging”// Structured logging with Serilog or built-in ILogger_logger.LogInformation( "App request {RequestId} approved by {ReviewerId} for user {UserId}", requestId, reviewerId, userId);
_logger.LogError( exception, "Failed to add user {UserId} to group {GroupId}", userId, groupId);Disaster recovery
Section titled “Disaster recovery”The default deployment includes Tier 2 disaster recovery capabilities with geo-redundant backups across Azure regions.
Built-in protection (default)
Section titled “Built-in protection (default)”| Component | Protection | RPO | RTO |
|---|---|---|---|
| SQL Database | Automated backups + geo-redundant storage | 5 min | 1-2 hours |
| Storage Account | Geo-redundant (GRS) - 6 copies across 2 regions | 0 | 1-2 hours |
| Key Vault | Soft delete (7-day recovery window) | 0 | 15 min |
| Application | GitHub releases + ARM template | 0 | 30 min |
Backup details
Section titled “Backup details”- SQL Point-in-Time Restore: 7 days (Basic tier), up to 35 days (Standard+)
- SQL Geo-Backup: Cross-region backup for regional disaster recovery
- Storage GRS: Synchronous replication to paired Azure region
- Key Vault Soft Delete: 7-day retention for accidental deletion recovery
High availability options (Tier 3)
Section titled “High availability options (Tier 3)”For organizations requiring higher uptime, these can be configured manually:
┌─────────────────────────────────────────────────────────────┐│ Traffic Manager ││ (DNS-based failover) │└──────────────────────┬───────────────────┬──────────────────┘ │ │ ┌───────────▼───────┐ ┌────────▼────────┐ │ Primary Region │ │ Secondary Region │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ App Service │ │ │ │ App Service │ │ │ └──────┬──────┘ │ │ └──────┬──────┘ │ │ │ │ │ │ │ │ ┌──────▼──────┐ │ │ ┌──────▼──────┐ │ │ │ SQL (R/W) │←─┼─┼─┤ SQL (Read) │ │ │ └─────────────┘ │ │ └─────────────┘ │ └───────────────────┘ └─────────────────┘ Active Standby- SQL Active Geo-Replication: ~$5/month (Basic replica)
- Traffic Manager: ~$0.75/million queries
- Secondary App Service: ~$55/month (B2)
Recovery procedures
Section titled “Recovery procedures”See the Disaster Recovery Guide for detailed runbooks covering:
- Accidental data deletion recovery
- Key Vault secret recovery
- Complete region failure recovery
- Application rollback procedures
Admin features
Section titled “Admin features”Setup Wizard
Section titled “Setup Wizard”A guided setup wizard helps administrators configure the portal on first use:
- License - Enter and validate PowerStacks license key
- Access Groups - Configure admin and approver Microsoft Entra ID groups
- Email Notifications - Set up email sender identity and notification preferences
- Sync Apps - Import apps from Intune catalog
Branding customization
Section titled “Branding customization”Administrators can fully customize the portal appearance:
- Logo & Favicon - Upload custom images (PNG, JPG, SVG, ICO)
- Colors - Primary color, hover color, secondary color, header text color, background color, card background
- Text - Portal title, tagline, welcome message, footer text, support contact info
Dark mode
Section titled “Dark mode”- Admin-controlled default dark mode setting
- User toggle to override admin preference (persisted in localStorage)
- Respects system preference when user hasn’t set a preference
- Full dark mode styling for all components
App categories
Section titled “App categories”Apps can be organized into categories for easier browsing:
- Categories are synced from Intune app metadata
- Apps display their category on the Browse Apps page
- Filter apps by category
Multi-stage approval workflows
Section titled “Multi-stage approval workflows”Custom approval workflows can be configured per app:
- Multiple stages - Define sequential approval stages
- Stage types - Manager approval, specific user approval, or group approval
- Notifications - Email notifications at each stage
- Tracking - View current approval stage in pending approvals list
App catalog integration
Section titled “App catalog integration”Import apps from the Windows Package Manager (WinGet) repository:
- Search the WinGet catalog by name
- View app details including publisher, version, and description
- Create Intune Win32 apps from WinGet packages (requires packaging service)
Reports & analytics
Section titled “Reports & analytics”The portal includes a full Reports tab for administrators with the following capabilities:
ROI calculator
Section titled “ROI calculator”- Calculates cost savings based on completed app requests
- Formula:
completedRequests × costPerTicket = totalSavings - Default help desk cost: $22/ticket (industry benchmark for Tier 1 support)
- Configurable currency (USD, EUR, GBP, CAD, AUD, JPY, CHF, INR)
- Time period filters: Last 30 days, 90 days, Year, All time
App-based reports
Section titled “App-based reports”- View total requests per app
- Breakdown by status (completed, pending, rejected)
- User history for each app
Person-based reports
Section titled “Person-based reports”- Search users who have made requests
- View all apps requested by a specific user
- Request dates and status tracking
Approval analytics
Section titled “Approval analytics”- Top 10 approvers by approval count
- Pending approvals count
- Average approval time (hours)
- Common rejection reasons
Licensing system
Section titled “Licensing system”The portal integrates with PowerStacks License API for device-based licensing:
License validation
Section titled “License validation”- API endpoint:
https://api.powerstacks.com/powerbi-app/auth - Validates license status, device count limits, and tenant authorization
- Auto-validates every 24 hours or on-demand via admin panel
Device count enforcement
Section titled “Device count enforcement”- Counts managed devices from Intune that have checked in within last 30 days
- Device count is updated during each app sync
- 3% grace period: If device count exceeds the licensed limit by up to 3%, the portal remains operational but displays a warning banner to users
- If device count exceeds the licensed limit plus the 3% grace period, new app requests are blocked
Security measures
Section titled “Security measures”- License validation forces API check for critical operations (not just cached data)
- Prevents database tampering from bypassing license enforcement
- Displays warning banners to end users when license issues occur
License configuration
Section titled “License configuration”The license API key can be configured in two ways:
-
Via Admin Dashboard (recommended): Go to Admin Dashboard → License tab and enter the API key in the “License Key” field. This stores the key in the database.
-
Via Setup Wizard: During initial setup, enter the API key in the License step of the Setup Wizard.
-
Via
appsettings.json(fallback): Add to your configuration file:
{ "License": { "ApiKey": "your-powerstacks-api-key" }}The system checks for the API key in the following order:
- Database (LicenseInfo.ApiKey)
- Configuration file (License:ApiKey)
Database tables
Section titled “Database tables”For column-level detail of the SQL data model, see the EF entity classes under src/AppRequestPortal.Core/Models/ and the migrations under src/AppRequestPortal.Infrastructure/Migrations/ in the App Store source repository; the EF source is the schema authority.
Future enhancements
Section titled “Future enhancements”- Power Automate integration: Visual workflow designer for approvals
- Real-time notifications: SignalR for live updates
Advanced analytics: Usage reports and insights dashboardImplemented- Mobile app: Native mobile app for iOS/Android
- Self-service group management: Let users manage their own app groups
App categories: Organize apps by category/departmentImplemented- Scheduled deployments: Schedule app installations for specific times
- Multi-language support: Internationalization (i18n)
Teams notifications: Notify Teams channels on request eventsImplemented- Teams approvals: Approve/reject directly from Teams Adaptive Cards