# 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 <tmpdir> -s install.ps1 -o <out>
         │         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*
