Architecture¶
Overview¶
The Intune Assignments Manager is a client-side single-page application built with SvelteKit 2. There is no server backend — all authentication and API communication happens directly in the browser. The app is deployed to Cloudflare Pages as static assets.
The core data flow is:
- User authenticates via Microsoft Entra ID using MSAL.js (OAuth2 PKCE popup flow)
- The app acquires access tokens and calls the Microsoft Graph API directly from the browser
- Graph API responses are validated with Zod schemas and rendered in the UI
System Flow¶
flowchart LR
Browser["Browser (SvelteKit SPA)"]
MSAL["MSAL.js"]
EntraID["Azure Entra ID"]
GraphClient["Graph Client"]
GraphAPI["Microsoft Graph API"]
Intune["Intune Backend"]
Browser --> MSAL
MSAL -->|OAuth2 PKCE| EntraID
EntraID -->|Access Token| MSAL
MSAL -->|Token| Browser
Browser --> GraphClient
GraphClient -->|Bearer Token| GraphAPI
GraphAPI --> Intune
Intune -->|Apps, Profiles, Groups| GraphAPI
GraphAPI -->|JSON| GraphClient
GraphClient -->|Validated Data| Browser
Authentication Flow¶
sequenceDiagram
participant User
participant App as SvelteKit App
participant MSAL as MSAL.js
participant Popup as Auth Popup
participant Entra as Azure Entra ID
User->>App: Click "Sign In"
App->>MSAL: Lazy-import & initialize
MSAL->>Popup: Open loginPopup()
Popup->>Entra: Redirect to login.microsoftonline.com
User->>Entra: Enter credentials & consent
Entra->>Popup: Return auth code
Popup->>MSAL: Exchange code for tokens (PKCE)
MSAL->>App: Return TokenResult (accessToken, scopes, account)
App->>App: Store account in auth rune state
App->>App: Merge granted scopes
Note over App,MSAL: Subsequent API calls
App->>MSAL: acquireTokenSilent()
alt Token cached & valid
MSAL->>App: Return cached token
else Interaction required
MSAL->>Popup: acquireTokenPopup()
Popup->>App: Return fresh token
end
MSAL is lazy-imported on first auth action to maintain SSR-safety (guarded by SvelteKit's browser check). See Authentication for full details.
Routing¶
SvelteKit file-based routing provides the following pages:
| Route | Page | Description |
|---|---|---|
/ |
Dashboard | Overview cards with app/profile counts, recent audit activity |
/apps |
App List | Browse all Intune mobile apps with search and filtering |
/apps/[id] |
App Detail | View a single app's properties and current assignments |
/profiles |
Profile List | Browse all configuration policies with search and filtering |
/profiles/[id] |
Profile Detail | View a single profile's properties and current assignments |
/assign |
Assignment Wizard | 5-step bulk assignment workflow |
/audit |
Audit Log | Browse Intune audit events |
/status |
Deployment Status | App install status via Reports API |
/settings |
Settings | Permission management and consent controls |
The root layout (src/routes/+layout.svelte) wraps all pages with authentication guards, navigation, and theme support.
State Management¶
The app uses pure Svelte 5 runes for all state management — no external state library (no Redux, no Svelte 4 stores).
Stores live in src/lib/stores/ and export reactive state via $state and $derived:
| Store | File | Purpose |
|---|---|---|
| Auth | auth.svelte.ts |
Current account, authentication state, granted scopes |
| Notifications | notifications.svelte.ts |
Toast notification queue (success, error, info) |
| Theme | theme.svelte.ts |
Dark/light mode preference |
| Permissions | permissions.svelte.ts |
Granted scopes, consent error tracking |
| Shortcuts | shortcuts.svelte.ts |
Keyboard shortcut registry |
| Command Palette | command-palette.svelte.ts |
Command palette open/close state |
| Dashboard Cache | dashboard-cache.svelte.ts |
Cached app/profile counts and recent activity |
| Filter Cache | filter-cache.ts |
Cached assignment filter definitions |
| Group Cache | group-cache.ts |
Cached group name resolutions |
| Graph Client | graph.ts |
Singleton graph client instance |
Three-Tier Type System¶
The codebase uses a layered approach to type safety:
flowchart TD
API["Microsoft Graph API Response (JSON)"]
ZOD["Zod Schemas (src/lib/types/schemas.ts)"]
TS["TypeScript Interfaces (src/lib/types/graph.ts)"]
BIZ["Business Logic Types (src/lib/types/wizard.ts)"]
API -->|Runtime validation| ZOD
ZOD -->|Parse & validate| TS
TS -->|Compose into| BIZ
-
TypeScript interfaces (
src/lib/types/graph.ts) define the shape of Graph API response objects —MobileApp,ConfigurationPolicy,MobileAppAssignment, etc. -
Zod schemas (
src/lib/types/schemas.ts) provide runtime validation when parsing API responses. ThefetchAllmethod uses schemas to validate each page of results. -
Business logic types (
src/lib/types/wizard.ts) compose Graph types into higher-level abstractions —AssignableItem,GroupTarget,ConflictChoice,AssignmentResult, etc.
Additional type files:
src/lib/types/diff.ts— Assignment diff types for comparing before/after statessrc/lib/types/status.ts— Status and reports typessrc/lib/types/status-schemas.ts— Zod schemas for Reports API responses
Graph API Client Architecture¶
The Graph client is a factory function that returns three methods:
flowchart TD
Factory["createGraphClient(getAccessToken)"]
Request["request<T>(path, options?)"]
FetchAll["fetchAll<T>(path, options?)"]
Batch["batch(requests[], options?)"]
Retry["Retry Logic (429 backoff, 401 re-auth)"]
Pagination["Pagination (@odata.nextLink)"]
Chunking["Auto-chunk at 20 requests"]
Factory --> Request
Factory --> FetchAll
Factory --> Batch
Request --> Retry
FetchAll --> Pagination
Batch --> Chunking
Chunking --> Request
request<T>()— Single HTTP request with automatic retry: exponential backoff on 429 (rate limit), one re-auth attempt on 401fetchAll<T>()— Paginated fetch that follows@odata.nextLinkup to a configurable page limit (default 50)batch()— Sends requests to the/$batchendpoint, auto-chunks at 20 requests per batch, retries individual 429 responses
All three methods default to the beta Graph API endpoint (https://graph.microsoft.com/beta). Pass version: 'v1.0' to use the stable endpoint.
See Graph API Client for the full deep dive.
Bulk Assignment Execution Flow¶
The bulk assignment wizard uses a three-phase execution model in src/lib/graph/execute.ts:
flowchart LR
subgraph "Phase 1: Fetch"
F1["Batch GET current assignments"]
F2["Fallback: individual fetch if paginated"]
end
subgraph "Phase 2: Merge"
M1["Combine existing + new assignments"]
M2["Detect conflicts (same group, different settings)"]
M3["Apply conflict resolutions (update or skip)"]
end
subgraph "Phase 3: Apply"
A1["Batch POST to /assign endpoints"]
A2["Retry 5xx errors (2x, exponential backoff)"]
end
F1 --> F2
F2 --> M1
M1 --> M2
M2 --> M3
M3 --> A1
A1 --> A2
Replace semantics
The Graph API assign endpoint replaces all assignments for an item. Existing assignments must always be fetched and merged first to avoid accidentally removing assignments that were not part of the current operation.
Merge Logic¶
The merge module (src/lib/graph/merge.ts) uses a target-key approach:
- Index existing assignments by a deterministic key (e.g.,
group::{groupId},exclusion::{groupId},allDevices,allUsers) - For each new assignment, check if that target key already exists
- If a conflict is found, apply the user's resolution choice: update (overwrite) or skip (keep existing)
- Return the full merged list to POST back to the API
Caching Strategy¶
| Data | Storage | TTL | Invalidation |
|---|---|---|---|
| Dashboard counts (apps, profiles, assigned) | localStorage |
15 minutes | Manual refresh, list page loads |
| Recent audit activity | localStorage |
15 minutes | Bundled with dashboard cache |
| Assignment filter definitions | In-memory (module scope) | Session lifetime | clearFilterCache() |
| Group name resolutions | In-memory (Map) |
Session lifetime | clearGroupCache() |
| MSAL tokens | localStorage (MSAL-managed) |
Token lifetime | MSAL handles refresh automatically |
| Granted scopes | localStorage |
Until sign-out | Cleared on logout |