# Stack Research

**Domain:** Self-hosted single-container internal tool — Windows printer deployment package generator
**Researched:** 2026-04-10
**Confidence:** HIGH (core stack), MEDIUM (.intunewin reimplementation specifics)

---

## Recommended Stack

### Core Technologies

| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| Python | 3.12 | Runtime | LTS-stable, broad library support, zipfile/cryptography stdlib covers .intunewin needs without C extensions. 3.13 is fine too; avoid 3.11 and below (FastAPI 0.130+ requires 3.10+ but 3.12 is the sweet spot for Docker image size vs feature parity). |
| FastAPI | 0.115.x (latest stable) | HTTP framework + API | De-facto standard for Python internal tools in 2025. Async-capable, Pydantic validation built-in, native file upload/download support, StreamingResponse for generated archives. Simpler than Django for this scope; more structured than Flask. |
| Uvicorn | 0.30.x | ASGI server | FastAPI's recommended server. Handles subprocess management internally — Gunicorn is not needed for a single-container internal tool with no concurrency requirements. |
| Jinja2 | 3.1.x | Server-side HTML templating | Ships with FastAPI's template support. No build step, no Node. HTMX+Jinja2 renders the full UI from the server. |
| HTMX | 2.0.x (CDN) | Dynamic UI without JavaScript SPA | Handles partial page updates (form submissions, driver list refresh, package generation progress) with zero build tooling. Ideal for CRUD-heavy internal tools. Single `<script>` tag. |
| Alpine.js | 3.x (CDN) | Client-side UI state | Complements HTMX: dropdowns, modals, toggle states, file-input previews. Replaces React/Vue for this scope. No npm, no bundler. |
| SQLite (stdlib) | 3.x (bundled) | Persistence: driver records, printer configs | No external database process. Python's built-in `sqlite3` module is sufficient for sync operations. Single-writer model is not a concern for a single-user internal tool. Mount via Docker named volume. |
| Tailwind CSS | 4.x (CDN Play) | Utility-first CSS | For an internal tool with no public users, the CDN Play script is acceptable. It avoids a Node.js build step inside the Docker image. If CSS size ever matters, switch to the standalone Tailwind CLI binary (no Node required). |

### Supporting Libraries

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `python-multipart` | 0.0.9 | Multipart file upload parsing | Required by FastAPI for `UploadFile`. Always install alongside FastAPI when handling file uploads. |
| `pycryptodome` | 3.20.x | AES-256-CBC + HMAC-SHA256 for .intunewin encryption | The .intunewin inner package is encrypted with AES-256-CBC; the HMAC-SHA256 digest is written to Detection.xml. Use pycryptodome (not the deprecated pycrypto). |
| `lxml` or `xml.etree.ElementTree` (stdlib) | stdlib | Generate Detection.xml metadata | Detection.xml is simple enough for stdlib ElementTree. Only reach for lxml if namespaces become complex. |
| `aiofiles` | 23.x | Async file I/O | Used with FastAPI's StreamingResponse when streaming generated ZIPs to the browser without loading the full archive into memory. |
| `python-dotenv` | 1.0.x | Environment-based configuration | Allows Docker-level config overrides (data directory, port, base URL) without rebuilding the image. |
| `peewee` | 3.17.x | ORM for SQLite | Lightweight sync ORM — perfectly matched to SQLite's single-writer model. Avoids the async overhead of SQLModel/aiosqlite, which provides no benefit with SQLite. Use only if raw `sqlite3` becomes unwieldy across multiple tables. |

### Development Tools

| Tool | Purpose | Notes |
|------|---------|-------|
| Docker (python:3.12-slim-bookworm) | Container base image | Slim variant keeps image under 200 MB. Bookworm (Debian 12) has modern glibc. Do NOT use Alpine Linux — pycryptodome and other C-extension wheels often lack musl-compatible builds, causing silent failures. |
| `uvicorn --reload` | Dev server with hot reload | Run locally during development. In Docker, use `CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]` without `--reload`. |
| Docker named volume | Driver + SQLite persistence | Mount `/data` as a named volume. Store driver ZIPs and the SQLite file there. Never store state in the container filesystem. |

---

## Installation

```bash
# Core runtime
pip install fastapi==0.115.* uvicorn[standard]==0.30.* jinja2==3.1.* python-multipart==0.0.9

# File generation and encryption
pip install pycryptodome==3.20.* aiofiles==23.*

# Configuration + optional ORM
pip install python-dotenv==1.0.* peewee==3.17.*

# Dev only
pip install httpx pytest pytest-asyncio
```

Tailwind CSS and HTMX are loaded from CDN in templates — no npm install.

---

## Alternatives Considered

| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| FastAPI + Jinja2 + HTMX | Django + django-htmx | If the project grows to need Django admin, multi-tenant auth, or complex ORM migrations. Overkill for this scope. |
| FastAPI + Jinja2 + HTMX | FastAPI + React/Vue SPA | If the UI needs real-time collaborative editing or complex client-side state (drag-and-drop ordering, offline mode). Not needed here. |
| Python (FastAPI) | Go (Gin/Chi) | If single binary with no runtime dependency is the top priority. Go produces a smaller Docker image but adds complexity for the .intunewin crypto/zip logic that Python stdlib already handles. |
| pycryptodome | cryptography (PyCA) | `cryptography` is fine too and arguably better maintained. Either works for AES-CBC. Prefer `cryptography` if you want FIPS-like assurance; pycryptodome is simpler to use for this exact pattern (CBC + HMAC). |
| SQLite (builtin / peewee) | PostgreSQL | If multi-user write concurrency is ever needed. Not the case here. |
| Tailwind CDN | Tailwind standalone CLI | Use the standalone CLI binary (no Node) if you want purged, production-sized CSS. Drop the `tailwindcss` binary into the Docker image at build time. Adds ~10 MB to the image but removes CDN dependency. |
| python:3.12-slim-bookworm | python:3.12-alpine | Alpine breaks C-extension wheels. Slim Debian is the correct base for any project using pycryptodome or similar. |

---

## What NOT to Use

| Avoid | Why | Use Instead |
|-------|-----|-------------|
| IntuneWinAppUtil.exe inside Docker | It is a Windows PE binary. It will not run in a Linux container. Even Wine would add hundreds of MB and introduce instability. | Reimplement the format in Python: `zipfile` (stdlib) for inner ZIP with `ZIP_STORED`, `pycryptodome` for AES-256-CBC encryption, `xml.etree.ElementTree` for Detection.xml. The format is fully documented by reverse engineering (svrooij.io). |
| Alpine Linux base image | musl libc breaks pre-built wheels for pycryptodome and other C-extension packages, causing pip to fall back to source builds or fail silently. | `python:3.12-slim-bookworm` — Debian-based, glibc, pre-built wheels always work. |
| Celery / Redis / task queue | Massively overengineered for a single-user internal tool. .intunewin generation takes < 5 seconds per package. | FastAPI `BackgroundTasks` is sufficient for post-response cleanup; synchronous generation on the request thread is fine. |
| SQLAlchemy async (aiosqlite) | Async gives zero throughput benefit with SQLite (single-writer lock). Adds complexity for no gain. | Synchronous `sqlite3` stdlib or `peewee`. Run database calls synchronously inside `asyncio.run_in_executor` if truly needed. |
| React / Vue / Next.js | Requires a Node.js build pipeline in the Docker image, a separate frontend build stage, and a JS bundle. Unnecessary complexity for an internal CRUD tool. | HTMX + Alpine.js served from Jinja2 templates. No build step. |
| PyCrypto (original) | Unmaintained since 2012. Known vulnerabilities. | `pycryptodome` — a maintained drop-in replacement. |

---

## Stack Patterns by Variant

**If .intunewin generation needs to stay on the request thread (simplest path):**
- Generate the .intunewin file synchronously, stream it with `FileResponse` or `StreamingResponse`, then delete the temp file with `BackgroundTasks`.
- No async file I/O needed for packages under ~500 MB.

**If driver ZIPs are very large (>500 MB) and upload times block Uvicorn:**
- Use `aiofiles` for async reads during upload processing.
- Uvicorn is single-threaded by default in development; add `--workers 2` in production Dockerfile if needed (though unlikely for an internal tool).

**If Tailwind CDN becomes a problem (slow intranet, offline use):**
- Download the Tailwind standalone CLI (`tailwindcss-linux-x64`) into the Docker image during build.
- Run `tailwindcss -i input.css -o static/output.css --minify` as a Dockerfile `RUN` step.
- Serve the generated CSS as a static file. Zero Node.js required.

---

## .intunewin Reimplementation (Critical Detail)

The official `IntuneWinAppUtil.exe` is Windows-only. The format is well-documented through reverse engineering:

1. **Inner ZIP** — compress source folder using `zipfile.ZipFile(..., compression=ZIP_DEFLATED)` (note: unlike the outer ZIP, the inner package uses DEFLATE not STORED)
2. **Encrypt** — AES-256-CBC with a random 32-byte key and 32-byte IV; prepend HMAC-SHA256 (32 bytes) then IV (32 bytes) to the ciphertext
3. **Detection.xml** — XML file with base64-encoded EncryptionKey, InitializationVector, Mac (HMAC), FileDigest (SHA256 of plaintext), FileDigestAlgorithm ("SHA256"), and ProfileIdentifier ("ProfileVersion1")
4. **Outer ZIP** — ZIP_STORED containing `IntuneWinPackage/Contents/IntunePackage.intunewin` (the encrypted blob) and `IntuneWinPackage/Metadata/Detection.xml`

Reference implementation: [svrooij.io — Creating IntuneWin files with C#](https://svrooij.io/2023/10/24/create-intunewin-file/) — the logic is language-agnostic and Python stdlib + pycryptodome covers all requirements. **Confidence: MEDIUM** (based on reverse-engineering documentation; validate against a real Intune upload during Phase 1).

---

## Version Compatibility

| Package | Compatible With | Notes |
|---------|-----------------|-------|
| fastapi 0.115.x | pydantic 2.x | FastAPI 0.115+ requires Pydantic v2. Do not mix with Pydantic v1. |
| pycryptodome 3.20.x | Python 3.12, 3.13 | Use `pycryptodome` not `pycryptodomex` (different package name, same code, different import path). Import as `from Crypto.Cipher import AES`. |
| uvicorn 0.30.x | fastapi 0.115.x | Compatible. Use `uvicorn[standard]` to include `uvloop` and `httptools` for better performance. |
| peewee 3.17.x | Python 3.12 | Sync only. Does not conflict with FastAPI's async model — call peewee from sync functions or `run_in_executor`. |
| Tailwind CDN v4 | Any browser | Play CDN is development-only per Tailwind docs. For production isolation, use standalone CLI instead. |

---

## Sources

- [FastAPI Deployment with Docker — Official Docs](https://fastapi.tiangolo.com/deployment/docker/) — Docker patterns, Uvicorn configuration
- [FastAPI Templates — Official Docs](https://fastapi.tiangolo.com/advanced/templates/) — Jinja2 integration
- [svrooij.io — Creating IntuneWin files with C#](https://svrooij.io/2023/10/24/create-intunewin-file/) — .intunewin format reference (MEDIUM confidence, reverse-engineered)
- [svrooij.io — Decrypting intunewin files](https://svrooij.io/2023/10/09/decrypting-intunewin-files/) — Encryption structure validation
- [SvRooij.ContentPrep on NuGet](https://www.nuget.org/packages/SvRooij.ContentPrep) — Cross-platform C# reference implementation, last updated 2025-10-03
- [PyCryptodome docs](https://pycryptodome.readthedocs.io/en/latest/src/examples.html) — AES-CBC usage
- [Python zipfile docs](https://docs.python.org/3/library/zipfile.html) — ZIP_STORED / ZIP_DEFLATED constants
- [HTMX + FastAPI patterns 2025](https://johal.in/htmx-fastapi-patterns-hypermedia-driven-single-page-applications-2025/) — HTMX suitability for internal tools
- [Python 2025 Web Stack — Medium](https://medium.com/@hadiyolworld007/pythons-2025-web-stack-fastapi-sqlmodel-and-htmx-changed-everything-58cc2da1cf14) — FastAPI + HTMX ecosystem confirmation
- [Tailwind Play CDN docs](https://tailwindcss.com/docs/installation/play-cdn) — CDN limitations for production
- [Docker Volumes for SQLite](https://dev.to/behainguyen/python-docker-volumes-where-is-my-sqlite-database-file-48fd) — Named volume pattern
- WebSearch (multiple queries, 2026-04-10) — ecosystem verification

---

*Stack research for: ImpTune — printer deployment package generator*
*Researched: 2026-04-10*
