# ARTHERIS EHDS: Phase 1 design decisions (shared hosting)

Pilot target: **shared hosting**. Each decision below closes one of the risks flagged in the blueprint review.

## 1. iOS offline limits (no Background Sync, storage eviction)
- Sync is **foreground-driven**: on app open, on tab becoming visible, on `online`, every 30s while visible, and a manual **Sync now** button.
- Chrome/Android additionally registers Background Sync (`sw.js`), but nothing depends on it.
- `navigator.storage.persist()` is requested on every start.
- On iOS the app shows a persistent prompt to **Add to Home Screen** (installed web apps are far less exposed to Safari's storage clean-up) and warns when work has been unsynced for more than 24h.
- The queue is idempotent and survives interrupted sessions. Nothing is ever deleted locally until the server confirms it.
- Field SOP (add to training): sync at least once per day with signal; do not clear browser data.

## 2. Local encryption
- Records and queued operations are AES-GCM sealed (`public/pwa/js/store.js`) with a **per-device, non-extractable** key held in IndexedDB; the auth token is sealed the same way.
- Honest scope: this protects copies of the browser profile/DB files. It does **not** protect an unlocked, logged-in phone. Policy requirements: device screen lock, auto sign-out on device loss (revoke the device token from the admin portal; tokens expire after 30 days).
- Upgrade path if required later: PIN-derived key (PBKDF2) wrapping the device key.

## 3. Shared hosting: packaging, cron, queues
- **No Composer/SSH on the server**: `tools/build-release.sh` ships `vendor/` inside the zip.
- **Document root**: a root `.htaccess` forwards everything to `public/` and blocks dotfiles and internal folders when the docroot cannot be changed. Installer health check verifies `/.env` is not downloadable.
- **Pre-install bootstrap**: no `.env`/DB exists yet, so the provider runs the installer on file sessions with a temporary key, then writes a fresh production `APP_KEY` and switches to database sessions/cache/queue.
- **Scheduler**: one cron line (`schedule:run` every minute), shown with the detected PHP path on the completion screen. A heartbeat task proves cron is alive (surfaced in the health check).
- **Queues**: no daemon. The schedule runs `queue:work --stop-when-empty --max-time=50` every minute with the **database** driver.
- **Evidence files** go on the private disk and are served through an authorised controller, so `storage:link` / symlinks are not required (often disabled on shared hosts).

## 4. Multi-tenancy
- One install per Assembly is the default. Every business table carries `organization_id`, and `BelongsToOrganization` adds a global scope plus auto-fill, so a central multi-Assembly deployment later needs no schema rewrite. The `User` model is deliberately excluded from the scope.

## 5. Sync conflict rules (`SyncService`)
| Case | Result |
|---|---|
| Same idempotency key sent twice | `duplicate`, nothing applied twice |
| `create` for an existing UUID | `ok` (idempotent) |
| Update to an append-only entity (e.g. submitted inspection) | `error: immutable` |
| Stale update, no protected field touched | last-write-wins, `ok` |
| Stale update touching a protected field | `conflict`, nothing applied, shown to the officer and counted on the supervisor sync-status endpoint |
- Inspections are officer-created and become append-only on submit, so real conflicts are limited to premises edits and task status.
- Client IDs are UUIDs generated on-device; the server stays the system of record.

## Not yet done / known limits
- Photo/evidence sync (chunked, resumable upload) is Phase 2.
- Web login and dashboard UI are not built yet; the API login and sync are.
- This code was written without a PHP runtime in the build environment, so it has **not been executed**. First task on your machine: run the smoke test in the README and fix whatever surfaces.
