Files
delivery-app-api/docs/architecture.md
T

49 lines
7.1 KiB
Markdown

# Initial architecture
## Decisions
Use a pnpm monorepo, React 19 + Vite 8 + TypeScript 5.9, Dexie 4 and dexie-react-hooks for local reactivity, vite-plugin-pwa/Workbox for the static app shell, Fastify 5 and the official MariaDB Node driver. Shared Zod 4 contracts validate both user operations and server responses. Vitest, fake-indexeddb and Playwright cover consistency and the actual offline browser flow. The lockfile pins the complete resolved dependency graph. No ORM, NestJS, router or state framework is needed for this slice.
The selected Vite release supports the installed Node 22.16 runtime; see the [Vite requirements](https://vite.dev/guide/). Network calls stay outside Dexie transactions, following [Dexie's transaction guidance](https://dexie.org/docs/Tutorial/Best-Practices). The service worker caches the app shell; IndexedDB owns operational data and the outbox.
## Data model
| Table | Identity and contents |
| --- | --- |
| customers | UUID; name, contact, phone, comments, three preferred delivery windows, version and timestamps/tombstone |
| customer_addresses | UUID; customer FK, label, formatted address plus city/street/building number, version and timestamps |
| delivery_types | UUID; unique code, Hebrew label, configurable unit hundredths, version and timestamps |
| deliveries | UUID; unique server business number, customer/address/type FKs, planned date, type version, immutable unit and price snapshots, notes, status, entity version and timestamps |
| sync_state | Singleton serialization lock, committed sequence, next delivery number |
| sync_operations | Globally unique operation UUID, device UUID, canonical payload hash, immutable acknowledgment and timestamp |
| sync_changes | Ordered sequence, entity type/UUID and complete versioned record (including tombstone) |
| schema_migrations | Ordered migration filename, SHA-256 checksum and application time |
| app_users | UUID, display name, role, active flag and last verified OIDC group snapshot; recovery password hash or unique issuer/subject identity hash |
| auth_sessions | Hashed session identifier, user FK, absolute expiry, provider and encrypted token bundle |
| oidc_settings | Encrypted singleton SSO configuration and administrator audit metadata |
| oidc_transactions | Hashed browser binding, encrypted state/nonce/PKCE verifier and ten-minute expiry |
The composite delivery FK `(address_id, customer_id)` references the assigned address/customer pair. JSON wire fields use camelCase; SQL uses snake_case. UUID identity exists before sync. Versions start at 0 locally and 1 after server creation. Creates require baseVersion 0. Customer and delivery changes require the exact current positive version and increment it. Customer removal writes a tombstone and preserves addresses/deliveries; archived customers cannot receive new deliveries. Delivery updates change customer/address/planned date/timing/notes. A named timing choice snapshots the customer's current delivery window; a custom choice stores one exact time. Cancellation stores a reason and server timestamp, retains the number/price and prevents further edits. Older records without a planned date fall back to their creation date in local summaries, and pre-v5 records display an unset timing until edited. There is no permanent-deletion API. Sync receipts include the authenticated actor's user UUID; legacy anonymous receipts remain readable for cache migration.
Money is stored as integer agorot (one hundredth of an ILS); shipment units as integer hundredths. `total = floor((unitPriceAgorot * shipmentUnitHundredths + 50) / 100)`. Input bounds keep multiplication within JavaScript safe-integer precision. The server checks the selected delivery type version and multiplier, then persists the supplied unit-price snapshot and calculated total. The first prototype allows explicit manual entry of the unit price, including zero; it has no pricing policy engine. Catalog version changes reject stale offline drafts for review instead of silently repricing them.
## Consistency
An IndexedDB transaction writes the optimistic entity and FIFO outbox entry together. A persisted device ID is generated atomically. Browser Web Locks serialize sync across tabs where supported; server idempotency provides duplicate protection independently.
The API locks sync_state before checking receipts and applying writes. Entity creation, number allocation, change log and receipt commit atomically. Serializing writes is a deliberate small-scale tradeoff: it guarantees sequence order follows commit order. An auto-increment change log without commit ordering can cause a client to skip an earlier, still-uncommitted change. At larger scale, replace this lock with a proven commit-ordered feed, not an uncoordinated sequence.
Migrations use a MariaDB advisory lock and checksum checks. DDL implicitly commits; initial CREATE statements are rerunnable after interruption. The small runner supports semicolon-separated SQL statements, not stored routines/DELIMITER syntax. Later migrations must account for interrupted DDL and use new files instead of altering applied ones.
OIDC login always requests Synology's `groups` scope. When group access is enabled, the verified ID-token/UserInfo group claim must contain at least one configured allowlist entry. Matching is exact after Unicode normalization and case folding. The validated group snapshot is stored on the user and checked on every online app-session read, so saving changed access settings invalidates or rejects ineligible SSO sessions. The local recovery administrator is outside this rule and remains able to repair configuration. Group membership changes at the provider are learned on the next SSO login; fully offline clients cannot learn revocation.
## Limits that remain
- One lab workspace, group-admitted users, separate device caches and role scaffolding; no tenant, role-specific action or record-level permission isolation. Keep both servers on loopback pending deployment work. See [authentication](authentication.md).
- Explicit customer and delivery conflict review is implemented. No automatic resolution, address editing, permanent-deletion API, compaction or arbitrary outbox editing. One pending operation per editable entity; finish synchronization before its next edit.
- Full delta history is retained. New devices start at cursor 0. A cursor ahead of the server is rejected; database replacement with a new history/epoch needs an explicit future bootstrap protocol. Do not reset a server database while reusing device stores.
- IndexedDB durability does not protect against browser data clearing or storage eviction. Persistence permission is best effort.
- Background OS execution is not required. Active tabs sync on reconnect, visibility, launch, manual request and every 30 seconds while visible. Transport failures keep entries pending; validation conflicts become explicit errors.
- The PWA must be loaded online once before offline launch. Service-worker updates prompt for refresh; unsent outbox data remains in IndexedDB, but unfinished form text is not a saved draft.
- Physical iPad/Samsung and Safari validation is separate from Chromium viewport checks.