# Architecture Research **Domain:** Single-container deployment package generator webapp (Intune + NinjaRMM) **Researched:** 2026-04-10 **Confidence:** HIGH (core patterns well-established; .intunewin internals MEDIUM — encryption layer may require IntuneWinAppUtil.exe rather than reimplementation) ## Standard Architecture ### System Overview ``` ┌──────────────────────────────────────────────────────────────────┐ │ Browser (SPA or SSR) │ │ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │ │ │ Printer │ │ Driver Library │ │ Package Export │ │ │ │ Config Form │ │ Browser │ │ (Download Trigger)│ │ │ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │ └──────────┼───────────────────┼──────────────────────┼────────────┘ │ HTTP/REST │ HTTP/REST │ HTTP/REST ┌──────────┼───────────────────┼──────────────────────┼────────────┐ │ ▼ ▼ ▼ │ │ API Layer (HTTP server) │ │ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │ │ │ /printers │ │ /drivers │ │ /packages │ │ │ │ CRUD routes │ │ upload routes │ │ generate routes │ │ │ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │ │ │ │ │ │ ├──────────┼───────────────────┼──────────────────────┼────────────┤ │ ▼ ▼ ▼ │ │ Service / Domain Layer │ │ ┌──────────────┐ ┌─────────────────┐ ┌────────────────────┐ │ │ │ Printer │ │ Driver │ │ Package Builder │ │ │ │ Service │ │ Service │ │ Service │ │ │ └──────┬───────┘ └────────┬────────┘ └──────────┬─────────┘ │ │ │ │ │ │ │ │ ┌────────┘ ┌────────┘ │ │ │ ▼ ▼ │ │ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │ │ │ Driver Store │ │ Package Generators │ │ │ │ │ (volume mount) │ │ ┌─────────┐ ┌───────────┐ │ │ │ │ └─────────────────┘ │ │ PS Tmpl │ │ intunewin │ │ │ │ │ │ │ Engine │ │ Builder │ │ │ │ │ │ └─────────┘ └───────────┘ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ ZIP │ │ │ │ │ │ │ Builder │ │ │ │ │ │ └─────────┘ │ │ │ │ └─────────────────────────────┘ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Data Layer │ │ │ │ ┌────────────────┐ ┌─────────────────┐ │ │ │ │ │ SQLite DB │ │ Filesystem │ │ │ │ │ │ (printers, │ │ (drivers/ vol, │ │ │ │ │ │ clients, │ │ tmp/ for │ │ │ │ │ │ driver refs) │ │ in-progress │ │ │ │ │ │ │ │ packages) │ │ │ │ │ └────────────────┘ └─────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────┘ ``` ### Component Responsibilities | Component | Responsibility | Typical Implementation | |-----------|----------------|------------------------| | API Layer | HTTP routing, request validation, response formatting | FastAPI (Python) or Hono/Express (Node) | | Printer Service | CRUD for printer configs, parameter validation | Plain service class over SQLite | | Driver Service | File upload handling, driver metadata storage, deduplication | Service + volume-mounted filesystem | | Package Builder Service | Orchestrates generation: invokes PS template engine, intunewin builder, ZIP builder | Coordinator service, calls sub-generators | | PowerShell Template Engine | Renders printer-specific PS install script from a template | Jinja2 or string template with parameter substitution | | IntuneWin Builder | Produces a valid .intunewin file from a source folder | Wraps bundled IntuneWinAppUtil.exe via subprocess call | | ZIP Builder | Produces NinjaRMM-ready ZIP: PS script + driver files | Python zipfile / Node archiver in-process | | SQLite DB | Stores printer configs, client groups, driver metadata references | SQLite via a lightweight ORM or raw queries | | Driver Store (volume) | Holds uploaded driver ZIP/INF blobs, persists across container restarts | Docker volume, addressed by content-hash filenames | | Temp workspace | Staging area for a build: assembled files before zipping | tmpfs or host-path temp dir, cleaned after download | ## Recommended Project Structure ``` imptune/ ├── api/ # HTTP route handlers │ ├── printers.py # /printers CRUD │ ├── drivers.py # /drivers upload + list │ ├── packages.py # /packages/generate endpoints │ └── clients.py # /clients group management ├── services/ │ ├── printer_service.py # Printer domain logic │ ├── driver_service.py # Driver file management │ └── package_builder.py # Orchestrates package generation ├── generators/ │ ├── ps_template.py # PowerShell script renderer │ ├── intunewin_builder.py # .intunewin packaging (calls IntuneWinAppUtil) │ └── zip_builder.py # NinjaRMM ZIP assembly ├── templates/ │ └── printer_install.ps1.j2 # Jinja2 PS script template ├── db/ │ ├── database.py # SQLite connection, migrations │ └── models.py # Printer, Client, Driver models ├── storage/ │ └── driver_store.py # Read/write to volume-mounted driver path ├── config.py # Env-var driven configuration ├── main.py # App entrypoint, mounts routes ├── Dockerfile └── docker-compose.yml ``` ### Structure Rationale - **api/:** Thin handlers only — validation and delegation to services. No business logic here. - **services/:** All domain logic lives here, testable without HTTP context. - **generators/:** Each output format is isolated. Adding a new format (SCCM, PDQ) means adding one file here. - **templates/:** PS script is a template file, not a string in code. Easier to edit, diff, and review. - **db/:** All persistence in one place. SQLite means no daemon dependency. - **storage/:** Abstracts the filesystem from the rest of the app; makes future swap to S3 straightforward. ## Architectural Patterns ### Pattern 1: Request-Generate-Stream (synchronous generation) **What:** The browser POSTs printer config → server assembles files → server streams the archive back as a file download, all in one HTTP call. **When to use:** Package generation completes in under ~10 seconds. For this domain (small driver bundles, scripted packaging), synchronous is correct. **Trade-offs:** Simple — no job queue, no polling, no WebSocket. The downside is the HTTP connection stays open during generation; acceptable for internal tooling on a LAN. **Example flow:** ``` POST /api/packages/generate body: { printerId, format: "intunewin" | "ninja" } → Package Builder assembles temp workspace → PS Template Engine renders install.ps1 → IntuneWin Builder or ZIP Builder runs → Response: file download (Content-Disposition: attachment) → Temp workspace deleted after send ``` ### Pattern 2: Content-Addressed Driver Storage **What:** When a driver archive is uploaded, compute a SHA256 of the content and store it under that hash as the filename. Record the hash + original name in SQLite. Multiple printers referencing the same driver point to one file. **When to use:** Always — avoids duplicate large files on the volume and makes driver references stable across renames. **Trade-offs:** Slight upload cost for hashing. Cleanup requires reference counting (a row delete check before GC). ### Pattern 3: Template-Driven Script Generation **What:** PowerShell install script is a Jinja2 (Python) or Handlebars/nunjucks (Node) template. Printer parameters are injected as variables. The template handles the elevation logic, port setup, and driver install commands in one canonical place. **When to use:** Always — never build the PS script by string concatenation in code. The template is testable, auditable, and editable without touching Python/Node. **Trade-offs:** Adds a template engine dependency (Jinja2 is stdlib-adjacent in Python; nunjucks is tiny in Node). **Template sketch:** ```powershell # printer_install.ps1.j2 $PrinterName = "{{ printer.name }}" $PrinterIP = "{{ printer.ip }}" $DriverName = "{{ printer.driver_name }}" $PortName = "IP_{{ printer.ip }}" # Elevation guard if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator)) { Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs exit } # ... driver install, port add, printer add ... ``` ### Pattern 4: Subprocess Isolation for IntuneWinAppUtil **What:** IntuneWinAppUtil.exe is a Windows binary. In a Linux container it must run under Wine, or the container must be Windows-based. Isolate this call behind an interface so the builder can be swapped (e.g., native reimplementation, different tool) without touching the orchestrator. **When to use:** Whenever calling an external binary for a critical generation step. **Trade-offs:** Wine on Linux adds image size (~500 MB). A Windows-based container image avoids Wine but is heavier by default. A pure Python reimplementation of the .intunewin format (outer ZIP + encrypted inner ZIP + detection.xml) is feasible and removes the tool dependency entirely — recommended if the format can be locked down to what Intune actually requires. ## Data Flow ### Printer Package Generation Flow ``` [Technician fills form] ↓ POST /api/packages/generate { printerId, format } ↓ API Layer validates request, loads printer config from SQLite ↓ Package Builder Service ├── Fetches driver files from Driver Store (volume path) ├── Renders install.ps1 via PS Template Engine (printer params injected) ├── Copies icon file (if Intune format) └── Calls appropriate generator: ├── [Intune] IntuneWin Builder │ └── subprocess: IntuneWinAppUtil.exe -c -s install.ps1 -o │ OR: native Python .intunewin assembler └── [Ninja] ZIP Builder └── zipfile: { install.ps1, drivers/, readme.txt } ↓ Generated file written to temp workspace ↓ HTTP response streams file to browser (Content-Disposition: attachment) ↓ Temp workspace cleaned up (finally block / background task) ``` ### Driver Upload Flow ``` [Technician uploads ZIP/INF] ↓ POST /api/drivers (multipart/form-data) ↓ Driver Service ├── Streams upload to temp file ├── Computes SHA256 ├── Checks SQLite: already exists? → return existing record ├── Moves file to drivers volume at hash-named path └── Inserts driver record (id, hash, original_name, size, uploaded_at) ↓ Response: driver record (id, name) for use in printer config ``` ### Printer Config Flow ``` [Technician submits printer form] ↓ POST /api/printers body: { name, ip, clientId, driverId, color, duplex, paperSize, tray, iconId } ↓ Printer Service validates params, resolves driverId FK ↓ SQLite INSERT → printer record with all settings ↓ Response: printer record ``` ## Build Order (Component Dependencies) Build these layers in order — each depends on the previous being stable: 1. **Data layer** — SQLite schema, models, connection management. Everything else reads/writes here. 2. **Storage layer** — Driver Store abstraction over the volume filesystem. 3. **Services (Printer + Driver)** — CRUD operations, driver upload with deduplication. 4. **PS Template Engine** — Standalone, can be developed and unit-tested in isolation. 5. **ZIP Builder (NinjaRMM)** — Depends on Driver Store + PS Template Engine. Simple to implement, good validation target. 6. **IntuneWin Builder** — Depends on PS Template Engine + icon handling. Most complex generator; validate ZIP Builder first. 7. **API Layer** — Thin wrappers once services are solid. 8. **UI** — Depends on all API routes being defined. ## Anti-Patterns ### Anti-Pattern 1: Generating Scripts via String Concatenation **What people do:** Build the PowerShell script by concatenating strings in Python/Node code. **Why it's wrong:** Injection risk (printer names with quotes, special chars), hard to read, impossible to review, breaks easily. **Do this instead:** Use a template file (Jinja2 / nunjucks). Escape all user-provided values explicitly in the template context. Test the rendered output for known edge cases. ### Anti-Pattern 2: Storing Driver Blobs in SQLite **What people do:** Base64-encode driver ZIPs and store them in a BLOB column. **Why it's wrong:** SQLite performs poorly with large BLOBs; the database file balloons; backups become unwieldy; streaming is impossible. **Do this instead:** Store files on the filesystem (Docker volume). Store only the path/hash reference in SQLite. ### Anti-Pattern 3: Blocking the Server During Package Generation **What people do:** Call IntuneWinAppUtil.exe synchronously in the request handler with no timeout. **Why it's wrong:** If generation hangs (tool crash, permission issue), the HTTP worker is permanently blocked. On a single-worker server this freezes the entire app. **Do this instead:** Run the subprocess with a timeout (e.g., 60s). Use a thread pool or async subprocess so other requests can proceed. Return a 500 with a clear error if timeout is exceeded. ### Anti-Pattern 4: Leaving Temp Workspaces on Disk **What people do:** Write package files to a temp directory and never clean up. **Why it's wrong:** Disk fills up. Sensitive driver files and generated scripts accumulate indefinitely. **Do this instead:** Use a try/finally pattern: generate in a uniquely-named temp dir, stream the response, then unconditionally delete the temp dir. For async flows, attach cleanup to the download-complete event. ### Anti-Pattern 5: Hardcoding the IntuneWinAppUtil Path **What people do:** Hardcode `/app/tools/IntuneWinAppUtil.exe` in the generator code. **Why it's wrong:** Breaks if the tool is moved, the container is Windows vs Linux with Wine, or the tool is replaced. **Do this instead:** Configure via an environment variable (`INTUNEWIN_TOOL_PATH`). The builder service reads from config. Switching to a native Python reimplementation means changing only config, not code. ## Integration Points ### External Tooling | Tool | Integration Pattern | Notes | |------|--------------------|-------| | IntuneWinAppUtil.exe | subprocess call with timeout | Must be bundled in container image. On Linux, requires Windows container base image OR Wine. Wine image ~500 MB extra. Consider native reimplementation instead. | | PowerShell (target endpoints) | Template output only — no runtime PS in container | Container never executes PS; it only generates .ps1 files. | ### Internal Boundaries | Boundary | Communication | Notes | |----------|--------------|-------| | API Layer ↔ Services | Direct function call (same process) | No IPC needed; single container, single process. | | Services ↔ DB | SQLite via ORM or raw queries | Use WAL mode for concurrent reads during long-running generates. | | Services ↔ Driver Store | Filesystem reads via Driver Store abstraction | Abstraction allows future swap to S3 without service changes. | | Package Builder ↔ Generators | Direct function call, returns file path | Each generator writes output to a caller-provided temp directory. | | API Layer ↔ Temp Files | Generator returns path → API streams file → cleanup | Use streaming response to avoid loading entire archive into memory. | ## Scaling Considerations This is an internal MSP tool. Scaling expectations are low; design for correctness and maintainability, not throughput. | Scale | Architecture Adjustments | |-------|--------------------------| | 1-5 concurrent technicians | Default single-worker setup is fine. Use thread pool for subprocess calls. SQLite WAL mode handles concurrent reads. | | 10+ concurrent technicians | Add Gunicorn/Uvicorn worker count (still single container). Background job queue (in-process, e.g. Python's concurrent.futures) if generation is slow. | | Multi-tenant or SaaS | Out of scope. Would require auth, per-tenant isolation, external storage — full redesign. | ### Scaling Priorities 1. **First bottleneck:** IntuneWinAppUtil.exe process duration (CPU/disk). Mitigation: timeout + thread pool. 2. **Second bottleneck:** Disk I/O on the Docker volume for large driver packages. Mitigation: content-addressing avoids redundant copies. ## Sources - [Microsoft: .intunewin format internals — How to decode Intune Win32 App Packages](https://msendpointmgr.com/2019/01/18/how-to-decode-intune-win32-app-packages/) - [Microsoft Learn: Prepare a Win32 App for Intune (.intunewin packaging)](https://learn.microsoft.com/en-us/intune/intune-service/apps/apps-win32-prepare) - [Microsoft Win32 Content Prep Tool (IntuneWinAppUtil) — GitHub](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool) - [FastAPI deployment in Docker containers](https://fastapi.tiangolo.com/deployment/docker/) - [FastAPI file upload handling](https://betterstack.com/community/guides/scaling-python/uploading-files-using-fastapi/) - [Web-Queue-Worker Architecture Style — Azure Architecture Center](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/web-queue-worker) - [PowerShell as Win32 installer in Intune (2026)](https://headsinthecloud.blog/2026/02/24/from-packaging-to-logic-powershell-as-the-new-win32-installer-in-intune/) - [NinjaOne Remote Script Deployment](https://www.ninjaone.com/remote-script-deployment/) --- *Architecture research for: ImpTune — single-container printer deployment package generator* *Researched: 2026-04-10*