One-time commit of AI-generated planning documents: - prd.md — validated PRD, 44 FRs across 8 domains - prd-validation-report.md — validation results (4/5 holistic quality) - architecture.md — complete architecture document (all 8 steps) - epics.md — epics skeleton with extracted requirements inventory _bmad-output/ remains gitignored; these are committed explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
28 KiB
stepsCompleted, lastStep, status, completedAt, inputDocuments, workflowType, project_name, user_name, date
| stepsCompleted | lastStep | status | completedAt | inputDocuments | workflowType | project_name | user_name | date | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
8 | complete | 2026-04-27 |
|
architecture | pictureFrame | Matt.edholm | 2026-04-27 |
Architecture Decision Document
This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together.
Project Context Analysis
Requirements Overview
Functional Requirements: 44 FRs across 8 domains — User & Account Management, Device Management, Image Library, Image Approval & Sharing, Device Provisioning, Image Rotation & Cycle Engine, Display & Status, Admin & Moderation.
Non-Functional Requirements:
- Image pre-rendering completes within 10 seconds of upload/approval trigger (async job target)
- Device image pull endpoint ≤10s on home broadband
- Web pages load ≤3s on standard broadband
- Image rotation ±5 minutes of configured interval
- HTTPS all device-server communication
- MAC-authenticated device endpoint
- Single-use authorization links with configurable TTL
- Scheduled cleanup without manual intervention
- Zero blank screens; last image persists through outages
Scale & Complexity:
- Primary domain: Full-stack web app with IoT device client
- Complexity level: Medium
- Deployment target: Single VPS, solo developer, personal-scale traffic
- Estimated architectural components: Web app (Symfony), Async image processing worker, Device API endpoint, Email/token service, Scheduler/cron runner, ESP32 firmware (separate codebase)
Technical Constraints & Dependencies
- Stack: Symfony (PHP), PostgreSQL, Nginx-FPM, DDEV local dev — mirrors aqua-iq
- APP_BASE_URL is a firmware build constant — domain must be established before firmware development
- API contract is stable by design; breaking changes require physical reflash of all deployed devices
- No OTA firmware updates in V1
- Infrastructure patterns from ~/src/aqua-iq (DDEV, server, SSH)
Cross-Cutting Concerns Identified
- Async image processing pipeline: Upload/approval dispatches a Symfony Messenger message → worker processes (resize, 4bpp palette quantization) → sets image rendering_status to
ready. Images only enter device pull pool whenready. Doctrine transport (consistent with aqua-iq). - Image rendering status: Each image asset tracks
pending → processing → ready | failedper device model/orientation. Rotation engine and device pull endpoint filter exclusively onready. - Device identity: MAC address as device identifier throughout — provisioning, image pull auth, ownership transfer.
- Authorization tokens: Single-use links with TTL for email approve/decline and hard-delete confirmation.
- Soft-delete with retention rules: Images retained until last approval removed; scheduled cleanup handles hard-delete.
- Rotation engine: Per-device schedule tracking, uniqueness window enforcement, next-image selection from
readypool only.
Starter Template Evaluation
Primary Technology Domain
Symfony full-stack web application with IoT device client. Stack pre-determined by user.
Selected Starter: Symfony 8.0 --webapp
Initialization Command:
symfony new pictureframe --webapp
Architectural Decisions Provided by Starter:
Language & Runtime: PHP 8.4+, Symfony 8.0
Templating: Twig with Stimulus and UX Turbo (Hotwire) — lightweight interactivity without a JS build step
ORM & Database: Doctrine ORM, PostgreSQL 16 (DDEV local), migrations via doctrine/migrations
Security: Symfony Security component — firewalls, voters, role hierarchy
Email: Symfony Mailer — handles sharing flows and hard-delete confirmations
Build Tooling: AssetMapper (no Webpack/Node build step required)
Testing: PHPUnit via symfony/test-pack
Code Organization: Standard Symfony structure — src/Entity, src/Controller, src/Repository, src/Service, templates/
Additional packages required:
symfony/messenger+ Doctrine transport — async image processing workersymfony/scheduler— rotation engine and scheduled cleanup- Image processing library (TBD step 4: GD vs Imagick)
Local Dev: DDEV configured to mirror aqua-iq (PHP 8.4, Nginx-FPM, PostgreSQL 16)
Note: Project initialization using this command is the first implementation story.
Core Architectural Decisions
Decision Priority Analysis
Critical Decisions (Block Implementation):
- APP_BASE_URL established:
https://pictureframe.edholm.me— bake into firmware as build constant - Image rendering status lifecycle required before rotation engine can select images
- Imagick required on VPS before image processing worker can run
Important Decisions (Shape Architecture):
- Local filesystem image storage (not object storage)
- Symfony Messenger async processing with
readystatus gate - Token entity pattern for all single-use authorization links
Deferred Decisions (Post-MVP):
- Caching layer (Redis) — add if query performance becomes an issue
- Additional display models beyond Waveshare 7.3" 800×480
Data Architecture
Image Storage: Local filesystem on VPS. Pre-rendered assets stored at storage/images/{image_id}/{device_model}_{orientation}.bin. Originals stored separately at storage/images/{image_id}/original.{ext}. Paths stored in DB are relative to STORAGE_PATH env var. No object storage — personal scale doesn't warrant the complexity.
Image Processing Library: Imagick (PHP extension). Chosen for Floyd-Steinberg dithering quality on the 6-color e-ink palette. GD's palette quantization produces inferior results for this use case. Imagick must be installed on VPS and in DDEV container.
Image Rendering Status: Each pre-rendered asset (per device model + orientation) tracks status independently: pending → processing → ready | failed. Rotation engine and device pull endpoint filter exclusively on ready. Stored as an enum column on a RenderedAsset entity.
Caching: None in V1. PostgreSQL at personal scale is sufficient. Add Redis later if needed.
Migrations: Doctrine Migrations (doctrine/migrations). Standard Symfony approach.
Authentication & Security
Web Authentication: Symfony form login — email + password, remember_me cookie. Standard Symfony Security firewall.
Roles: Two roles only — ROLE_USER and ROLE_SUPER_ADMIN. Configured in security.yaml with role hierarchy. Super admin is a single designated account.
Authorization Token Pattern: A Token entity — UUID primary key, type enum (share_approve, share_decline, hard_delete_confirm), expires_at datetime, used_at nullable datetime. Single-use enforced: token is invalid if used_at IS NOT NULL or expires_at < NOW(). TTL is configurable per token type.
Device MAC Authentication: Symfony controller guard on the image pull endpoint. Device sends MAC address via URL segment; server validates it exists in the Device entity and is linked to an active account before serving the binary asset.
HTTPS: Enforced at Nginx level on VPS. DDEV provides HTTPS locally.
API & Communication Patterns
Device API: Single endpoint — GET /api/device/{mac}/image — returns raw binary (application/octet-stream). This is the only machine-to-machine API surface. No versioning scheme beyond URL stability guarantee (no breaking changes in V1).
Web Controllers: Standard Symfony controllers returning Twig responses. No JSON API for the web application.
Email: Symfony Mailer. Transactional emails: image share notification (with approve link), hard-delete confirmation. Authorization links embedded as tokenized URLs pointing to Symfony routes.
Async Messaging: Symfony Messenger with Doctrine transport. Messages dispatched on image upload and image approval. Worker consumes queue and processes images via Imagick. Sets RenderedAsset status to ready on completion, failed on exception.
Scheduled Tasks: Symfony Scheduler. Two recurring tasks:
- Rotation engine: per-device, fires on each device's configured interval to advance the current image pointer
- Cleanup job: periodic hard-delete of soft-deleted images with no remaining approvals
Frontend Architecture
Templating: Twig. Identical pattern to aqua-iq.
Interactivity: Stimulus controllers + Turbo Drive (Hotwire). No SPA, no build step required.
Forms: Symfony Form component.
Assets: AssetMapper (no Webpack/Node).
Infrastructure & Deployment
Domain: pictureframe.edholm.me — APP_BASE_URL and API base URL. Must be configured on VPS before firmware build constants are set.
VPS: Single server. Nginx reverse proxy → PHP-FPM → Symfony. PostgreSQL on same host. Mirrors aqua-iq production setup.
Local Dev: DDEV — PHP 8.4, Nginx-FPM, PostgreSQL 16. Config mirrors aqua-iq .ddev/config.yaml.
Git Hosting: git.edholm.me (self-hosted Gitea/Forgejo). Credentials from existing projects.
CI/CD: Gitea Actions (if enabled on git.edholm.me) or SSH-based deploy script. Mirror aqua-iq deployment pattern.
SSH/Server Access: Documented in aqua-iq project. Same VPS, same access pattern.
Decision Impact Analysis
Implementation Sequence:
- DDEV setup + Symfony scaffold (
symfony new pictureframe --webapp) - Add Messenger, Scheduler, Imagick
- Domain + Nginx config on VPS
- Core entities (User, Device, Image, RenderedAsset, Token)
- Image processing worker
- Device pull endpoint
- Web application features (library, approval, sharing, admin)
- Firmware (after domain + API contract confirmed)
Cross-Component Dependencies:
- Firmware cannot be finalized until
pictureframe.edholm.meis live and API endpoint format is confirmed - Device pull endpoint depends on
RenderedAsset.status = ready - Rotation engine depends on uniqueness tracking and
readyasset pool - Email sharing depends on Token entity and Mailer configuration
Implementation Patterns & Consistency Rules
Entity & Enum Patterns
PHP Backed Enums: Use PHP 8.1 backed enums for all finite value sets.
enum RenderStatus: string {
case Pending = 'pending';
case Processing = 'processing';
case Ready = 'ready';
case Failed = 'failed';
}
enum TokenType: string {
case ShareApprove = 'share_approve';
case ShareDecline = 'share_decline';
case HardDeleteConfirm = 'hard_delete_confirm';
}
enum Orientation: string {
case Landscape = 'landscape';
case Portrait = 'portrait';
}
Doctrine maps backed enums to VARCHAR columns via built-in PHP 8.1 enum type support. No string constants elsewhere.
Repository Naming Convention
Soft-Delete Awareness: All repository methods that exclude soft-deleted records use the findActive* prefix.
findActiveByDevice(Device $device)— images wheredeleted_at IS NULLfindActiveReadyByDevice(Device $device)— images withdeleted_at IS NULLANDstatus = RenderStatus::ReadyfindBy*— raw Doctrine finders, may include soft-deleted records
This makes soft-delete awareness visible at the call site rather than hidden in query logic.
Image Storage Pattern
Storage Location: storage/images/{image_id}/{device_model}_{orientation}.bin for pre-rendered assets. Originals at storage/images/{image_id}/original.{ext}. Never var/images/.
Database References: Store paths relative to STORAGE_PATH root, not absolute paths. The storage root can move without a data migration.
Environment Variable: STORAGE_PATH — configured per environment. In DDEV, mapped to a local volume. On VPS, absolute path under the project root.
Async Image Processing Pattern
Exclusive Dispatch Points: ProcessImageMessage is dispatched in exactly two places:
ImageService::upload()— after persisting a new uploaded imageImageService::approve()— after a share-approval token is consumed
No other code dispatches ProcessImageMessage.
Messenger Transport Configuration:
# config/packages/messenger.yaml
framework:
messenger:
transports:
image_processing:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
queue_name: image_processing
retry_strategy:
max_retries: 1
delay: 1000
multiplier: 2
max_retries: 1 — a single retry on transient failures. Failed messages set RenderedAsset.status to RenderStatus::Failed so the error surfaces in the admin UI.
Device Pull Endpoint — Critical Response Codes
CRITICAL RULE:
GET /api/device/{mac}/imagemust return:
204 No Content— MAC is valid, device exists, but noreadyimage is currently available404 Not Found— MAC is not registered in the system200 OK— returns raw binary (application/octet-stream)Never return 404 when the device is known but has no ready image. The ESP32 firmware treats 204 as "wait and retry" and 404 as "this device is not configured." Confusing them causes the device to enter a permanent error state.
Testing Strategy
Unit Tests (tests/Unit/): Pure logic, no database or I/O. Covers: enum behavior, token TTL calculation, image processing pipeline steps, rotation selection algorithm.
Integration Tests (tests/Integration/): Hit the real test database via Doctrine, using Symfony's KernelTestCase. Covers: repository findActive* methods, Messenger handler with real entity persistence, token single-use enforcement.
Functional Tests (tests/Functional/): Full HTTP stack via WebTestCase. Covers: device pull endpoint (204/404/200 response codes), form submissions, auth flows, email dispatch.
No database mocks. Integration tests use a real test database — mirrors the pattern from aqua-iq.
Enforcement Guidelines
Mandatory rules for all code in this project:
findActive*prefix on all repository methods filtering out soft-deleted recordsImageService::upload()andImageService::approve()are the only dispatch points forProcessImageMessage204not404when device is known but has no ready imagestorage/images/for all file storage — nevervar/images/- Relative paths in the database — never absolute filesystem paths
max_retries: 1on theimage_processingMessenger transport- PHP backed enums for
RenderStatus,TokenType,Orientation— no raw string constants - Rotation engine and device pull endpoint filter exclusively on
RenderStatus::Ready
Project Structure & Boundaries
Requirements to Structure Mapping
User & Account Management → src/Controller/SecurityController.php, src/Entity/User.php, src/Repository/UserRepository.php, templates/security/
Device Management → src/Controller/Device/, src/Entity/Device.php, src/Service/DeviceService.php, src/Repository/DeviceRepository.php, templates/device/
Image Library → src/Controller/Image/, src/Entity/Image.php, src/Entity/RenderedAsset.php, src/Service/ImageService.php, src/Service/ImageProcessingService.php, templates/image/
Image Approval & Sharing → src/Controller/Token/, src/Entity/Token.php, src/Service/TokenService.php, templates/token/
Device Provisioning → src/Controller/Device/DeviceProvisionController.php, templates/device/provision.html.twig
Image Rotation & Cycle Engine → src/Schedule/RotationSchedule.php, src/Service/RotationService.php
Display & Status (device pull endpoint) → src/Controller/Api/DeviceImageController.php
Admin & Moderation → src/Controller/Admin/, templates/admin/
Cross-Cutting: Async Processing → src/Message/ProcessImageMessage.php, src/MessageHandler/ProcessImageMessageHandler.php, config/packages/messenger.yaml
Cross-Cutting: Scheduled Tasks → src/Schedule/RotationSchedule.php, src/Schedule/ImageCleanupSchedule.php, config/packages/scheduler.yaml
Cross-Cutting: Enums → src/Enum/RenderStatus.php, src/Enum/TokenType.php, src/Enum/Orientation.php
Complete Project Directory Structure
pictureframe/
├── .ddev/
│ ├── config.yaml ← PHP 8.4, nginx-fpm, pgsql 16 — mirror aqua-iq
│ └── docker-compose.imagick.yaml ← adds Imagick to web container
├── .gitea/
│ └── workflows/
│ └── ci.yml ← Gitea Actions: lint + test on push
├── assets/
│ ├── app.js ← AssetMapper entry, imports Stimulus + Turbo
│ ├── controllers/
│ │ ├── image_upload_controller.js
│ │ └── device_status_controller.js
│ └── styles/
│ └── app.css
├── bin/
│ └── console
├── config/
│ ├── packages/
│ │ ├── doctrine.yaml
│ │ ├── doctrine_migrations.yaml
│ │ ├── mailer.yaml
│ │ ├── messenger.yaml ← image_processing transport, max_retries: 1
│ │ ├── scheduler.yaml
│ │ ├── security.yaml ← form_login, remember_me, ROLE_SUPER_ADMIN hierarchy
│ │ └── twig.yaml
│ ├── routes.yaml
│ ├── routes/
│ │ └── api.yaml ← /api/device/{mac}/image route
│ └── services.yaml
├── migrations/
├── public/
│ └── index.php
├── src/
│ ├── Controller/
│ │ ├── Api/
│ │ │ └── DeviceImageController.php ← GET /api/device/{mac}/image → 200/204/404
│ │ ├── Admin/
│ │ │ ├── AdminDashboardController.php
│ │ │ └── AdminModerationController.php
│ │ ├── Device/
│ │ │ ├── DeviceController.php
│ │ │ └── DeviceProvisionController.php
│ │ ├── Image/
│ │ │ ├── ImageLibraryController.php
│ │ │ ├── ImageUploadController.php
│ │ │ └── ImageShareController.php
│ │ ├── SecurityController.php
│ │ └── Token/
│ │ └── TokenActionController.php ← consume approve/decline/hard-delete tokens
│ ├── Entity/
│ │ ├── Device.php
│ │ ├── Image.php
│ │ ├── RenderedAsset.php ← per device-model+orientation, RenderStatus
│ │ ├── Token.php ← UUID PK, TokenType, expires_at, used_at
│ │ └── User.php
│ ├── Enum/
│ │ ├── Orientation.php
│ │ ├── RenderStatus.php
│ │ └── TokenType.php
│ ├── Form/
│ │ ├── DeviceType.php
│ │ ├── ImageUploadType.php
│ │ └── RegistrationType.php
│ ├── Message/
│ │ └── ProcessImageMessage.php ← DTO: imageId, deviceModel, orientation
│ ├── MessageHandler/
│ │ └── ProcessImageMessageHandler.php ← Imagick → .bin → sets RenderStatus::Ready
│ ├── Repository/
│ │ ├── DeviceRepository.php
│ │ ├── ImageRepository.php ← findActiveByDevice(), findActiveReadyByDevice()
│ │ ├── RenderedAssetRepository.php ← findReadyForDevice()
│ │ ├── TokenRepository.php ← findValidToken() (unused + unexpired)
│ │ └── UserRepository.php
│ ├── Schedule/
│ │ ├── ImageCleanupSchedule.php ← hard-delete orphaned soft-deleted images
│ │ └── RotationSchedule.php ← advance current_image pointer per device
│ ├── Service/
│ │ ├── DeviceService.php
│ │ ├── ImageProcessingService.php ← Imagick: resize → 4bpp dither → .bin
│ │ ├── ImageService.php ← upload() and approve() — exclusive dispatch points
│ │ ├── RotationService.php ← next-image selection, uniqueness window
│ │ └── TokenService.php ← issue and consume single-use tokens
│ └── Kernel.php
├── storage/
│ └── images/ ← gitignored; STORAGE_PATH points here
├── templates/
│ ├── admin/
│ │ ├── dashboard.html.twig
│ │ └── moderation.html.twig
│ ├── device/
│ │ ├── index.html.twig
│ │ ├── provision.html.twig
│ │ └── show.html.twig
│ ├── image/
│ │ ├── library.html.twig
│ │ ├── share.html.twig
│ │ └── upload.html.twig
│ ├── security/
│ │ └── login.html.twig
│ ├── token/
│ │ ├── approve.html.twig
│ │ └── decline.html.twig
│ └── base.html.twig
├── tests/
│ ├── Functional/
│ │ ├── Api/
│ │ │ └── DeviceImageControllerTest.php
│ │ ├── Device/
│ │ │ └── DeviceControllerTest.php
│ │ └── Image/
│ │ └── ImageLibraryControllerTest.php
│ ├── Integration/
│ │ ├── MessageHandler/
│ │ │ └── ProcessImageMessageHandlerTest.php
│ │ ├── Repository/
│ │ │ ├── ImageRepositoryTest.php
│ │ │ └── RenderedAssetRepositoryTest.php
│ │ └── Service/
│ │ └── TokenServiceTest.php
│ └── Unit/
│ ├── Enum/
│ │ └── RenderStatusTest.php
│ └── Service/
│ ├── ImageProcessingServiceTest.php
│ └── RotationServiceTest.php
├── var/
│ ├── cache/
│ └── log/
├── .env
├── .env.local ← gitignored; STORAGE_PATH, DATABASE_URL, MAILER_DSN
├── .env.test
├── .gitignore
├── composer.json
├── composer.lock
├── importmap.php
├── phpunit.xml.dist
└── symfony.lock
Architectural Boundaries
API Boundary — Device Pull Endpoint
GET /api/device/{mac}/image is the only machine-to-machine surface. MAC address validated against Device entity before serving. Returns application/octet-stream, 204, or 404. Isolated in config/routes/api.yaml.
Web Application Boundary
All other routes behind Symfony form-login firewall. Twig responses only. ROLE_SUPER_ADMIN gates admin controllers.
Async Processing Boundary
ImageService → Messenger bus → ProcessImageMessageHandler. Handler is the only component writing to storage/images/. RenderedAsset.status is the only signal crossing this boundary.
Scheduled Task Boundary
RotationSchedule and ImageCleanupSchedule run in the worker process — no HTTP context, no session access. Database reads and writes only.
Storage Boundary
storage/images/ written by ProcessImageMessageHandler, read by DeviceImageController. All other code uses STORAGE_PATH env var. Paths in DB are always relative.
Integration Points & Data Flow
Image Upload → Display
User uploads → ImageService::upload() persists Image → dispatches ProcessImageMessage
→ Worker: Imagick resize + dither → storage/images/{id}/waveshare73_landscape.bin
→ RenderedAsset.status = Ready
→ RotationSchedule advances device pointer
→ ESP32: GET /api/device/{mac}/image → 200 + binary stream
Image Sharing Flow
User shares image with owner → ImageService creates Token (ShareApprove/ShareDecline)
→ Mailer sends tokenized URL
→ Owner clicks link → TokenActionController::approve()
→ TokenService consumes token → ImageService::approve() dispatches ProcessImageMessage
→ Image enters ready pool for recipient device
Soft-Delete & Cleanup Flow
User soft-deletes image → Image.deleted_at set (excluded by findActive*)
→ If last approval removed → ImageCleanupSchedule hard-deletes image + storage files
Architecture Validation Results
Coherence Validation ✅
Decision Compatibility: PHP 8.4 + Symfony 8.0 + Doctrine ORM + Messenger + Scheduler + PostgreSQL 16 is a fully supported combination. Imagick is a standalone PHP extension with no framework conflicts. AssetMapper + Stimulus + Turbo has first-party Symfony UX support and requires no build tooling.
Pattern Consistency: findActive* naming, storage/images/ path convention, exclusive Messenger dispatch points, and backed enum usage are consistently applied. One contradiction (var/images/ in Data Architecture section) found during validation and corrected — all references now point to storage/images/.
Structure Alignment: Every controller, service, entity, and test file has a defined home. All five boundaries (API, web, async, storage, scheduled) are respected in the project structure.
Requirements Coverage Validation ✅
All 8 FR domains covered:
| Domain | Architectural Support |
|---|---|
| User & Account Management | User entity, form login, SecurityController |
| Device Management | Device entity, DeviceController, DeviceService |
| Image Library | Image entity, ImageLibraryController, ImageService |
| Image Approval & Sharing | Token entity, TokenService, TokenActionController |
| Device Provisioning | DeviceProvisionController |
| Image Rotation & Cycle Engine | RotationSchedule, RotationService |
| Display & Status | DeviceImageController, RenderedAsset |
| Admin & Moderation | AdminDashboardController, AdminModerationController |
All 9 NFRs covered: pre-rendering ≤10s (async Messenger), device pull ≤10s (direct binary stream), web pages ≤3s (standard Symfony), rotation ±5min (Scheduler), HTTPS (Nginx), MAC auth (controller guard), single-use links (Token entity), scheduled cleanup (ImageCleanupSchedule), zero blank screens (204 not 404 + last image persistence).
Implementation Readiness Validation ✅
All critical decisions documented. Enforcement guidelines provide 8 explicit, checkable rules. Project tree is specific with no generic placeholders. The 204/404 distinction is called out as a critical rule with the reason documented.
Gap Analysis
Critical Gaps: None.
Minor (non-blocking):
.env.examplenot in project tree — add alongside.envon scaffolddevice_modelfield type onRenderedAssetnot fully specified — for V1 with a single display (Waveshare 7.3" 800×480), a string constant or single-value enum is sufficient
Architecture Completeness Checklist
✅ Requirements Analysis — context, scale, constraints, cross-cutting concerns
✅ Architectural Decisions — stack, data architecture, auth, API patterns, infrastructure
✅ Implementation Patterns — enums, findActive*, storage, Messenger, 204/404, testing, enforcement rules
✅ Project Structure — complete directory tree, boundaries, FR mapping, data flow diagrams
✅ Validation — coherence, coverage, readiness, gaps
Architecture Readiness Assessment
Overall Status: READY FOR IMPLEMENTATION
Confidence: High. Coherent, fully covers the PRD, no critical gaps, sufficient specificity for consistent implementation.
First Implementation Step:
symfony new pictureframe --webapp
Implementation Handoff
AI Agent Guidelines:
- Follow all architectural decisions exactly as documented
- Apply the 8 enforcement rules in every code generation task
- Respect the
storage/images/boundary — never write image assets tovar/ - Confirm
204 vs 404on every implementation of the device pull endpoint - Reference this document for all architectural questions; do not invent decisions not documented here