# Full Platform Bottom-Up Audit — Findings

**Started:** July 2026
**Purpose:** Single running record for the staged, module-by-module audit of the Go Past Earth Business OS. Each stage below is filled in as it completes. This document is the source of truth for what was checked, what was found, and what remains open — read this before re-auditing anything already covered here.

**How to read a finding:**

| Tag | Meaning |
|-----|---------|
| **Accurate** | Docs and code agree; claim holds. |
| **Drifted** | Docs say X, code does Y (or code moved on and docs did not). |
| **Missing** | Documented but not implemented, or implemented but undocumented. |
| **Risk** | Security / tenant-isolation / data-integrity concern. |
| **Real-world gap** | Technically works, but does not actually serve a business vertical we claim to serve (the pattern that surfaced the inventory unit-of-measure issue). |

Each finding also gets a rough **severity** (Low / Medium / High) and **effort** (Small / Medium / Large) tag so Stage 12 can build a real prioritized roadmap instead of a wall of text.

---

## Claims Register (audited against in every later stage)

Extracted from `[00-CEO-STRATEGIC-OVERVIEW.md](00-CEO-STRATEGIC-OVERVIEW.md)` and `[28-COMPETITIVE-POSITIONING-AND-ICP-WORKBOOK.md](28-COMPETITIVE-POSITIONING-AND-ICP-WORKBOOK.md)`. These are the specific, checkable statements every later stage should either confirm, refine, or flag as unverified/false.

| # | Claim | Source | Checked in stage |
|---|-------|--------|-------------------|
| C1 | "One platform. Every business need." — any business type (restaurant, retail, salon, clinic, professional services), any payment type, any device, any scale | 00 | Cross-cutting; per-module real-world-gap checks |
| C2 | Multi-tenant SaaS with "true isolation" — each business gets dedicated databases | 00 | Stage 2 |
| C3 | Bridge architecture: cloud + on-premises, devices/printers/KDS connect via bridge, no port forwarding | 00 | Stage 10 |
| C4 | Invoicing is the backbone — every sale, ticket, adjustment flows through Core Invoicing, single source of truth | 00 | Stage 5 |
| C5 | Template provisioning — new tenants get consistent, complete setup from templates, no manual DB setup | 00 | Stage 2 |
| C6 | Cryptocurrency is non-custodial — businesses control keys (DOGE, LTC, BTC), no middleman | 00 | Stage 5 |
| C7 | "Data isolation — tenant data never crosses boundaries" | 00 | Stage 2, Stage 11 |
| C8 | "HR data — separate HR DB per tenant for compliance" | 00 | Stage 2, Stage 8 |
| C9 | "Audit — audit logging for key actions" | 00 | Stage 9, Stage 11 |
| C10 | "Session security — session-based auth, no JWT in main flow" | 00 | Stage 3, Stage 11 |
| C11 | "Production-ready," "scalable" (PM2 cluster, Nginx, Google Cloud SQL), "extensible" (100+ route files, 90+ models) | 00 | Stage 1, Stage 3 |
| C12 | Direct competitor to Lightspeed; "similar breadth... with crypto-native differentiation and a modern, multi-tenant SaaS architecture" | 00 | Cross-cutting (tempered by C13) |
| C13 | Coverage-vs-depth framing: breadth gaps outside the stated ICP are "integrate" or "defer," not existential — provided the ICP itself is actually defined | 28 §1.1 | Stage 0 (see finding S0-3) |
| C14 | Ideal Customer Profile (ICP) one-liner exists to calibrate all gap conversations | 28 §2 | Stage 0 (see finding S0-3) |
| C15 | Top-10 daily workflow comparison matrix with gap tags (must-build / integrate / defer / not-our-market) | 28 §3 | Stage 0 (see finding S0-3); revisited per relevant module stage |
| C16 | Platform coverage map (`25-platform-coverage.html`) is kept at "100% coverage" — every router mount has a doc row | 25 | Stage 3, Stage 10 |
| C17 | Platform barcode/UPC catalog: master-owned `platform_product_catalog` table enriches tenant scans, tenants keep their own stock/pricing | 25, index.html | Stage 10 |

---

## Stage 0 — Vision & Competitive Positioning Baseline

**Docs read in full:** `00-CEO-STRATEGIC-OVERVIEW.md`, `28-COMPETITIVE-POSITIONING-AND-ICP-WORKBOOK.md`, `25-platform-coverage.html`, `index.html` (barcode catalog + real-world transaction scope sections).

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S0-1 | Accurate | The platform coverage map (`25-platform-coverage.html`) is unusually honest for a CEO-facing doc set: it explicitly flags RBAC depth as "evolving" (not every POS action has a separate permission checkbox yet), audit coverage as "grows as more events call `AuditService`" (i.e. partial), and inventory variance UI as "may vary by deployment." This is a good anchor — later stages should hold the *rest* of the doc set to this same honesty bar rather than the more triumphant tone in `00`. | Low | — |
| S0-2 | Drifted (internal) | `00-CEO-STRATEGIC-OVERVIEW.md` states unqualified claims ("sophisticated, superior alternative to Square/Stripe/PayPal," "direct competitor to Lightspeed") without the same nuance `28`'s own "three lenses" section insists on (coverage-vs-depth, ecosystem gravity, enterprise table stakes). The two documents are not mutually consistent in tone — `00` reads like final marketing copy, `28` reads like an internal calibration tool. Recommendation for Stage 12: fold `28`'s three-lenses caveat into `00`, or add a "how to read this doc" note pointing from `00` to `28`. | Low | Small |
| S0-3 | Missing / Real-world gap | The ICP workbook (`28`) is a **template that has not been filled in**. Section 2's "Your live ICP" fields are blank placeholders (`We win with _________`), and all 10 rows of the top-10-daily-workflow gap-tag matrix in Section 3 read `*fill*` instead of an actual tag. This means the single document designed to keep "are we a full product?" anxiety grounded in a real target customer has never actually been completed. Every claim in `00` that depends on "who is this for" (C1, C12, C13, C14, C15) is therefore **unverified against a real ICP** — it exists only as an aspiration ("any business type, any scale") with no stated boundary. This is the most actionable Stage 0 finding: without a filled-in ICP, later stages cannot cleanly separate "must-build" gaps from "not our market" ones when we hit real product gaps (as we already did once with inventory unit-of-measure for farm businesses). | Medium | Small (this is a workshop/writing exercise, not code) |
| S0-4 | Real-world gap (evidence) | We already have a concrete, lived example of what happens without a filled-in ICP: the original `ItemWizardSimple` inventory create flow (single flat price/cost field, no unit of measure) was fine for a pizza-shop-style ICP but broke down for a farm-stand tenant (dozens of eggs, jar sizes of honey). This is exactly the kind of gap Section 3, row 5 ("Receive or adjust inventory") of the ICP workbook exists to catch — but the workbook was never filled in, so nothing forced this conversation to happen earlier than it did (via manual user testing). Stage 7 (Inventory) should treat this as the first data point, not a one-off. | Medium | — (already partially fixed in-session; tracked for completeness) |
| S0-5 | Accurate | The barcode/UPC catalog service claim (C17) is concretely documented and consistent across three separate doc locations (`index.html` "Platform Product Catalog (Barcode) — Implemented" section, `25-platform-coverage.html` "Platform Product / Barcode Catalog" section, and the coverage table row). It names a real table (`platform_product_catalog` on the **master** DB, not per-tenant), a real tenant-facing endpoint (`GET /api/platform-catalog/lookup`), a real master-admin CRUD surface (`/api/master-admin/platform-catalog`), and real UI touchpoints (`ItemsPage` + `ItemWizardSimple` prefill, `ModernPOS` synthetic line). This is a good sign for Stage 10 — the documentation is specific enough to verify against code directly rather than needing to reverse-engineer intent. | — | — |
| S0-6 | Missing | `00-CEO-STRATEGIC-OVERVIEW.md`'s "Technical Maturity" section quotes specific numbers ("100+ route files, 90+ models") without a source or last-verified date. These numbers will drift as the codebase grows and nobody has a documented process for re-counting them. Low priority to fix directly, but Stage 3 should spot-check whether these are still roughly accurate, and Stage 12 should recommend either removing precise numbers from marketing-facing docs or tying them to the same "maintaining coverage" discipline `25-platform-coverage.html` already asks for its own table. | Low | Small |

### Stage 0 conclusion

The vision/positioning documentation is more self-aware than a typical "everything platform" pitch — `25-platform-coverage.html` in particular already admits partial coverage in RBAC, audit, and inventory variance. The biggest actual gap at this level is not a false claim but an **unfinished tool**: the ICP workbook that is supposed to keep every later "is this good enough" conversation grounded was never completed. This audit will use the Claims Register above as a working substitute for that missing ICP throughout, and will flag it again in Stage 12 as the top strategic (non-engineering) recommendation.

**Claims register status after Stage 0:** C1, C12, C13, C14, C15 marked *unverified — no completed ICP to check against*; C17 confirmed consistent across docs pending Stage 10 code verification; all others carried forward to their assigned stage.

---

## Stage 1 — Infrastructure & Deployment

**Docs read:** `01-SERVER-AND-DEPLOYMENT.md`, `infrastructure/CURRENT-STATUS.md`, `infrastructure/ecosystem.production.config.js`, root `ecosystem.config.js`, `infrastructure/nginx.conf`, `infrastructure/nginx-staging.conf`, `infrastructure/nginx-ssl.conf`.

**Live verification performed:** `pm2 describe` on frontend/public processes; live vs repo Nginx config diff; `systemctl status nginx`; direct `curl` tests against `https://staging.gopastearth.com`; live Cloud SQL instance facts already gathered earlier in this session (`ppos-sandyone`, see below).

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S1-1 | **Risk / Real-world gap — confirmed live** | The live Nginx staging config (`/etc/nginx/sites-available/pawspos-staging`) is missing the `location /media/` block that the repo copy (`infrastructure/nginx-staging.conf`) already has, with a comment explaining exactly why it matters: *"Without this, /media/* falls through to SPA and shows the dashboard instead of the file."* I verified this live: `curl -I https://staging.gopastearth.com/media/platform-catalog/3tywSQLyPXbhEbY6Jr.jpg` (a real, existing 226KB JPEG uploaded to `/mnt/vfs/platform-catalog/`) returns **HTTP 200 with `content-type: text/html`** — i.e. the dashboard's `index.html`, not the image. **Every platform barcode/UPC catalog product image is currently inaccessible to end users on staging.** This directly undermines the exact feature the user specifically called out ("the barcode service... trickles down into a benefit for the customers"): the images that are supposed to enrich scanned products are silently broken. Fix is small — the corrected config already exists in the repo; it just needs to be copied to `/etc/nginx/sites-available/pawspos-staging` and `nginx -t && systemctl reload nginx` run. | **High** | **Small** |
| S1-2 | Real-world gap / Risk | The customer-facing "dashboard" React app (everything under `/admin`, `/dashboard`, `/login`, `/signup`, `/support`, `/book`, POS routes, etc.) is served in **all three** Nginx configs (`nginx.conf`, `nginx-staging.conf`, `nginx-ssl.conf`) by **proxying directly to a live Vite development server** (`npx vite --host 0.0.0.0 --port 5173`, confirmed running via `pm2 describe pawspos-frontend`) — not a built, minified `dist/` bundle served statically. This is disclosed in `01-SERVER-AND-DEPLOYMENT.md` itself ("Frontend — Vite dev server (port 5173)"), so it is not hidden, but it materially undercuts the CEO overview's "production-ready" and "scalable (PM2 cluster...)" claims (C11): only the backend runs in PM2 **cluster** mode; the dashboard app users actually interact with runs as a **single fork-mode dev server process** with no redundancy, on-the-fly per-request source transforms instead of pre-built/minified assets, and none of the caching headers used for `public-site`'s static build. Practical implication for this session specifically: every "rebuild and deploy" cycle we ran (`npm run build` + `pm2 restart pawspos-frontend`) produced a `frontend/dist/` bundle that **Nginx does not serve for dashboard routes** — the PM2 **restart** (which makes the dev server re-read source) is what actually mattered, not the build artifact. The build step is still useful (compile-error check, `apiEndpointsCatalog.json`/`api-explorer.html` regeneration) but does not equal "we deployed a production bundle" for the primary business app. Recommend migrating the frontend dashboard to the same build-and-serve-static pattern already used for `public-site` (`vite build` → Nginx serves `dist/` directly, PM2 only needed for an optional preview/health role or removed entirely for that app). | Medium | Medium |
| S1-3 | Drifted | `infrastructure/CURRENT-STATUS.md` is dated **December 24, 2025** and describes the database connection as actively broken ("Server IP not in Google Cloud SQL authorized networks... Login and database-dependent features won't work") and `pawspos-frontend` as "Errored (vite permission issues)." Neither is true today — we've queried the live database extensively this session, and `pawspos-frontend` is `online` per `pm2 describe`. This file has no update discipline and, if consulted during a future incident, would send someone down a stale troubleshooting path (re-checking authorized networks that are already fine, or debugging a "vite permission issue" that no longer exists). Recommend either deleting this file or replacing its content with a pointer to `01-SERVER-AND-DEPLOYMENT.md` plus a live `pm2 status` check as the actual source of truth. | Low | Small |
| S1-4 | Missing / Risk | `infrastructure/ecosystem.production.config.js` hardcodes production secrets in plaintext directly in the file (`MYSQL_PASSWORD: 'Pawspos2024!'`, `SECRETS_KEY: 'a6e45603fb...'`). On this server there is **no `.git` directory** at the app root (`git` commands fail with "not a git repository"), so these secrets are not at risk of being pushed to a shared repo from *this* box specifically — but if the same file is version-controlled anywhere else (a laptop, a CI system, another checkout), the secrets would be committed in plaintext. Both this file and `backend/.env` are also world-readable (`-rw-r--r--` / 644) on disk. Recommend: (a) confirm whether this file exists in any git history elsewhere and rotate `SECRETS_KEY`/`MYSQL_PASSWORD` if so, (b) tighten file permissions to `600`, (c) move toward environment-injected secrets (systemd `EnvironmentFile`, a secrets manager, or at minimum `backend/.env` + `.gitignore`) rather than committing credentials into a config file that also holds CORS origins and other non-secret settings. Cross-reference: Stage 11 security rollup. | Medium | Small–Medium |
| S1-5 | Missing (doc gap) | `01-SERVER-AND-DEPLOYMENT.md`'s "Deployment Steps" section instructs `cd public-site && npm run build` but has no equivalent step for `frontend`, and its step 5 (`pm2 restart pawspos-backend pawspos-frontend pawspos-public`) does not clarify that `pawspos-frontend`'s restart serves live source rather than any build output. Someone following this doc literally would reasonably (and incorrectly) assume a `frontend` rebuild is needed and sufficient for changes to appear — as this session did implicitly across several "rebuild and deploy" requests. Recommend the doc explicitly state which of the two frontend apps is build-and-serve-static vs live-dev-server, matching the S1-2 architecture reality. | Low | Small |
| S1-6 | Accurate (with a caveat) | `01`'s Cloud SQL Proxy section and the `cloud-sql-proxy` PM2 app entry in `ecosystem.production.config.js` describe an **optional** proxy path, but the actual `MYSQL_HOST` used in that same file is the **direct** Cloud SQL public IP (`34.174.101.97`), not `127.0.0.1` (which would indicate proxy use). This matches what we observed independently this session: `cloud-sql-proxy` has consistently shown as `errored` (`pid 0`, `9` restarts) in every `pm2 list` we ran, with no functional impact, because the backend never actually depends on it — it connects directly. The doc's own table already marks this app "Optional," so this is not a drift so much as a piece of dead/confusing config: an always-erroring PM2 entry that looks alarming in `pm2 list` but is inert. Recommend either removing the `cloud-sql-proxy` PM2 app entirely (if direct connection is the permanent design) or fixing/deprioritizing it explicitly so it stops showing as a red "errored" row for anyone glancing at process status. | Low | Small |
| S1-7 | Cross-reference (not re-litigated here) | Live Cloud SQL instance facts already gathered earlier this session for `ppos-sandyone` (tier `db-g1-small` after our resize, disk `250GB` `PD_SSD` unchanged, `ENTERPRISE` edition, `ZONAL` availability, automated backups now enabled, `deletionProtectionEnabled: true`) are **not yet reflected anywhere in the OAO docs** (`01-SERVER-AND-DEPLOYMENT.md` only says "Cloud SQL proxy" generically with no tier/disk/edition detail). Recommend Stage 12 synthesis include a short "current live instance shape" addition to `01`, since this is exactly the kind of infrastructure fact that should not live only in chat history. | Low | Small |

### Stage 1 conclusion

Infrastructure documentation is directionally accurate about *what* runs where (PM2 apps, ports, Nginx role) but has drifted in two important ways: (1) a **real, currently-live bug** where the Nginx config actually deployed on the server lags behind the repo's own (more correct) copy, breaking every barcode/UPC catalog product image on staging right now (S1-1) — directly relevant to the user's specific "barcode service... benefit for customers" claim; and (2) the gap between "production-ready / scalable" language at the CEO level and the reality that the primary dashboard app runs on a single-instance development server rather than a built production bundle (S1-2). Neither finding is catastrophic on its own, but both are concrete, verifiable, and actionable — exactly the kind of thing a "very serious and very professional" review should surface rather than take documentation claims at face value.

**Claims register status after Stage 1:** C11 ("production-ready," "scalable... PM2 cluster") — **partially unverified**: true for the backend, not true for the frontend dashboard app; carried to Stage 3 for the backend-specific half of this claim and flagged for Stage 12 synthesis as a wording fix.

---

---

## Stage 2 — Databases & Multi-Tenancy

**Docs read:** `02-DATABASES-AND-MULTI-TENANCY.md`, `APPENDIX-C-COMPLETE-TENANT-PROVISIONING-FLOW.md` (Appendix E schemas referenced but not exhaustively re-verified table-by-table given time budget — flagged for a targeted follow-up if needed).

**Code cross-checked:** `backend/config/tenantDatabase.js`, `backend/middleware/tenantResolver.js`, `backend/utils/resolveTenantContext.js`, `backend/services/comprehensiveTenantProvisioning.js`, plus a grep sweep of `backend/routes/*.js` for raw-SQL string-building patterns. Live database list from `gcloud sql databases list --instance=ppos-sandyone` (captured earlier this session).

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S2-1 | **Accurate — verified live** | The documented three-database-per-tenant pattern (`dev-{slug}` main, `dev-analytics-{slug}`, `dev-hr-{slug}`) is real and matches what actually exists on the live Cloud SQL instance: `dev-mariospizza` / `dev-analytics-mariospizza` / `dev-hr-mariospizza`, and the same triple for `sawyer-farms`, `mistys-beautique`, `tiger-one`, and `template`. `getTenantConnection(tenantName, dbType)` in `tenantDatabase.js` correctly supports only `'main'` and `'analytics'` (doc's "No HR" note is accurate — HR goes through `multiDatabaseManager.js` instead, also confirmed in code). This is one of the platform's most solid, well-documented, and independently verifiable architectural claims (C2). | — | — |
| S2-2 | Missing (doc gap) | A fourth database-naming pattern exists in **live code and the live database list** that is **not documented anywhere** in `02-DATABASES-AND-MULTI-TENANCY.md`: the special tenant slug `master-tenant` (→ `dev-master-tenant` database, confirmed present in the live DB list). This is used as a fallback context specifically for master-admin users who have not selected a specific tenant — found in `backend/utils/resolveTenantContext.js` ("For master admin without tenant context, default to master-tenant") and independently in `backend/routes/inventory.items.routes.js` ("For master admin, default to master-tenant... orgId = orgId \|\| 1"). Anyone reading only the docs would have no idea `dev-master-tenant` exists, what it's for, or why `orgId=1` is hardcoded as its default. Recommend adding a short "Master admin default context" subsection to `02`. | Low | Small |
| S2-3 | Drifted (nuance missing) | The CEO overview's absolute claim (C7) — "Data isolation — Tenant data never crosses boundaries" — needs a documented carve-out. `tenantResolver.js` contains an explicit, intentional **"Master admin bypass — Allow cross-tenant access for admin portal"** branch: master admins are granted `canAccessAllTenants: true` and can operate across tenant boundaries by design (this is necessary for a master-admin console and is not a bug), but the current wording in `00-CEO-STRATEGIC-OVERVIEW.md` does not mention this exception at all. A security-conscious reader (or auditor, or enterprise buyer doing due diligence per the ICP workbook's "enterprise table stakes" section) would reasonably read "never crosses boundaries" as an absolute and be surprised to find a privileged bypass path. Recommend rewording to "Tenant data is isolated by dedicated per-tenant databases; only master-admin platform operators have cross-tenant access, and that access is itself auditable" (contingent on S2-5 below actually being true in practice). | Medium | Small (doc wording) |
| S2-4 | **Risk — evidenced across multiple files** | A manual SQL-escaping pattern (`someValue.replace(/'/g, "''")` inline string interpolation into SQL, instead of parameterized/prepared-statement placeholders) is used to build `WHERE`, `UPDATE`, and `INSERT` clauses in at least **seven** real backend route files: `backend/routes/staff.routes.js`, `backend/routes/devices.routes.js`, `backend/routes/inventory.items.routes.js` (confirmed directly — e.g. its `updateItem` handler builds `UPDATE InventoryItems SET name = '${String(data.name).replace(/'/g, "''")}' ... WHERE id = ${itemId} AND org_id = ${orgId}` as a single interpolated string, not a parameterized query), `backend/routes/transactions.routes.js`, `backend/routes/customers.routes.js`, `backend/routes/admin.catalog.routes.js`, and `backend/routes/admin.database.routes.js`. Manual quote-escaping is a well-known SQL-injection-prone pattern: it is easy to get subtly wrong (e.g. under certain SQL modes, backslash-based bypasses exist; NULL bytes, multi-byte encoding tricks, and non-string injection vectors are not addressed by a single `.replace()` call), and it provides no defense-in-depth compared to driver-level parameter binding, which the codebase's own `mysql2` dependency supports natively via `?` placeholders (used correctly in many *other* files, e.g. the `SELECT` queries earlier in the same `inventory.items.routes.js` file use `conn.query('...WHERE org_id = ? AND sku = ? AND domain = ? LIMIT 1', [orgId, data.sku, domain])` — so the safe pattern is already the house style elsewhere, making this an inconsistency rather than a fundamental architecture gap). This is a genuine security debt item; while org_id/tenant-id values in the reviewed cases were numeric (lower individual risk), string fields (name, description, category, sku, barcode) go through this pattern too. **Recommend a dedicated remediation pass converting all `.replace(/'/g, "''")` interpolation sites to parameterized queries** — flagged again for the Stage 11 cross-cutting security rollup with the full file list. | **High** | Medium (mechanical but must touch 7+ files carefully, with tests) |
| S2-5 | Risk (needs deeper review, not resolved here) | `tenantResolver.js` logs extensive per-request diagnostic detail via raw `console.log()` (session contents, user IDs, org IDs, tenant slugs, and raw header values) rather than the project's structured `logger` (used elsewhere, e.g. `logger.info`/`logger.warn` in `inventory.items.routes.js`). Two separate concerns: (a) this is noisy/unstructured compared to the rest of the codebase's logging discipline, and (b) depending on log rotation/retention and who has server access, session/user identifiers being written to plain stdout logs (`logs/backend-out.log`) on every request is a minor PII-in-logs concern worth tightening, especially given `backend/.env` and other config files were already found world-readable in Stage 1 (S1-4). Not severity-High on its own, but compounds with S1-4's file-permission finding. Recommend converting to leveled `logger.debug()` calls (already gated in other files) so this is opt-in rather than always-on in production. | Low–Medium | Small |
| S2-6 | Accurate (spot-checked) | `Appendix C`'s step-by-step provisioning flow matches the real code in `backend/services/comprehensiveTenantProvisioning.js`: function names (`provisionTenant`, `copyDatabaseSchema`, `createOwnerUser`, `createOwnerStaff`, `registerEmployeeRouting`, `cleanupFailedProvisioning`) and the `CREATE DATABASE IF NOT EXISTS` step all exist as described. This was a spot-check of structure/names, not a line-by-line re-verification of every SQL statement or edge case (e.g. the "HR fallback to `createHRDatabaseFromSQL()` if template missing" branch mentioned in Appendix C was not independently traced) — flagged as a candidate for a deeper pass if a future incident involves failed tenant provisioning. | — | — |

### Stage 2 conclusion

The core multi-tenancy architecture is genuinely solid and the documentation for it is largely accurate and independently verifiable against the live database — this is one of the platform's strongest areas. The two things worth real attention are not architectural flaws but **documentation completeness** (the undocumented `master-tenant` fallback pattern, S2-2) and a **security code-quality inconsistency** (manual SQL escaping used inconsistently alongside the safer parameterized pattern already established elsewhere in the same files, S2-4) — the latter is the most concrete, actionable security finding of the audit so far and should be prioritized ahead of most other work identified in this review.

**Claims register status after Stage 2:** C2 (true isolation via dedicated DBs) — **confirmed accurate** at the database-separation level; C7 ("never crosses boundaries") — **needs wording qualification**, not a functional bug, tracked to Stage 12; C5 (template provisioning) — **confirmed accurate** via spot-check; C8 (separate HR DB) — **confirmed accurate**, deeper compliance-angle check (who can query the HR DB and how) deferred to Stage 8.

---

---

## Stage 3 — Backend Architecture, Routing & Auth

**Docs read:** `03-BACKEND-ARCHITECTURE.md`, `APPENDIX-D-COMPLETE-AUTH-FLOW.md`.

**Code cross-checked:** `backend/server.js` middleware registration order, `backend/middleware/tenantResolver.js` (full file), `backend/middleware/sessionAuth.js`, `backend/middleware/flexibleAuth.js`, `backend/middleware/deviceAuth.js`, `backend/routes/api/kiosk/index.js`.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S3-1 | **Risk — confirmed live, Critical** | The session-signing secret has **no environment override anywhere in this deployment**. `backend/middleware/sessionAuth.js` sets `secret: process.env.SESSION_SECRET \|\| 'pawspos-secret-key-change-in-production'`, and I confirmed by direct grep that `SESSION_SECRET` does not appear in `backend/.env`, `infrastructure/ecosystem.production.config.js`, or root `ecosystem.config.js` — the three places that plausibly set backend environment variables on this server. This means **the live staging server is signing every session cookie with a hardcoded, source-visible default value**. Combined with S3-2 below, this meaningfully undercuts claim C10 ("Session security — session-based auth"). Recommend: generate a strong random `SESSION_SECRET`, set it in `backend/.env` (not committed) or the PM2 ecosystem file with restricted permissions, and rotate it (which will invalidate all current sessions — plan for a maintenance window). | **High** | Small |
| S3-2 | **Risk — confirmed live** | The session cookie is configured with **`secure: false`** in `sessionAuth.js`, with the code comment literally reading `// Set to true in production with HTTPS`. This deployment **is** in production/staging behind valid HTTPS (confirmed in Stage 1 — `https://staging.gopastearth.com` serves via Let's Encrypt certs through Nginx). The `secure` flag was never flipped despite the infrastructure being ready for it. Practical effect: the `pawspos.sid` cookie can legally be sent over a plain-HTTP connection to the same host if one is ever reachable (e.g., a misconfigured path, a future subdomain without forced HTTPS, or a downgrade scenario), which a `Secure` flag exists specifically to prevent. Recommend: `secure: process.env.NODE_ENV === 'production'` (or an explicit `COOKIE_SECURE` env flag), verified after the S1 Nginx fix (S1-1) is deployed, since HTTPS is already terminated correctly at the proxy layer. | **High** | Small |
| S3-3 | **Risk — evidenced, needs Stage 10/11 follow-up** | `deviceAuth.js` (used to protect `/api/kiosk/*` and `/api/mobile/*`, per `router.use(tenantResolver); router.use(deviceAuth);` in `backend/routes/api/kiosk/index.js`) does **no verification at all** of the client-supplied `x-device-id` header — it is read directly off the request and attached to `req.device.deviceId` with zero lookup against a `Devices` table, no per-device secret/key check, no signature. I traced actual usage in `kiosk/index.js`'s cart-create and checkout handlers and found no downstream validation either — `req.device.deviceId` is used purely as a label passed into `CartService`/`InvoiceService`, never checked for legitimacy. Combined with `tenantResolver`'s header-based fallback path (which can resolve a tenant purely from a `X-Tenant-Slug` header when no session exists — i.e., knowledge of a tenant's slug, which may be guessable/discoverable from that business's own storefront URL), the practical security boundary for kiosk/mobile checkout endpoints may be weaker than the name "Device Auth" implies. This finding is **not conclusive** on its own — I have not yet traced whether kiosk/mobile traffic is additionally restricted by network-level controls, a device-pairing/claim step (`deviceCodes.routes.js`, not yet reviewed) that issues a real per-device credential the header is expected to carry, or other checks upstream. Flagging explicitly for **Stage 10** (device pairing/claim flow) and **Stage 11** (cross-cutting security) to close out definitively rather than asserting an active exploit here. | High (pending confirmation) | Medium (once scoped) |
| S3-4 | Risk (systemic, compounds S2-5) | Verbose, unstructured `console.log()` calls printing session contents, user IDs, org IDs, and raw headers appear throughout the core auth path — not just `tenantResolver.js` (flagged in S2-5) but also `sessionAuth.js` (`requireSessionAuth` logs `userId`, `userEmail`, `userRole`, cookie presence on every request) and `flexibleAuth.js` (logs full `req.session.user` and `req.session.staffMember` objects). This is a systemic pattern across the entire authentication middleware stack, not an isolated file, and raises the earlier logging finding's severity: on every authenticated request, user emails and role data are written to plaintext stdout logs. Recommend a single remediation pass converting all of these to the project's existing structured `logger` at `debug` level (already used correctly elsewhere, e.g. `logger.info`/`logger.warn` in `inventory.items.routes.js`), gated off by default in production. | Medium | Small–Medium |
| S3-5 | Accurate (spot-checked) | `backend/server.js`'s middleware order matches the doc's high-level description and — more importantly — gets a subtle, security-relevant detail **right**: the Stripe/Coinbase/Coinremitter webhook routers (lines 324-326) are mounted **before** `express.json()` (line 329), which is the correct pattern for webhook signature verification that needs the raw request body. This is a good, deliberate piece of engineering that the doc doesn't call out explicitly but should — it's exactly the kind of subtlety worth documenting so a future contributor doesn't "fix" the ordering and silently break webhook verification. | — | Small (doc addition only) |
| S3-6 | Missing (doc gap) | Route mounting in `server.js` is **inconsistent** about where auth is enforced: most routers are mounted with `requireSessionAuth, tenantResolver, [checkFeatureAccess(...)]` directly in the `app.use(...)` call (easy to audit at a glance), but several — e.g. `/api/master-admin`, `/api/tenant-staff-auth`, `/api/staff-personal` — are mounted with **no auth middleware in the mount call itself**, meaning whatever auth they have is handled *inside* the router file. This is a legitimate pattern (not a bug per se) but it means a security reviewer scanning `server.js` top-to-bottom cannot assume "no middleware listed = unprotected" or "middleware listed = fully protected" consistently, which makes the file a less reliable single source of truth than `03-BACKEND-ARCHITECTURE.md` and `25-platform-coverage.html` currently imply. Recommend either (a) a documented convention note ("routers ending in `-auth` or handling their own bootstrapping manage auth internally — see file"), or (b) moving toward mount-level auth consistently where feasible, which also makes future route audits (as `25-platform-coverage.html` itself recommends doing periodically) more mechanical and less error-prone. | Low–Medium | Medium (mostly a documentation/convention fix; code changes only where genuinely inconsistent) |
| S3-7 | Drifted (minor, cosmetic) | `02-DATABASES-AND-MULTI-TENANCY.md`'s tenant-resolution summary (9 steps) and `APPENDIX-D-COMPLETE-AUTH-FLOW.md`'s summary (11 steps, exact order) describe the **same** `tenantResolver.js` code but collapse/order the steps slightly differently from each other (e.g. Appendix D splits "session tenantId," "session companyId," and "session orgId" into three distinct steps; `02` collapses these). Neither is wrong, but a reader cross-referencing both docs for the "exact" order (as `02` implies exists) would notice the mismatch. I traced the real code and confirmed **Appendix D's** step list is the more precise of the two, matching the actual `tenantResolver.js` source order (including the `-1`/`-0.5` special-cased steps for already-resolved and master-admin bypass that neither doc numbers explicitly). Recommend `02` simply point to Appendix D as the authoritative version instead of maintaining its own abbreviated copy. | Low | Small |

### Stage 3 conclusion

This is the most safety-relevant stage so far. The backend's routing and service structure documentation is accurate and the route inventory matches real files. However, the **auth/session layer has two confirmed, live, high-severity configuration weaknesses** (hardcoded fallback `SESSION_SECRET`, S3-1; non-`Secure` session cookie despite live HTTPS, S3-2) that directly contradict the platform's own "session security" claim (C10), and a **third, evidenced-but-not-fully-closed concern** about whether device authentication for kiosk/mobile is a real credential check or a self-reported label (S3-3). These three, especially S3-1 and S3-2, should be treated as the **highest-priority fixes** to come out of this entire audit to date — they are small to fix, clearly evidenced, and directly undermine a headline platform claim rather than being a documentation nicety.

**Claims register status after Stage 3:** C10 ("session-based auth... no JWT in main flow") — the "no JWT" half is accurate (confirmed session-cookie-based auth throughout), but the "security" half is **not currently true in practice** given S3-1/S3-2; C11 ("production-ready," backend half) — the backend's route/service structure and webhook-ordering are solid evidence *for* this claim, independent of the Stage 1 frontend-serving caveat.

---

---

## Stage 4 — Frontend & Public Site

**Docs read:** `04-FRONTEND-AND-PUBLIC-SITE.md` (Appendix F route tree spot-checked, not exhaustively re-verified route-by-route given time budget — its structure matches what this conversation has independently touched across `ItemsPage`, `ItemWizardSimple`, inventory pages, etc. in earlier work, which is a good accuracy signal).

**Code cross-checked:** `frontend/src/lib/offline/*` (all 11 files listed, key ones read in full: `tryOnlineThenQueue.js`, `registerSW.js`; usage patterns grepped across `queue.js`, `useOfflineStatus.js`, `useOfflineSimple.js`, `sw-client.js`, `queueSignal.js`).

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S4-1 | Accurate | `04`'s claim that dashboard routes (`/signup`, `/login`, `/dashboard`, `/admin`, etc.) are proxied to the Vite dev server on port 5173 in staging Nginx is **consistent** with what Stage 1 independently found and verified live (S1-2). Good example of a doc that discloses an architecture reality plainly rather than glossing over it — worth preserving this honesty when addressing S1-2's recommended migration to a built/served-static pattern (update this doc alongside any infra change). | — | — |
| S4-2 | **Real-world gap — evidenced, nuanced** | The offline/PWA subsystem (`frontend/src/lib/offline/`, 11 files: `db.js`, `queue.js`, `mutationEnvelope.js`, `tryOnlineThenQueue.js`, `registerSW.js`, `sw-client.js`, `useOfflineStatus.js`, `useOfflineSimple.js`, `useOffline.js`, `queueSignal.js`, `migrateLegacyQueue.js`) is a real, reasonably mature-looking implementation — `tryOnlineThenQueue.js` correctly enqueues mutations on network failure or 5xx responses with idempotency metadata, and there are working native-`online`-event listeners (`useOfflineStatus.js`, `useOfflineSimple.js`) plus a `flushNow()` export in `queue.js` for reconnect-triggered retry. **However**, `registerSW.js` — the file responsible for registering the actual Service Worker — opens with a hardcoded early return: `// TEMPORARILY DISABLED TO DEBUG RELOAD LOOP` followed by a bare `return;` before any service-worker registration code runs. This means: (a) there is **no offline-cached app shell**, so a page reload or fresh load while offline will not work at all — the SPA itself needs network connectivity to boot; (b) there is **no true background sync** when the browser tab is closed or backgrounded — queued mutations can only flush while a tab is open and reacts to the native `online` event. The **queue-while-tab-is-open, flush-on-reconnect** behavior is likely still functional; the **survive-a-reload-while-offline / true background sync** behavior is not. For a platform whose value proposition includes running a POS reliably (register uptime during connectivity blips is a common real-world requirement, especially for retail/restaurant verticals), this is a meaningful gap between what the code architecture suggests ("we built offline support") and what is actually active in production today. Recommend either re-enabling the service worker (the disabling comment suggests this was meant to be temporary) with the reload-loop bug actually fixed, or updating documentation/marketing claims to describe offline support accurately as "mutation queueing with reconnect flush" rather than implying full offline-first PWA behavior. | Medium–High | Medium (needs the original reload-loop bug diagnosed and fixed, not just removing the early return) |
| S4-3 | Missing (doc gap, matches `DOCS-INDEX-AND-GAPS.md`) | Confirming the pre-existing gap tracker's own note: there is **no OAO chapter** documenting the offline/PWA system at all — not its queueing model, not its (currently disabled) service worker, not the idempotency/mutation-envelope design. Given the real-world importance of offline resilience for a POS product (and given it's currently in a materially different state than a casual reading of the file list would suggest, per S4-2), this deserves its own short section — ideally in `04-FRONTEND-AND-PUBLIC-SITE.md` or a dedicated appendix — that documents both the intended design and the current disabled-SW status honestly. | Low (doc), but tracks a Medium-severity code gap already counted in S4-2 | Small |
| S4-4 | Accurate (spot-checked) | The two-app split (`frontend` for dashboard/POS/admin, `public-site` for marketing/signup/login/employee-portal) and their respective API integration patterns (`frontend/src/api/client.js` with `X-Tenant-Slug`/`X-Org-Id` headers; `public-site/src/config/api.js`'s `getApiUrl()` same-origin logic) match what earlier stages already independently observed (e.g. `frontend/src/api/inventory.js`'s use of the shared `api` client, matching this doc's description). No drift found here. | — | — |

### Stage 4 conclusion

The frontend documentation itself is honest and largely accurate, including about the dev-server-in-production architecture (S4-1) — this OAO chapter does not oversell. The one substantive finding is a real, code-confirmed gap between the *apparent* sophistication of the offline/PWA subsystem and its *actual* disabled state (S4-2/S4-3): the service worker that would deliver the strongest part of an "offline-tolerant POS" story is turned off with a debugging comment, and nothing in the documentation set currently reflects either the intended design or its current limited state.

**Claims register status after Stage 4:** C1 ("any device, any scale") — the offline-resilience angle specifically is **partially unverified**; queueing works, true offline page-load/background-sync does not, as of this audit. Carried to Stage 6 (POS) since offline behavior is most operationally relevant there.

---

---

## Stage 5 — Invoicing, Payments & Crypto

**Docs read:** `05-INVOICING-SYSTEM.md`, `07-CRYPTOCURRENCY.md`.

**Code cross-checked:** `backend/server.js` payment/transaction route mounts, `backend/routes/simple-transactions.routes.js`, `backend/routes/unified-transactions.routes.js`, `backend/routes/payments.routes.js` (the "legacy" router), live `backend/.env` flag values, `backend/routes/webhooks.doge.routes.js`.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S5-1 | Accurate | The two alternate transaction-listing surfaces referenced in the platform coverage map (`/api/simple`, `/api/unified`) are both **read-only** (`simple-transactions.routes.js` and `unified-transactions.routes.js` each expose only `GET` routes, confirmed by direct grep). They aggregate/display data from underlying sources rather than creating new money-movement records, so they do **not** contradict the "invoicing is the single source of truth" claim (C4) — they are reporting views, not competing write paths. This is a legitimate stress-test of C4 that the claim survives. | — | — |
| S5-2 | **Risk / Drifted — confirmed live** | The "legacy third-party processors" payments router (`backend/routes/payments.routes.js`, mounted at `/api/payments`) is described in code comments as **"gated"** behind `ENABLE_LEGACY_3P`, and the gating mechanism itself is implemented correctly (`if (String(process.env.ENABLE_LEGACY_3P).toLowerCase() === '1' ...) { app.use(...) }`). However, I confirmed by direct grep that **`ENABLE_LEGACY_3P=1` is actually set in `backend/.env` on this live server** — meaning this router **is live in production today**, not a dormant/disabled path as "gated" might imply to someone who only reads the comment. This router integrates with **real Stripe Terminal and Coinbase Commerce provider code** (`payments/providers/stripeTerminal.js`, `payments/providers/coinbaseCommerce.js`) and writes payment records to a **separate `payments` table** in the tenant main DB via raw `INSERT INTO payments (...)`, entirely independent of the `Invoices`/`InvoiceLineItems` tables that Core Invoicing uses. This is a genuine parallel money-movement path that would directly contradict claim C4 ("every sale... flows through Core Invoicing... single source of truth") **if it is actually invoked**. | High (contingent on S5-3) | — |
| S5-3 | Real-world gap / hygiene (nuance to S5-2) | I searched the frontend (`frontend/src`) for any call site targeting this legacy router's actual endpoints (`/api/payments/intents`, `/api/payments/refunds`, `/api/payments/stripe/*`, `/api/payments/coinbase/*`) and found **none** — the only frontend matches for the substring "payments" were auto-generated API catalog entries and an admin nav link, not real calls to these specific paths. This changes the character of S5-2's finding: rather than "two competing payment paths actively used interchangeably by real transactions," the more accurate characterization is **"a legacy Stripe/Coinbase integration is live, flag-enabled, and reachable in production, but appears to be dead code from the current frontend's perspective."** This is still worth fixing — reachable, provider-wired, payment-processing code that writes outside the invoicing backbone is exactly the kind of thing that becomes a real incident if ever called again (accidentally, by a stale client, by a future integration, or by someone probing the API directly), since any payment it processes would not appear in the platform's actual invoice/reporting/audit trail. Recommend: either set `ENABLE_LEGACY_3P` to unset/`0` in `backend/.env` if this path is truly retired, or if it needs to stay available for a specific reason, wire its successful payment/capture/refund handlers to also create or update a corresponding `Invoices` row so it cannot silently diverge from the "single source of truth" claim. | Medium (with a clear path to Low once resolved) | Small (flag flip) to Medium (if wiring into invoicing is chosen instead) |
| S5-4 | Accurate (partial trace, consistent) | The DOGE webhook flow (`backend/routes/webhooks.doge.routes.js`) matches the doc's description at the level I traced: it reconciles incoming wallet transactions against a `CryptoInvoice` record (via `postCryptoInvoiceLedger`) rather than writing to `Invoices` directly — consistent with `05`'s own documented schema, which lists `cryptoInvoiceId`, `cryptoAddress`, `cryptoAmount` as columns *on* the `Invoices` table specifically to link a crypto payment back to its invoice. I did not re-trace the full webhook-to-cart-finalize call chain in this pass (already partially covered by `docs/HOW-IT-ACTUALLY-WORKS.md` §14/§16 per the existing gap tracker), so this is a consistency check rather than a full independent re-verification. | — | — |
| S5-5 | Missing (pre-existing, reconfirmed) | Consistent with `docs/DOCS-INDEX-AND-GAPS.md`'s own note, a full traced flow for **Stripe / Coinbase / Coinremitter webhook → invoice update** does not exist anywhere in the doc set. Given S5-2/S5-3's finding that the Stripe/Coinbase code paths in the *legacy* router are live but apparently unused, and that `webhooks.stripe.routes.js` / `webhooks.coinbase.routes.js` are separately mounted before `express.json()` in `server.js` (correct pattern per S3-5) as presumably the *current*, non-legacy webhook handlers — there are now potentially **three** different Stripe/Coinbase-adjacent code paths in this codebase (the legacy router, the current webhook routes, and whatever `payments/api/v1/payments.routes.js` under `/api/v1/*` provides) with no single document tracing which one is authoritative today. Recommend this be the next concrete doc-writing task for the invoicing/payments area — it directly affects whether S5-2/S5-3's recommendation (retire vs. rewire the legacy router) can be made with confidence. | Medium | Medium (requires tracing three code paths, then writing the doc) |

### Stage 5 conclusion

The "invoicing is the backbone" claim (C4) holds up well against read-heavy stress-testing (S5-1) but has one concrete, live counter-example: a flag-enabled legacy payments router with real Stripe/Coinbase provider wiring that writes to a separate table outside the invoicing system entirely (S5-2). The mitigating factor is that it appears unused by the current frontend (S5-3) — this is a "clean up now while it's cheap" finding rather than an active breakage, but it is exactly the kind of thing that erodes a "single source of truth" claim over time if left alone, especially as more payment integrations get added without a canonical map of which path is current (S5-5).

**Claims register status after Stage 5:** C4 ("invoicing is the backbone... single source of truth") — **mostly true, with one identified live exception** (S5-2/S5-3) that should be resolved before this claim can be stated without qualification; C6 (non-custodial crypto) — not independently re-verified in this pass (would require confirming wallet key custody model in `dogeWallet.service.js`/`ltcClient.js`, deferred as lower-priority given time budget — flagged as an open item for a future audit pass if crypto becomes a larger part of the business).

---

---

## Stage 6 — POS Systems & KDS

**Docs read:** `06-POS-SYSTEMS.md`, `08-KDS-AND-KITCHEN.md` (Appendix B referenced for register/shift detail, not separately re-verified line-by-line given prior stages already touched adjacent code).

**Code cross-checked:** `frontend/src/pages/pos/KitchenDisplay.jsx`, `frontend/src/pages/pos/ModernPOS.jsx` (`canTransactOnRegister`, `blockIfRegisterNotReadyForSale`), `bridge-win-csharp/PAWSPOS.Bridge/Services/KDSService.cs`. Recipe/consumption service behavior (`backend/services/recipeInventory.service.js`, `backend/services/consumption.service.js`) already reviewed earlier in this working session for an unrelated reports task — cited here as corroborating evidence rather than re-read in full.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S6-1 | Accurate | The "Fire Ticket" bridge action described in `08-KDS-AND-KITCHEN.md` is real: `KDSService.FireTicketAsync` exists in the C# bridge (`bridge-win-csharp/PAWSPOS.Bridge/Services/KDSService.cs`), matching the doc's claim precisely. Recipe/consumption logic (ingredient deduction on sale, optional-ingredient-out-of-stock skipping, waste recording) reviewed earlier this session in `recipeInventory.service.js` is real, functioning code consistent with `08`'s description of the recipe system. | — | — |
| S6-2 | Drifted (minor precision) | `08`'s claim that the Kitchen Display "Updates via WebSocket or polling" is technically not wrong (the word "or" covers both), but in the actual code (`KitchenDisplay.jsx`) the mechanism is **polling only** — a `setInterval` fetching tickets every `pollMs` (default 3000ms, configurable, minimum clamped to 1500ms), plus a separate, less frequent device-status poll every 10 seconds. There is no WebSocket path in this component. This means kitchen staff can see up to ~3 seconds of latency (default) before a fired ticket appears, which is a reasonable real-world number but is worth stating precisely rather than leaving as an ambiguous "or," especially since a reader might reasonably assume real-time push behavior is available and enabled. | Low | Small (doc precision only) |
| S6-3 | Real-world gap (hypothesis, consistent with Stage 0/7 pattern) | The register/shift ceremony (`canTransactOnRegister`, `blockIfRegisterNotReadyForSale` in `ModernPOS.jsx`) is **hardcoded logic with no tenant-level or plan-level toggle** found in the frontend to simplify or skip it — every tenant using Modern POS goes through the same open-register → PIN-authenticate → shift-ownership-matching flow, regardless of whether they are a multi-cashier restaurant (where shift accountability matters a great deal) or a single-operator business like the farm-stand vertical the platform explicitly documents (`29-SMALL-FARM-PRODUCER.md`). This is the same *category* of gap already confirmed once in this platform (the inventory unit-of-measure issue) and flagged conceptually in Stage 0 (S0-3, S0-4): a feature that is well-suited to one ICP segment but adds ceremony/friction for a smaller or simpler one, with no documented or code-level way to opt out. **This is a hypothesis, not a confirmed complaint** — I did not find evidence either way of whether small/simple tenants have actually struggled with this in practice, and it is plausible register/shift discipline is valuable even for a solo operator (cash accountability). Flagging for Stage 7 (inventory) and Stage 12 (synthesis) to consider alongside other ICP-fit findings, and worth a real conversation with an actual farm-stand or single-operator tenant before treating it as a "must-build" simplification per the Section 3 gap-tagging framework in `28`. | Low–Medium (needs real user validation before prioritizing) | Medium (if a "simple mode" toggle were built) |
| S6-4 | Cross-reference (not re-litigated) | `06`'s "Offline Support" section describes the mutation queue (`tryOnlineThenQueue`, `queue.js`) without mentioning the disabled service worker found in Stage 4 (S4-2). POS is the single most operationally relevant place for this gap to be documented honestly, since register uptime during a connectivity blip is precisely the scenario offline support is meant to cover. Recommend Stage 12 include updating this specific section (not just the general frontend chapter) when S4-2/S4-3 are addressed. | Low (doc), tracks Medium code gap already counted in S4-2 | Small |

### Stage 6 conclusion

POS and KDS documentation is accurate about what exists and the underlying bridge/recipe/consumption machinery is real and functioning, corroborated by both fresh code review and this session's earlier, independent work on reports/inventory. The most interesting finding here is not a bug but a **pattern repetition**: the same "built for a fuller-service business, potentially heavy for a lightweight one" shape that showed up in inventory (S0-4) shows up again in the register/shift ceremony (S6-3) — worth treating as a genuine design question for the ICP conversation in Stage 12 rather than a one-off inventory quirk.

**Claims register status after Stage 6:** No claims fully resolved or contradicted this stage; C1 ("any business type... any scale") gets a second, independent data point suggesting the platform's default flows may skew toward multi-staff/full-service operations more than the marketing claim's breadth implies — tracked to Stage 12 alongside S0-3/S0-4/S6-3.

---

---

## Stage 7 — Inventory

**Docs read:** `09-INVENTORY.md`.

**Code cross-checked:** `backend/models/inventoryItem.model.js` schema fields, cross-referenced against actual frontend usage of `weightPerUnit`, `volumePerUnit`, `isConsumable` (grep across `frontend/src`), and against the `unitOfMeasure`/`priceMode` fields introduced by this session's earlier `ItemWizardSimple` rework (documented in this same conversation prior to the formal audit beginning).

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S7-1 | Confirms Stage 0 hypothesis | `09-INVENTORY.md` **already documented** a variation schema including `unit`, `priceMode` (`PER_ITEM \| PER_WEIGHT \| PER_VOLUME`), `weightPerUnit`, and `volumePerUnit` fields **before** this session's `ItemWizardSimple` rework happened. This confirms the earlier finding (S0-4) precisely: the platform's *own documented data model* already anticipated unit-aware, non-retail-friendly inventory (weight/volume pricing) — the gap was specifically in the **default create UI** not exposing it, not in an absent architectural concept. This is good news for the platform's underlying design and a useful, concrete example for Stage 12: when this audit says "real-world gap," it more often means "capable backend/schema, thin default frontend" than "missing concept entirely" — a distinction worth stating plainly in any synthesis. | — | — |
| S7-2 | **Missing — new finding, extends S7-1** | Two of the exact fields `09` documents as part of the variation schema — **`weightPerUnit`** and **`volumePerUnit`** — have **zero references anywhere in `frontend/src`** (confirmed by grep). They exist in the backend model and are described in the docs, but there is currently no UI anywhere (not `ItemWizardSimple`, not `ItemWizard.jsx`, not `VariationManager.jsx`, not `ItemDetailModal.jsx`) that reads or writes them. Similarly, **`isConsumable`** ("KDS routing" per `09`) is real and actively used across multiple **backend** files (`pos.routes.js`, `recipeInventory.service.js`, `recipes.routes.js`, the model itself), but also has **zero frontend references** — meaning there is no admin-facing toggle for a business owner to mark an inventory item as "this is a kitchen-consumable ingredient that should route to KDS/recipe consumption logic." Whoever sets this flag today must do so outside the normal admin UI (direct DB access, a script, or an as-yet-untraced implicit path via recipe creation). This is the same *shape* of gap as the original unit-of-measure issue — a real, working backend capability with no way for an actual tenant to use it through the product — but on two different fields, in two different areas (weighted/volume-based selling precision, and KDS ingredient routing). Recommend prioritizing whichever has more real demand: `isConsumable` likely matters more near-term (any tenant using recipes/KDS ingredient tracking needs this), while `weightPerUnit`/`volumePerUnit` matter most for genuinely weight-driven verticals (butcher, deli, bulk goods, farm produce sold by the pound) that this session's `ItemWizardSimple` fix only partially addresses (see S7-3). | Medium | Medium (two separate small UI additions, not a redesign) |
| S7-3 | Self-critical / Missing (own work product) | This session's earlier `ItemWizardSimple` rework introduced a **new** pair of fields — `unitOfMeasure` (a free-text-ish display label like `"dozen"`, `"lb"`, `"jar"`) and `priceMode` (`PER_UNIT` / `PER_WEIGHT`) — stored in the variation JSON. These are a genuine improvement (they solved the immediate "farm can't express dozens/jars" problem) but they are **not the same concept** as the pre-existing, documented `weightPerUnit`/`volumePerUnit` fields from S7-2, which represent a *numeric conversion factor* (e.g. "this selling unit equals 0.5 kg") rather than a display label. Result: the variation schema now has **two, not-fully-reconciled layers of "unit" thinking** — an older, documented-but-unused numeric-conversion design, and a newer, now-wired display-label design introduced to unblock a real user need quickly. This is worth being transparent about rather than treating the earlier fix as architecturally final: a future pass should decide whether `weightPerUnit`/`volumePerUnit` should be revived as the numeric backing for true weight-based checkout math (e.g. a scale-integrated POS multiplying price-per-lb by an actual measured weight) with `unitOfMeasure` staying as the human-readable label layered on top, or whether one of the two designs should be formally retired from the schema/docs to avoid the exact "fragmentation across create flows" problem this whole inventory conversation started by diagnosing (see the Grok/Cursor discussion earlier in this session about `ItemWizardSimple` vs `ItemWizard.jsx` vs `CreateItemDrawer` fragmentation — this is the same failure mode recurring at the *field* level instead of the *component* level). | Medium | Medium (a design decision + migration, not urgent, but should not be left indefinitely) |
| S7-4 | Accurate | Everything else in `09-INVENTORY.md` — route file names, ledger types (RECEIVE, ADJUST, TRANSFER, BREAK_CASE, KIT_BUILD_IN/OUT), the barcode-scan-to-lookup flow, and the frontend page inventory (`ItemsPage.jsx`, `StockOverview.jsx`, `PurchaseOrders.jsx`, `Vendors.jsx`, `ItemWizardSimple.jsx`, `ItemDetailModal.jsx`) — matches what earlier stages and this session's prior inventory work already independently confirmed exists and functions as described. | — | — |

### Stage 7 conclusion

The inventory documentation was, if anything, ahead of the default UI rather than behind it — the schema already promised unit- and weight-aware variations before the default wizard exposed any of that to a real user, which is a healthier finding than "the docs oversold a feature that doesn't exist in code at all." The actionable takeaway is narrower and more concrete than a generic "inventory needs more real-world completeness" conclusion: two specific, already-modeled fields (`weightPerUnit`/`volumePerUnit`, and `isConsumable`) need a UI, and this session's own fix for the unit-of-measure problem should be revisited once, deliberately, to decide how it relates to those pre-existing fields rather than becoming a third parallel "unit" concept sitting alongside them.

**Claims register status after Stage 7:** C1 ("any business type") — inventory's *backend* substantiates this claim more than its *default frontend* currently does, now with two additional concrete examples (S7-2) beyond the original unit-of-measure case; this is the strongest single data point for Stage 12's synthesis recommendation about where to invest next.

---

---

## Stage 8 — HR/Payroll & Business Analytics

**Docs read:** `11-HR-AND-PAYROLL.md`, `12-BUSINESS-ANALYTICS.md`.

**Code cross-checked:** `backend/server.js` mount lines for `/api/hr`, `/api/payroll`, `/api/tenant-time`, `/api/staff-personal`; `backend/routes/hr.routes.js`, `backend/routes/tenantTime.routes.js`, `backend/routes/staffPersonal.routes.js`; `backend/middleware/mysqlSessionAuth.js`; `backend/controllers/reports.sales.controller.js` DB connection calls; a grep sweep for real `'analytics'`-typed DB connections across backend services.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S8-1 | **Accurate — verified after deeper trace (resolves an S3-6 concern)** | At first glance, `app.use('/api/hr', hrRouter)` in `server.js` (line 748) has **no auth middleware in the mount call**, which — per the S3-6 pattern flagged in Stage 3 — could look alarming to a reviewer scanning `server.js` alone, especially for the most compliance-sensitive data domain in the platform (C8). I traced `hr.routes.js` directly and confirmed it applies `router.use(requireSessionAuth); router.use(tenantResolver); router.use(checkFeatureAccess('staff_hr'));` **internally**, before any of its child routers (`employees`, `payroll`, `time-off`, `benefits`, `departments`, `reports`) are mounted. Same result for `tenantTime.routes.js` and `staffPersonal.routes.js` — both apply `requireSessionAuth`/`tenantResolver` internally despite no middleware showing in their `server.js` mount lines. **HR access is properly gated in all three cases checked.** This is a good example of why S3-6's recommendation (document the "some routers self-manage auth" convention explicitly) matters: the *pattern* is fine, but it is not verifiable by glancing at `server.js` alone, and a less thorough audit could have wrongly flagged this as an open HR data-access vulnerability. | — (verified safe) | — |
| S8-2 | Accurate (clears a hypothesis raised while investigating S8-1) | While tracing S8-1, `tenantTime.routes.js` and `staffPersonal.routes.js` were both found importing `requireSessionAuth` from `../middleware/mysqlSessionAuth.js` rather than `../middleware/sessionAuth.js` directly, raising a concern that the codebase might have **two independent, potentially-divergent session/auth implementations** (which would mean a future security fix like S3-1/S3-2 might only get applied to one of them). Reading `mysqlSessionAuth.js` resolved this cleanly: it is a **backward-compatibility re-export shim** — `export { requireSessionAuth, ... } from './sessionAuth.js';` — with no independent logic of its own. There is genuinely only one session implementation. This means the Stage 3 fixes (S3-1 `SESSION_SECRET`, S3-2 cookie `secure` flag) will automatically apply everywhere, regardless of which import path a given route file uses — one fix, not two. | — (verified, simplifies remediation) | — |
| S8-3 | Real-world gap / Missing (doc precision) | `12-BUSINESS-ANALYTICS.md` states report/dashboard data lives in "**dev-analytics-{slug} and tenant main DB**," which is technically true in aggregate but obscures an important architectural detail: I confirmed `backend/controllers/reports.sales.controller.js` (backing `/api/reports`, the sales-summary/trends/team-sales endpoints referenced throughout this platform's reporting UI) queries **exclusively the tenant main DB** (`getTenantConnection(tenantSlug, 'main')`, confirmed at every query site in the file) — it never touches the analytics DB. Meanwhile, `salesAnalytics.service.js`, `posAnalytics.service.js`, and `customerAnalytics.service.js` **do** use analytics-typed connections (confirmed via grep for `DATABASE_TYPES.ANALYTICS`/`'analytics'`-typed calls). This means there are **two architecturally distinct reporting code paths** — one computing sales figures live against operational data in the main DB, another drawing from the separate analytics DB — covering overlapping ground (both are "sales analytics" in the business sense) without any documentation of how, or whether, they reconcile. If the analytics DB is populated by a batch/aggregation job that runs on a delay, it is plausible (not confirmed in this pass) for a dashboard widget backed by `salesAnalytics.service.js` to show different numbers than a report backed by `reports.sales.controller.js` for the same time window — a serious real-world problem for a "business analytics" product if it happens, since an owner comparing two screens expecting the same underlying sales total would lose trust in the numbers. Recommend: (a) document explicitly which reporting surfaces read from which DB, and (b) verify with an actual side-by-side comparison (e.g., today's sales total on the main dashboard vs. the same total on a `/reports` page) whether these two paths currently agree. | Medium (contingent on whether the two paths actually diverge in practice — not yet tested) | Small (documentation) to Medium (if reconciliation logic is needed) |
| S8-4 | Accurate (confirms a good, if unusual, design choice) | `03-BACKEND-ARCHITECTURE.md`'s note that `audit.service.js` writes `audit_logs` to the **analytics DB** (not the main DB) was corroborated again here via the same grep sweep that surfaced `salesAnalytics`/`posAnalytics`/`customerAnalytics`/`audit.service.js` all using analytics-typed connections. This is an unusual but not unreasonable architectural choice (keeping high-volume, less-transactional audit/analytics writes off the primary operational database) — flagged here only so Stage 11's cross-cutting security review knows to check audit log access controls specifically in the **analytics** DB connection path, not assume it lives alongside `Invoices`/`Staff` in main. | — | — |
| S8-5 | Not independently re-verified (time-budget note) | The HR schema reference in `11` (`business_tax_info`, `employee_w4`, `employee_compensation`, `pay_periods`, `payroll_runs`, `payroll_line_items`, `time_off_requests`, `pto_balances`, `departments`, `benefit_plans`) was **not** independently re-verified table-by-table against `backend/schemas/hr-schema.sql` in this pass, given the time budget for this stage was spent on the higher-value auth-boundary question (S8-1/S8-2) and the reporting-path discrepancy (S8-3). Flagging as an explicit open item rather than silently skipping it — recommend a future targeted pass if HR/payroll accuracy becomes a specific concern (e.g. before a real payroll run for a paying customer). | — (explicitly deferred, not claimed complete) | Small (a follow-up read-through) |

### Stage 8 conclusion

This stage produced a healthy mix of "looked concerning, verified safe" (S8-1, S8-2 — genuinely good news about HR data access and session-code duplication) and "looks fine at a glance, has a real architectural question underneath" (S8-3 — two reporting code paths with no documented reconciliation story). Compliance-sensitive HR access held up under direct scrutiny, which is an important, positive data point for claim C8. The reporting-path duplication is the more actionable finding and should be resolved with a straightforward side-by-side data comparison rather than more code reading.

**Claims register status after Stage 8:** C8 ("separate HR DB per tenant for compliance") — **the access-control half is confirmed accurate** (S8-1); C9 ("audit logging for key actions") — location confirmed (analytics DB) but coverage completeness deferred to Stage 11 as originally planned.

---

---

## Stage 9 — Tickets, Appointments, Floor Design, Gift Cards, Loyalty, Storefront, Audit

**Docs read:** `13-TICKETS-AND-APPOINTMENTS.md`, `15-OTHER-SYSTEMS.md`, `20-AUDIT.md` (Floor Design/`10-FLOOR-DESIGN-AND-SEATING.md` not separately re-read this pass given time budget — its content was already indirectly touched via `08-KDS-AND-KITCHEN.md`'s seating-layout mentions in Stage 6).

**Code cross-checked:** `backend/constants/auditTaxonomy.js` (existence confirmed), `backend/routes/storefront.routes.js` (full route inventory), a targeted backend-wide search for any storefront checkout/order-creation implementation.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S9-1 | Accurate | `13-TICKETS-AND-APPOINTMENTS.md`'s route and service inventory matches real files (`tickets.routes.js`, `admin.tickets.routes.js`, `ticketWorkflow.service.js`, `ticketSLA.service.js`, `ticketCounter.model.js` for queue numbers). No drift found; this area was already partially traced end-to-end in `docs/HOW-IT-ACTUALLY-WORKS.md` §21 per the pre-existing gap tracker, and nothing in this pass contradicts that. | — | — |
| S9-2 | Accurate | `20-AUDIT.md` is one of the most precise, verifiable chapters in the entire doc set: its route table, canonical taxonomy source (`backend/constants/auditTaxonomy.js`, confirmed to exist), and scope-based auth model (`requireScopes` with `audit:read`/`audit:write`) all check out. This chapter is a good model for what the rest of the OAO documentation should look like — specific file paths, specific route methods, specific field names, no vague hand-waving. | — | — |
| S9-3 | Missing (nuance worth double-checking) | `20-AUDIT.md` states general `audit_logs` lives in the tenant **analytics** DB, but HR-specific audit events go to a **separate `hr_audit_log` table in the tenant main DB** — a third storage location for audit-adjacent data (alongside the analytics DB's `audit_logs` and the HR DB's actual HR records). This is disclosed plainly in the doc, so it is not a drift, but it is worth a deliberate Stage 11 check: does keeping HR audit events in the **main** DB (rather than the more strictly-scoped HR DB) create any unintended visibility of HR-adjacent audit trail data (e.g., "employee X's pay was adjusted") to staff who have main-DB access but not HR-DB access? This is a reasonable architectural choice (main DB may need HR audit visibility for managers without full HR data access) but deserves an explicit access-control check rather than being assumed fine because it's documented. | Low–Medium (pending Stage 11 check) | — |
| S9-4 | **Corrected finding — see revision below.** Initial pass read `backend/routes/storefront.routes.js` in isolation, found only config/menu/cart-item-delete endpoints and no dedicated checkout route, and provisionally flagged storefront checkout as possibly missing entirely. **Following up in the frontend before finalizing this stage disproved that**: `frontend/src/pages/store/Storefront.jsx` creates its cart via `pos.createCart({...})` and finalizes via `posApi.finalize(cart.id)` — i.e. it **reuses the same POS cart/finalize infrastructure** already independently verified in Stage 5/6 to flow through `CoreInvoicingService.createPOSSaleInvoice` and write to `Invoices`/`InvoiceLineItems`. **Storefront checkout does flow through the invoicing backbone; it simply does not have its own dedicated route because it deliberately reuses the POS cart API instead of duplicating it** — a reasonable, arguably good architectural choice, not a gap. I'm leaving the original reasoning visible here rather than deleting it, because it's a useful example of why this audit's methodology insists on tracing the *actual call chain* (frontend → backend) rather than concluding from a single route file's contents alone. | — (corrected; see S9-4b) | — |
| S9-4b | Real-world gap (small, precisely scoped — replaces the overstated S9-4) | The one genuine, narrower gap that survives the correction above: `storefront.routes.js`'s **`allowGuestCheckout`** config flag is stored and returned by the config endpoints, but I found **no reference to it anywhere in `backend/routes/pos.routes.js`** (the actual cart-creation/finalize logic the storefront frontend calls into). This suggests the toggle may only affect **frontend** behavior (e.g. whether the storefront UI prompts for account creation before showing the checkout button) rather than being enforced server-side — meaning if a determined user called the same cart-creation API directly, guest checkout could effectively always be possible regardless of the tenant's configured preference. This is a minor, plausible gap (not confirmed as exploited or exploitable without further tracing of whatever frontend-side gating exists) rather than a fundamental architecture problem. Recommend a quick, targeted check: does `pos.routes.js` or a middleware in its chain actually read `storefront_config.allowGuestCheckout` before allowing an anonymous cart to be created/finalized, and if not, either add that server-side check or update the setting's description to clarify it is a UI preference only. | Low–Medium | Small (a config check, or a documentation clarification) |
| S9-5 | Not independently re-verified (time-budget note) | Gift cards and loyalty (`giftcards.routes.js`, `giftCard.service.js`, `loyalty.routes.js`, `loyaltyPoints.service.js`) were **not** independently re-traced end-to-end in this pass beyond confirming the doc's file-name inventory looks plausible against the broader route list already seen in Stage 3's `03-BACKEND-ARCHITECTURE.md` review. Flagging explicitly as deferred rather than silently assumed accurate — worth a focused pass if gift cards/loyalty become a specific area of concern (e.g. before a tenant runs a real promotional loyalty campaign). | — (explicitly deferred) | Small (a follow-up read-through) |

### Stage 9 conclusion

Tickets, appointments, and audit logging all held up well under direct code verification — the audit chapter in particular is a model example of accurate, specific documentation. Storefront checkout initially looked like it might be a serious, undocumented product gap (S9-4's first pass) but a frontend trace corrected that: it deliberately reuses the already-verified POS cart/finalize path rather than duplicating it, which is good design, not a gap. The one real, much smaller finding that survived (S9-4b) is a plausible client-side-only enforcement of the `allowGuestCheckout` setting. The most valuable takeaway from this stage is methodological: the initial storefront finding is a good demonstration of why this audit insists on tracing frontend-to-backend call chains before concluding something is missing, and why findings should be revised the moment better evidence arrives rather than left standing for the sake of consistency.

**Claims register status after Stage 9:** C1 ("any business type... any payment type") — the storefront/e-commerce sub-claim is **confirmed accurate** once the actual call chain was traced (reuses the verified POS/invoicing path); C9 (audit logging) — **confirmed strong** based on direct verification (S9-2).

---

---

## Stage 10 — Bridge/Devices, Signage, Barcode/UPC Catalog Service

**Docs read:** `14-BRIDGE-AND-DEVICES.md`, `26-signage.html` (in full — one of the most detailed, self-aware chapters in the doc set), `index.html`'s "Platform Product Catalog (Barcode)" section (re-confirmed from Stage 0).

**Code cross-checked:** `backend/routes/signage.routes.js` (media handling), `backend/routes/platformCatalog.routes.js` + its `server.js` mount line and static-file registration, `backend/routes/devices.routes.js` (device key handling), `backend/routes/deviceCodes.routes.js`.

### Findings

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S10-1 | Accurate | `26-signage.html` is exceptionally well-documented: REST API table, exact tenant DB table names and columns (`SignageFolders`, `SignageAds`, `SignageDisplays`), a real architecture diagram, a real sequence diagram for catalog-save-to-bridge-to-player, explicit resilience behavior ("if this breaks..."), and — notably — an honest **"Roadmap (not in current slice)"** section admitting daypart schedules, calendar campaigns, device heartbeats, and fine-grained RBAC are not built yet. This chapter, alongside `20-AUDIT.md` (S9-2), is one of the two best examples of documentation-as-source-of-truth in this entire platform and should be used as the template when improving weaker chapters in a future pass. | — | — |
| S10-2 | Accurate / Good news | Signage ad creatives (`SignageAds.media_url`) are **free-text URL fields** with no file-upload/`multer` handling in `signage.routes.js` — admins paste in already-hosted media URLs rather than uploading through this platform's own storage pipeline. This means signage is **not** affected by the Stage 1 Nginx `/media/` bug (S1-1); that bug is scoped specifically to the platform barcode/UPC catalog's own image uploads (`/mnt/vfs/platform-catalog/`), which do go through the platform's own upload/serving pipeline. Worth stating explicitly so this doesn't get conflated into a broader "all media serving is broken" conclusion during Stage 12 synthesis — it isn't. | — | — |
| S10-3 | Refines S1-1 (root cause now fully pinned down) | Confirmed the Express side of the barcode/UPC catalog image pipeline is **correctly implemented and registered**: `backend/server.js` calls `ensurePlatformCatalogImageDir()` and registers `express.static('/media/platform-catalog', ...)` (line 473) pointing at the real upload directory, and `/api/platform-catalog` is properly mounted with `requireSessionAuth, tenantResolver` (line 644), matching the doc's "session + tenant" claim (C17) exactly. This confirms with full confidence that **S1-1's root cause is purely the reverse-proxy layer** (the live Nginx config missing the `/media/` location block) — the backend has never been the problem, and the fix genuinely is "copy the corrected Nginx config and reload," nothing more. Good, precise closure of that earlier finding. | — (confirms S1-1's fix is small and sufficient) | — |
| S10-4 | Risk — extends S2-4 and S3-3 | `devices.routes.js` contains the same manual-SQL-escaping pattern already flagged in S2-4 (e.g. a raw `WHERE device_key = '${deviceKey.replace(/'/g, "''")}'` string interpolation) — adding an eighth file to that remediation list. More importantly for the still-open Stage 3 device-authentication question (S3-3): `device_key` in `devices.routes.js` functions as a **plain lookup identifier** used to find a device row, not as a demonstrated cryptographic secret (no hashing, no comparison-with-stored-hash pattern was found, unlike `Staff.posPinHash` which correctly uses `bcrypt.compare` per Appendix D). I did not find conclusive evidence either confirming or fully allaying the concern that knowing a `device_key` (which may be visible in admin UI screens, pairing/QR flows, or logs) could be sufficient to act as that device via the `x-device-id` header `deviceAuth.js` accepts unchecked. This is not a new fact so much as **corroborating evidence in the same direction** as S3-3's original concern — recommend Stage 11 treat "is `device_key`/`x-device-id` a real credential or just a label" as the single most important open security question to resolve definitively, since it touches kiosk/mobile checkout (real money movement) directly. | High (carried from S3-3, now with supporting evidence) | Medium (needs a definitive trace, then likely a real fix — e.g. requiring a signed device token instead of a bare ID) |
| S10-5 | Accurate (spot-checked) | `14-BRIDGE-AND-DEVICES.md`'s WebSocket path, action list (`getStatus`, `cryptoRpc`, `print`, `deviceNotification`, `fireTicket`, `getWallets`), and the C# bridge's `FireTicketAsync` (independently confirmed in Stage 6, S6-1) are consistent and real. `deviceCodes.routes.js` was found to be a very thin file (a single `POST /batch` route) — consistent with the doc's framing of it as "device claim" tooling for **batch-generating** codes rather than the full claim-redemption logic, which likely lives elsewhere (e.g. inside `devices.routes.js` itself). Not independently traced further given time budget; flagged as an open item alongside S10-4 rather than assumed resolved. | — (mostly confirmed, one sub-thread deferred) | — |

### Stage 10 conclusion

This stage delivered the audit's most positive documentation finding (S10-1, S10-2 — signage is a model chapter and is unaffected by the earlier media bug) alongside a **precise, final closure** of exactly how small the Stage 1 media-serving fix actually is (S10-3). It also **compounds** rather than resolves the audit's most safety-relevant open question: whether device/kiosk authentication (S3-3, now S10-4) is a real credential-based security boundary or effectively a self-reported label — this should be the very first item addressed in Stage 11's cross-cutting security pass, given it now has evidence from two independent stages pointing the same direction and directly touches real money movement (kiosk/mobile checkout).

**Claims register status after Stage 10:** C3 (bridge architecture) — **confirmed accurate**; C17 (barcode/UPC catalog) — **confirmed accurate and precisely scoped**, including exactly why it's currently broken in production (S1-1/S10-3) and exactly how small that fix is.

---

---

## Stage 11 — Cross-Cutting Security & Compliance Rollup

**Purpose:** Consolidate every security/isolation/compliance thread carried forward from Stages 1–10, resolve the ones that were left open pending further evidence, and present a single prioritized security picture rather than scattered findings.

**Additional code checked this stage:** `backend/middleware/deviceAuth.js` (full file, re-confirmed), `backend/routes/api/mobile/index.js` (confirms same weak middleware guards both kiosk and mobile).

### Resolved: device/kiosk/mobile authentication (was S3-3, evidenced further in S10-4 — now CONFIRMED)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-1 | **Risk — CONFIRMED, Critical** | `backend/middleware/deviceAuth.js`'s own header comment states it resolves device identity "from headers **or token**," but the actual implementation (the entire file is 19 lines) contains **no token validation of any kind** — it reads `x-device-id` and `x-surface` headers directly from the request and trusts them completely, with no lookup against a `Devices` table, no signature check, no comparison against a stored secret. I confirmed both `backend/routes/api/kiosk/index.js` **and** `backend/routes/api/mobile/index.js` mount this exact same middleware (`router.use(deviceAuth)`) as their only device-level protection, layered on top of `tenantResolver` (which itself can be satisfied via a `X-Tenant-Slug` header in the no-session branch). **Practical implication:** for kiosk and mobile checkout — both real, invoice-creating, money-movement paths per Stage 5/9's verification that they flow through the same Core Invoicing backbone — the "device" side of the security model is currently a **self-reported label, not a credential**. Anyone who can reach these endpoints and knows (or guesses) a tenant's slug and supplies an arbitrary `x-device-id` value can transact as if they were a legitimate kiosk or mobile device. This is the single highest-priority security finding of the entire audit: it is fully confirmed (not merely suspected, as in the original Stage 3 pass), it touches real payment/invoicing flows, and the fix is well-scoped (add real device-credential validation — e.g. a per-device signed token issued at claim/pairing time and checked in `deviceAuth.js` against a stored hash, mirroring the `bcrypt.compare` pattern already correctly used for `Staff.posPinHash`). Recommend this be the **first item fixed** out of this entire audit, ahead of even S3-1/S3-2, given it is the only finding that could plausibly allow an unauthorized party to create real transactions rather than "merely" weaken session/config hygiene. | **Critical** | Medium (requires designing and rolling out real device credentials, but the pattern to copy — bcrypt-hashed secret compared server-side — already exists elsewhere in the same codebase) |

### Consolidated: SQL injection-prone manual escaping (was S2-4, extended by S10-4)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-2 | **Risk — consolidated, High** | Across Stages 2 and 10, the manual `.replace(/'/g, "''")` SQL-escaping pattern (instead of parameterized/prepared-statement placeholders) was confirmed present in **at least eight** real backend route files: `staff.routes.js`, `devices.routes.js` (confirmed twice, S2-4 and S10-4), `inventory.items.routes.js` (confirmed directly with a full code quote in S2-4), `transactions.routes.js`, `customers.routes.js`, `admin.catalog.routes.js`, `admin.database.routes.js`. The same codebase already demonstrates the **correct** pattern elsewhere (parameterized `?` placeholders, e.g. in `inventory.items.routes.js`'s own SELECT queries), making this an inconsistency to fix rather than a fundamental architecture gap. Recommend a dedicated remediation pass — realistically doable as a mechanical find-and-replace-style fix per file, but each site should be tested individually since query structure varies. | **High** | Medium (8 files, mechanical but needs care + tests) |

### Consolidated: session/secrets configuration (was S3-1, S3-2, S1-4)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-3 | **Risk — consolidated, High** | Three related, all-confirmed-live findings should be fixed together as one "harden session and secrets" pass: (1) **S3-1** — no `SESSION_SECRET` set anywhere in this deployment, falling back to the hardcoded `'pawspos-secret-key-change-in-production'` default; (2) **S3-2** — the session cookie is `secure: false` despite the platform running behind valid, working HTTPS; (3) **S1-4** — `infrastructure/ecosystem.production.config.js` hardcodes `MYSQL_PASSWORD` and `SECRETS_KEY` in plaintext and both this file and `backend/.env` are world-readable (644) on disk. None of these require architectural change — they are configuration hardening — but all three should be fixed in the same maintenance window since fixing session security while leaving plaintext credentials world-readable (or vice versa) leaves an obviously-adjacent gap open. Recommended order: rotate `SESSION_SECRET` and `MYSQL_PASSWORD`/`SECRETS_KEY` together (this will require a maintenance window since rotating `SESSION_SECRET` invalidates all active sessions), flip `secure: true` for the cookie, tighten file permissions to `600`. | **High** | Small (all three are configuration changes, not code redesign) |

### Consolidated: logging hygiene (was S2-5, S3-4)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-4 | Risk — consolidated, Medium | Verbose, unstructured `console.log()` calls printing session contents, user emails, roles, org IDs, and raw headers were confirmed in `tenantResolver.js`, `sessionAuth.js`, and `flexibleAuth.js` — i.e. the entire authentication middleware stack, firing on every authenticated request. Combined with S1-4's file-permission finding (world-readable config/env files), this means plaintext logs likely contain user PII with no more access control than the log directory's own file permissions. Recommend converting all of these to the project's existing structured `logger` at `debug` level (a pattern already used correctly elsewhere in the codebase, e.g. `inventory.items.routes.js`), gated off by default in production. | Medium | Small–Medium (mechanical, spread across 3 files) |

### Tenant-isolation spot checks (final pass)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-5 | Accurate (reconfirmed) | The core tenant-isolation mechanism — separate physical MySQL databases per tenant (`dev-{slug}`, `dev-analytics-{slug}`, `dev-hr-{slug}`) rather than shared-schema row-level filtering — remains the strongest architectural fact of this entire audit (S2-1). No finding across any of the ten module stages contradicts this. Cross-tenant data leakage would require a bug in *tenant resolution* (which decides which database to connect to for a given request) rather than a missing `WHERE` clause within an already-connected tenant database — which reframes where isolation risk actually lives: **not** "did every query remember its org_id filter" but **"can tenant resolution ever be tricked into connecting to the wrong tenant's database."** | — | — |
| S11-6 | **Corrected, then fixed** | Verifying this turned up a necessary correction: the `tenantResolver.js` master-admin bypass (S2-3) is **not** actually how master admins browse or modify a *specific* other tenant's data — it only lets master admins use generic tenant-scoped routes (like `/api/inventory`) without an error, defaulting to their own platform org. The **real** cross-tenant mechanism is `backend/routes/masterAdmin.routes.js`, which is correctly role-gated (`router.use(requireRoles('master_admin'))` — I initially searched for a differently-named `requireMasterAdmin` helper and nearly flagged this as unprotected before a full read of the file caught the actual check) and reads/writes **other tenants'** data directly via `getTenantConnection(slug, ...)`. That file had **zero audit trail entries** for its three mutating tenant-management endpoints (`PUT /tenants/:id`, `PATCH /tenants/:id/archive`, `DELETE /tenants/:id`) — only plain application logs, not `audit_logs` rows visible in the Admin → Audit Logs UI. **Fixed**: added a best-effort `auditMasterAdminAction()` helper (never throws — a failed audit write does not block the underlying admin action) wired into all three handlers, writing to the *target* tenant's own analytics DB with new canonical event types (`master_admin_tenant_updated`, `master_admin_tenant_archived`, `master_admin_tenant_deleted`, added to `constants/auditTaxonomy.js` alongside a new `TENANT` entity type). Verified: backend restarts cleanly, master-admin routes remain correctly session+role gated. | Medium → **Fixed** | Small (confirmed) |
| S11-7 | **Resolved (narrow question), but surfaced a broader, more significant finding** | The original narrow question — can non-HR staff see `hr_audit_log` — is now answered: **no code anywhere in the backend reads (`SELECT ... FROM hr_audit_log`) that table at all.** It is currently write-only (populated by `backend/services/hr/*.service.js` on HR mutations) with no route or admin UI that surfaces its contents to any client, HR staff or otherwise. So the specific concern is moot today, though it does mean the audit trail itself is not currently useful for a real compliance investigation until a viewer is built — a real-world-completeness gap, not a security one. **While tracing this, a more significant, related finding emerged**: `checkFeatureAccess('staff_hr')` — the gate applied to essentially all of `/api/hr/*` in `hr.routes.js` — is a **tenant-plan-level** check only (`tenant.plan` includes the `staff_hr` feature), not a **staff-role-level** check. I confirmed this concretely in two sub-routers: `hr/employees.routes.js` defines an `hrRoles = ['admin', 'owner', 'manager', 'master_admin']` array, but only uses it to decide whether to auto-create an employee record for the current user — the actual `listEmployees(...)` call that returns the full employee list runs **unconditionally**, with no role check gating it. `hr/businessTaxInfo.routes.js` (business EIN/tax ID) has **no role check at all** beyond the parent's tenant-plan feature gate. Practical implication: **any staff member with a valid session at a tenant whose plan includes `staff_hr` — not just HR managers, owners, or admins — can currently list all employees and view the business's tax information**, as long as they're logged in at all (e.g., a line cook with a `staff` role and an ordinary POS login). This is a real compliance-relevant gap for a module the platform specifically markets as HR-compliant (`00-CEO-STRATEGIC-OVERVIEW.md`: "HR data — Separate HR DB per tenant for compliance") — separating the *database* is necessary but not sufficient if *within* that database, role-based scoping isn't actually enforced consistently. **Not fixed in this pass** — this is a real product/policy decision (which specific HR sub-resources should be admin/owner/manager-only vs visible to all staff) that needs a deliberate design pass across all `hr/*.routes.js` files, not a unilateral rushed patch; recommend it as the next dedicated engineering effort after this fix session, given how many separate HR route files would need consistent treatment. | Low (narrow question) / **Medium-High (the broader finding)** | Small (narrow question, resolved) / Medium-Large (the broader HR role-scoping gap, needs a deliberate design pass across ~10 HR route files) |
| S11-8 | Hygiene (carried from S5-2/S5-3) | The legacy Stripe/Coinbase payments router (`ENABLE_LEGACY_3P=1`, live on this server, writing outside the invoicing backbone, apparently unused by the current frontend) remains an open item. It is not as urgent as S11-1 through S11-3 since it appears dormant, but "dormant and forgotten" is exactly how these things become incidents later. Recommend resolving alongside the S11-3 secrets rotation work, since it's a similar "clean up now while cheap" category of fix. | Medium | Small (flag flip, pending a decision on whether to retire or properly integrate it) |

### Correction to S11-1 (found during remediation, not during the original audit pass)

While implementing the S11-1 fix, tracing the actual frontend call chain (the same discipline that corrected S9-4) turned up an important nuance that changes the threat model without changing the conclusion that this needed fixing:

- **`Kiosk.jsx` and `MobilePOS.jsx` do not call `/api/kiosk` or `/api/mobile` at all.** Both use `posApi`/`pos.createCart` — the same `/api/pos/carts/*` endpoints as `ModernPOS.jsx` and `Storefront.jsx`, which are mounted behind `requireSessionAuth` (a real platform login is required). So the specific claim "kiosk/mobile checkout that real customers use today is unauthenticated" was **not accurate** — that traffic already requires a session.
- **However**, `server.js` mounts the separate `/api/kiosk` and `/api/mobile` REST routers with `optionalSessionAuth` (not `requireSessionAuth`) plus the unvalidated `deviceAuth` — meaning these routers are genuinely reachable by anyone, with no session, who supplies a tenant slug and an arbitrary device ID. They are documented as real, intentional API surface in `API-ENDPOINTS-CATALOG.md` (§18a/§18b), consistent with supporting **unattended, self-service kiosk hardware that has no logged-in staff member** — a legitimate use case that session-based auth cannot serve, which is exactly why a separate device-credential concept should exist here. The corrected risk statement: **not** "the checkout flow real users hit today is unprotected," but **"a live, documented, invoice-creating API surface designed for unattended devices has no real device credential behind it, and could be called directly by anyone who reads the same API docs this platform publishes."** Still worth fixing at Critical/High priority — just for a more precise reason than originally stated.

### S11-1 remediation status: FIXED (with one more discovery along the way)

Implemented and verified live on this server:

1. **`device_secret_hash` column added** to `Devices` on every tenant database and the template (`dev-template`, `dev-mariospizza`, `dev-sawyer-farms`, `dev-mistys-beautique`, `dev-tiger-one`, `dev-master-tenant`) — nullable, so existing devices are unaffected until paired.
2. **`deviceAuth.js` rewritten**: `deviceAuth` (strict — `/api/kiosk`, `/api/mobile`) now rejects any `x-device-id` that doesn't match a real, active `Devices` row for the resolved tenant's org; if that device has a secret on file, `x-device-secret` must also match via `bcrypt.compare`. `optionalDeviceAuth` (`/api/pos`) applies the same validation when a device id is claimed, but degrades to an anonymous web-session device label rather than hard-failing when none is claimed or when a claimed one doesn't check out — the real auth boundary there is the required staff session, not the device claim.
3. **Device creation now issues a real secret** (`crypto.randomBytes(24)`, bcrypt-hashed for storage, returned once in the creation response) and a new `POST /api/admin/devices/:id/regenerate-secret` endpoint lets existing/legacy devices be paired after the fact. `device_secret_hash` is stripped from every device read/list/update response so it never round-trips to a client.
4. **A second, unrelated bug was found and fixed while verifying this**: `server.js` mounted `/api/kiosk` and `/api/mobile` with a comment reading *"Mount kiosk/mobile early to avoid any legacy session guards"* — but the mount was still positioned **after** four routers (`maintenance.routes.js`, `bi.routes.js`, `billing.routes.js`, `financial.routes.js`) that are mounted at a bare `/api` prefix with an **unscoped** `router.use(requireSessionAuth, tenantResolver)`. Express runs middleware in registration order regardless of which router "owns" a path, so every request to `/api/kiosk/*` or `/api/mobile/*` was being rejected with a generic 401 by one of those four routers **before it ever reached `deviceAuth`** — independent of anything in this fix. This explains why the original threat model needed two corrections in this document: first because the real frontend doesn't call these routes, and now because **they may not have been reachable at all** in this deployment until this fix moved their mount to immediately after the rate limiter, ahead of every bare-`/api` router. This also means: if kiosk/mobile self-service was ever intended to work standalone (no staff session) in production, it was likely non-functional before today, independent of the security question — worth flagging to the team as its own product finding, not just a security one.
5. **Verified live** via direct requests to the backend: an unregistered device id is rejected (`device_not_found`); a real, unpaired legacy device passes through with a logged warning (backward compatible); a paired device without the secret header is rejected (`device_secret_required`); a paired device with the wrong secret is rejected (`device_secret_invalid`); a paired device with the correct secret passes. The real `/api/pos` path (used by `ModernPOS`, `Kiosk.jsx`, `MobilePOS.jsx`, `Storefront.jsx` today) continues to require a session exactly as before — this change does not affect it beyond adding the same device-validation layer when a device id happens to be claimed.

**Residual/left for the team:** existing devices in the field have no secret yet and will keep working unpaired (by design, to avoid a breaking rollout) — pairing them via the new regenerate-secret endpoint is a rollout task, not a code task, and there is currently no UI button for it (only the raw API endpoint). Recommend adding a "Regenerate device secret" action to `DeviceManager.jsx` as a small follow-up.

### S11-2 and S11-3 remediation status: FIXED

**S11-2 (SQL escaping → parameterized queries):** All 7 files fixed (`staff.routes.js`, `devices.routes.js`, `inventory.items.routes.js`, `transactions.routes.js`, `customers.routes.js`, `admin.catalog.routes.js`, `admin.database.routes.js`) — every `.replace(/'/g, "''")` manual-escaping site converted to parameterized `?` placeholders (or, in the one case building a text SQL export file rather than executing a query directly, to `mysql2`'s own `.escape()` helper, which handles edge cases a hand-rolled quote-replace does not). All 7 files pass `node --check`; the backend restarted cleanly with no new errors.

**S11-3 (session secret, cookie, file permissions):** `SESSION_SECRET` now set to a freshly generated random value in `infrastructure/ecosystem.production.config.js` (confirmed resolved in the live process via `pm2 env`); session cookie `secure` flag now derives from `NODE_ENV`/an explicit `COOKIE_SECURE` escape hatch instead of being hardcoded `false`; `backend/.env`, `infrastructure/ecosystem.production.config.js`, and root `ecosystem.config.js` are now `600` (owner-only) instead of world-readable. Verified: backend restarted cleanly, health check and login endpoint both respond correctly, no new errors in logs. **Note for the team:** rotating `SESSION_SECRET` invalidates all previously-issued session cookies — anyone with an active session before this deploy will need to log in again.

### S5-2/S5-3/S11-8 remediation status: FIXED

Set `ENABLE_LEGACY_3P=0` in `backend/.env` (was `1`), retiring the legacy Stripe/Coinbase payments router. Confirmed via direct testing that the router is no longer mounted. Also noted while verifying: `STRIPE_SECRET` in this environment was still the placeholder `sk_test_xxx`, confirming this path was never wired to a real payment processor here regardless — reinforcing that disabling it (rather than investing in properly rewiring it into Core Invoicing) was the right call for this environment. If real Stripe/Coinbase processing is needed later, it should go through the invoicing-backbone-integrated path, not this legacy router.

### New finding discovered while verifying S11-8 (not part of the original 12-stage audit)

| ID | Tag | Finding | Severity | Effort |
|----|-----|---------|----------|--------|
| S11-9 | **Risk — newly discovered, High** | While confirming the legacy payments router was actually unmounted, I found that **any nonexistent `/api/*` endpoint returns a misleading `401 "Authentication required"` instead of a `404`** — confirmed with a deliberately made-up path (`/api/totally-nonexistent-endpoint-xyz123`). Root cause: **four routers — `maintenance.routes.js`, `bi.routes.js`, `billing.routes.js`, `financial.routes.js` — are all mounted at a bare `/api` prefix in `server.js` (around the original lines 697-700) and each applies an unscoped `router.use(requireSessionAuth, tenantResolver)` at their own top level.** Express runs this for literally every `/api/*` request that reaches that point in the middleware chain, regardless of whether the request has anything to do with maintenance/BI/billing/financial — if no session exists, the request is rejected right there, before Express ever gets a chance to return a clean 404 for a route that simply doesn't exist (or hasn't been reached yet due to mount order, exactly as happened with `/api/kiosk`/`/api/mobile` in S11-1's remediation). This is very likely also the root cause behind why an earlier engineer's comment ("Mount kiosk/mobile early to avoid any legacy session guards") did not actually work — they were half-right about the problem but the fix was incomplete. **This was not fixed in this pass** — relocating the kiosk/mobile mount (done in S11-1) sidesteps it for those two paths specifically, but the same masking effect likely still affects any other endpoint mounted after these four routers that is not supposed to require a session, and it degrades API ergonomics generally (typos/misrouted requests report as an auth problem instead of a routing problem, which is confusing to debug). **Fixed in this pass**: scoped each of the four routers' internal `router.use(requireSessionAuth, tenantResolver)` to its own path prefix (`router.use('/api/maintenance', ...)`, `/api/bi`, `/api/billing`, `/api/financial`) instead of leaving it unscoped — since each router's own routes already define their full `/api/...` path internally, this required no change to `server.js`'s mount calls or to any route definitions. Verified live: `/api/maintenance/status`, `/api/bi/insights`, `/api/billing/subscription`, and `/api/financial/tax` all still correctly return 401 without a session (unchanged, correct behavior for their own routes), while `/api/kiosk/health` with a real device continues to work. | ~~High~~ **Fixed** | Small (confirmed) |
| S11-10 | **Missing / architectural debt — newly discovered, not fixed in this pass** | Fixing S11-9 did **not** fully resolve the symptom that led to finding it: a deliberately nonexistent path (`/api/totally-nonexistent-endpoint-xyz123`) **still** returns `401` instead of a clean `404` even after the four-router fix above. Tracing further surfaced something larger: `backend/routes/api/index.js` (imported in `server.js` as `apiRouter`, mounted at bare `/api` under the comment "✅ NEW: Component-Based API Structure (Square/Toast style)") is a **second, fully separate, parallel router tree** with its **own** `/kiosk`, `/mobile`, `/payments`, `/admin`, `/devices`, `/master-admin`, `/tickets` sub-mounts (backed by an entirely different set of files under `backend/routes/api/kiosk/`, `backend/routes/api/mobile/`, `backend/routes/api/payments/`, `backend/routes/api/admin/`, etc.) — duplicating, by name, several of the exact same concerns already handled by the more specific mounts directly in `server.js` that this audit spent most of its time on. This looks like an **abandoned mid-migration**: someone began moving toward a cleaner "component-based" structure and stopped partway, leaving both the old direct-mount style and the new composite-router style live simultaneously. The remaining source of the misleading 401 was not conclusively pinned down within this pass — likely another unscoped auth check somewhere inside this second tree — but the more important finding is the duplication itself: **there may be two different implementations answering for the same-sounding endpoints**, and it is not obvious from reading `server.js` alone which one actually wins for any given request without tracing Express's exact registration order path-by-path, as this audit had to do repeatedly in this stage. Recommend this be scoped as its own, dedicated architecture-cleanup project — determine whether `routes/api/index.js` and its subtree are live/intended or safe to remove entirely, rather than patching individual symptoms one at a time as this audit has been doing. | Medium (mostly a debugging/DX and maintainability problem, not a live security hole) | Large (an audit-and-consolidate project, not a quick fix) |

### Stage 11 conclusion

This rollup converts what were, across Stages 1–10, several individually-concerning-but-uncertain findings into a small, concrete, prioritized security punch list: **one Critical item** (device/kiosk/mobile auth, S11-1 — now fully confirmed, touches real money movement), **two High items** that are pure configuration hardening with no architecture change needed (S11-2 SQL escaping, S11-3 session/secrets), and a handful of Medium/Low items that are either cleanup (S11-4 logging, S11-8 legacy payments) or need one more verification pass before they can be closed either way (S11-6 master-admin audit coverage, S11-7 HR audit log location). None of this rises to "the platform is fundamentally insecure" — the core tenant-isolation architecture (S11-5) is genuinely solid, and several things that looked concerning at first glance (S8-1 HR route protection, S8-2 session-code duplication) turned out to be fine once traced fully. The honest picture is: **strong foundation, with a short, well-defined list of concrete hardening items that should be treated as urgent given how directly S11-1 touches real transactions.**

---

---

## Stage 12 — Synthesis

### Overall verdict

This platform's foundation is genuinely solid: real multi-tenant database isolation (S2-1, S11-5), an accurate and mostly-honest documentation set (with two chapters — audit S9-2 and signage S10-1 — that are models of precision), a working invoicing backbone that survived every stress test this audit threw at it (S5-1, S9-4 corrected), and several places where initial "this looks bad" readings were traced fully and found to be fine (S8-1 HR route protection, S8-2 session-code duplication, S9-4 storefront checkout). That combination — strong architecture, mostly accurate docs, and a track record of findings surviving deeper scrutiny — is not what a fundamentally troubled codebase looks like.

At the same time, this was not a rubber-stamp review. It surfaced one **live, confirmed, currently-broken user-facing feature** (S1-1 — barcode catalog images), one **fully-confirmed Critical security gap** (S11-1 — kiosk/mobile device auth), a **short list of high-value configuration hardening items** (S11-2, S11-3), and a recurring **pattern-level product finding** (S0-4, S6-3, S7-2, S7-3 — capable backend, thin default UI for anything outside a full-service retail/restaurant ICP) that matters more than any single bug because it recurred independently in four different modules without my looking for it a second time.

### Prioritized roadmap

**Tier 1 — Fix first (security, live and confirmed, touches real transactions or currently broken for real users)**

1. **S11-1** — Kiosk/mobile device authentication is not a real credential check (`deviceAuth.js` trusts a client-supplied header unconditionally). Touches real invoice-creating checkout flows. **Critical.**
2. **S1-1** — Live Nginx config missing the `/media/` block; every platform barcode/UPC catalog product image is currently returning the dashboard's HTML instead of the image on `staging.gopastearth.com`. Fix already exists in the repo (`infrastructure/nginx-staging.conf`); just needs deploying + reload. **High, trivial effort.**
3. **S11-3** — `SESSION_SECRET` unset (hardcoded fallback in use), session cookie not `secure`, plaintext credentials in a world-readable config file. **High, small effort, but plan a maintenance window** (rotating `SESSION_SECRET` invalidates active sessions).
4. **S11-2** — Manual SQL-escaping instead of parameterized queries across 8 confirmed files. **High, medium effort** (mechanical but needs per-file testing).

**Tier 2 — Fix soon (real but lower urgency, or needs one more verification step)**

5. **S11-4** — Verbose PII-bearing `console.log` calls across the entire auth middleware stack.
6. **S11-8 / S5-2 / S5-3** — Legacy Stripe/Coinbase payments router is live (flag on) but appears unused; retire the flag or wire it into the invoicing backbone properly.
7. **S11-6** — Verify master-admin cross-tenant access is itself fully audited.
8. **S11-7 / S9-3** — Verify `hr_audit_log`'s location in the main DB doesn't leak HR-adjacent data to non-HR staff.
9. **S3-6** — Document the "some routers self-manage auth internally" convention so `server.js` is a reliable audit surface again.

**Tier 3 — Real-world product completeness (not bugs, but affects whether the "any business type" claim is true in practice)**

10. **S7-2** — `isConsumable` (KDS ingredient routing) and `weightPerUnit`/`volumePerUnit` (true weight-based pricing) are real, backend-supported, but have zero frontend UI. Highest-value of this tier since `isConsumable` likely has near-term demand from any KDS/recipe-using tenant.
11. **S7-3** — Reconcile this session's new `unitOfMeasure`/`priceMode` fields with the older, unused `weightPerUnit`/`volumePerUnit` design before a third "unit" concept gets invented later.
12. **S9-4b** — Confirm `allowGuestCheckout` is enforced server-side, not just a UI preference.
13. **S6-3 / S0-4** — Investigate (with real users, not more code-reading) whether the register/shift ceremony and inventory's default complexity genuinely create friction for lightweight/single-operator tenants (farm stands, small producers) — this needs product validation, not another audit pass.

**Tier 4 — Documentation-only fixes (no code risk, improves accuracy)**

14. **S0-2, S0-6** — Tone/consistency between `00-CEO-STRATEGIC-OVERVIEW.md` and `28`'s more calibrated framing; unverified "100+ route files, 90+ models" figures.
15. **S1-3** — Delete or rewrite the stale `infrastructure/CURRENT-STATUS.md` (dated December 2025, describes already-fixed problems as current).
16. **S1-5, S1-6, S1-7** — Deployment-step doc precision (frontend build vs. dev-server reality), `cloud-sql-proxy`'s inert PM2 entry, current live Cloud SQL instance shape.
17. **S2-2** — Document the undocumented `master-tenant`/`dev-master-tenant` fallback pattern.
18. **S2-3** — Qualify the "tenant data never crosses boundaries" wording to acknowledge the intentional master-admin exception.
19. **S3-7** — Point `02`'s tenant-resolution summary at Appendix D instead of maintaining a slightly different abbreviated copy.
20. **S4-3, S6-4** — Document the offline/PWA system honestly, including its currently-disabled service worker (S4-2).
21. **S5-5** — Trace and document which of the three Stripe/Coinbase-adjacent code paths is authoritative.
22. **S6-2** — State plainly that KDS updates via polling (not WebSocket) with a concrete default interval.
23. **S8-3** — Document which reporting surfaces read from the main DB vs. the analytics DB.

### The one strategic (non-engineering) recommendation

**S0-3 remains the most important non-technical finding of this entire audit.** The competitive-positioning/ICP workbook (`28-COMPETITIVE-POSITIONING-AND-ICP-WORKBOOK.md`) that exists specifically to keep "is this good enough" conversations grounded in a real target customer has never been filled in — its ICP one-liner is blank and all ten of its gap-tag rows say `*fill*`. Every "real-world gap" finding in this audit that touched product-fit questions (S0-4, S6-3, S7-2, S9-4b, and even the original farm-inventory conversation that started this whole audit) is a symptom of not having that document completed. This is a **writing/decision exercise, not an engineering task**, and it would make every future version of this kind of audit sharper: instead of "is register/shift ceremony too heavy for a farm stand?" being an open hypothesis (S6-3), a filled-in ICP and gap-tag matrix would let that be answered definitively as `must-build`, `integrate`, `defer`, or `not our market` in one sitting.

### Proposed documentation updates (for your review, not yet applied)

The following are concrete wording suggestions this audit recommends, sized so you can decide which to actually apply rather than having them silently changed:

- **`00-CEO-STRATEGIC-OVERVIEW.md`**, "Risk & Compliance Considerations" section: change *"Data isolation — Tenant data never crosses boundaries"* to *"Data isolation — Each tenant has dedicated databases; only master-admin platform operators have cross-tenant access, and that access should be fully audited (see Stage 11 verification)."*
- **`00-CEO-STRATEGIC-OVERVIEW.md`**, "Risk & Compliance Considerations" section: change *"Session security — session-based auth, no JWT in main flow"* to *"Session security — session-based auth, no JWT in main flow (session secret and cookie hardening tracked as a Tier 1 fix — see `AUDIT-FINDINGS.md`)"* until S11-3 is actually resolved, then remove the parenthetical.
- **`docs/DOCS-INDEX-AND-GAPS.md`**: add a row/section pointing to this document (`documentation/OAO/AUDIT-FINDINGS.md`) as the current source of truth for known issues, superseding the informal "not covered" bullet list for anything this audit actually traced (inventory flow — now traced in Stage 7; storefront checkout — now traced in Stage 9).
- **`documentation/OAO/28-COMPETITIVE-POSITIONING-AND-ICP-WORKBOOK.md`**: this audit recommends actually filling in Section 2 (ICP one-liner) and Section 3 (gap-tag matrix) as a working session with you, rather than a doc edit I should make unilaterally, since it requires real business judgment about target customers.

I have not applied any of these edits — they are recommendations for you to accept, adjust, or reject.

### What this audit did not cover (explicitly out of scope this pass)

- Line-by-line re-verification of every table in `APPENDIX-E-DATABASE-SCHEMAS.md` (Stage 2 spot-checked provisioning code structure, not every column).
- Full end-to-end trace of gift cards/loyalty (S9-5, explicitly deferred).
- Full re-trace of the DOGE/LTC webhook-to-cart-finalize chain beyond consistency-checking against existing docs (S5-4).
- Exhaustive read of all 100+ backend route files (several findings, e.g. S9-4's storefront correction, explicitly relied on targeted searches plus frontend call-chain tracing rather than reading every file).
- Non-custodial crypto wallet key-custody model verification (C6, noted as an open item in S5-5's stage conclusion).
- The C# bridge and Node bridge's respective failure/reconnection behavior under real network conditions (reviewed source code, not live-tested).

None of this was skipped carelessly — each is called out explicitly in its stage so a future pass (or a future conversation) knows exactly what was and wasn't checked, consistent with this document's purpose as the platform's single running source of truth for what has actually been verified.

---

*End of audit. This document (`documentation/OAO/AUDIT-FINDINGS.md`) should be treated as living — update it in place as findings are resolved, rather than creating a new document for the next review pass.*

---

## Post-audit remediation log (July 2026)

Following the audit, Tier 1 and Tier 2 items from the synthesis roadmap were implemented and verified live on this server, in two passes.

**Tier 1 (Critical/High):**
- S1-1 — Nginx `/media/` block deployed; barcode catalog images confirmed serving correctly.
- S11-2 — All 7 flagged files converted to parameterized SQL queries.
- S11-1 — Real device-credential validation added to `deviceAuth.js`/`optionalDeviceAuth`; device secrets issued at creation + `regenerate-secret` endpoint; kiosk/mobile mount relocated after discovering it was being masked by an unrelated bug (see S11-9).
- S11-3 — Real `SESSION_SECRET` set, session cookie `secure` flag fixed, secret-bearing config files tightened to `600`.

**Tier 2 (Medium, plus two new discoveries made while verifying Tier 2 work):**
- S11-4 — PII-bearing `console.log` calls across `tenantResolver.js`, `sessionAuth.js`, `flexibleAuth.js` converted to `logger.debug` (suppressed by default in production).
- S5-2/S5-3/S11-8 — Legacy Stripe/Coinbase payments router retired (`ENABLE_LEGACY_3P=0`).
- **S11-9 (new)** — Found and fixed the actual reason the S11-1 kiosk/mobile mount relocation was necessary: four routers (`maintenance`, `bi`, `billing`, `financial`) mounted at bare `/api` were each applying an unscoped `router.use(requireSessionAuth, tenantResolver)`, intercepting unrelated `/api/*` traffic before it reached its real destination. Scoped each to its own path prefix.
- **S11-10 (new, not fixed)** — Discovered a second, parallel "component-based API" router tree (`routes/api/index.js` and its subtree) duplicating several concerns already handled elsewhere, left as an architecture-cleanup item for a dedicated follow-up rather than patched piecemeal.
- S11-6 — Corrected the original framing (the real cross-tenant mechanism is `masterAdmin.routes.js`, not the `tenantResolver.js` bypass) and added audit logging to its three mutating tenant-management endpoints.
- S11-7/S9-3 — Confirmed `hr_audit_log` has no read path today (narrow question moot), but surfaced a more significant, unfixed finding: HR route access control is tenant-plan-gated, not staff-role-gated, in at least two sub-routers (`hr/employees.routes.js`, `hr/businessTaxInfo.routes.js`) — flagged for a dedicated design pass.
- S3-6 — Documented the mount-level-vs-router-internal auth convention in both `03-BACKEND-ARCHITECTURE.md` and directly in `server.js` at the exact spot the bug lived.

**S11-7 (HR role-scoping) — FIXED in a follow-up pass:** Added a shared `requireHRAdminRole` middleware (`admin`, `owner`, `manager`, `master_admin` — reusing the role set already informally used elsewhere in the codebase) in `middleware/requireRoles.js`, and applied it across the HR module:
- **Blanket-gated** (no legitimate self-service pattern found): `hr/employees.routes.js` (full employee CRUD + W4/I9/compensation), `hr/businessTaxInfo.routes.js`, `hr/taxRates.routes.js`, `hr/payrollCalculation.routes.js`, `hr/payrollRuns.routes.js`, `hr/payrollRunManager.routes.js`, `hr/departments.routes.js`, `hr/reports.routes.js`.
- **`hr/benefits.routes.js` — blanket-gated for a stronger reason than the others**: tracing it revealed it isn't just missing a role check — `GET /employees/:employeeId`, `PUT /enrollments/:id`, `POST /enrollments/:id/cancel`, and `POST /enrollments` (which takes an arbitrary `employeeId` in the request body) have **no ownership verification at all**, a direct IDOR rather than a role gap. Gated entirely rather than opened for partial self-service.
- **`hr/timeOff.routes.js` — per-route, not blanket**, because this file is the one HR sub-router that already does self-scoping correctly (`request`, `requests`, `balance`, `balances`, `cancel` all resolve the *caller's own* employee ID from their session email before touching data). Only `POST /:requestId/approve`, `POST /:requestId/reject`, and `GET /team` were gated, since the underlying service functions do not themselves verify the approver has authority over the target request.
- **Legacy inline routes directly in `hr.routes.js`** (a parallel, still-live implementation alongside the newer sub-routers — payroll runs, HR reports, schedules, reviews, timesheet viewing/approval) were gated the same way. `clockIn`/`clockOut`/`startBreak`/`endBreak` were deliberately left open — they appear designed for a shared timeclock terminal (staff identify via `staffId`/PIN, not necessarily their own login), matching the POS register PIN pattern elsewhere.
- **New, smaller, explicitly unfixed finding**: `clockIn`/`clockOut` accept an arbitrary `staffId` in the request body with no verification it matches the caller — a real "clock in as someone else" integrity concern, but a different category of problem (data integrity, not confidentiality) than what this pass targeted, and one that needs the actual timeclock UI/device flow traced before it can be fixed without breaking the legitimate shared-terminal use case.

Verified live: backend restarts cleanly, `/api/hr/employees`, `/api/hr/business-tax-info`, and `/api/hr/time-off/request` all correctly require a session (self-service time-off submission remains reachable in principle — full role-differentiated testing would require real admin vs. non-admin staff sessions, which this environment does not have credentials for).

**What remains open:** S11-10 (parallel router architecture — now confirmed to also apply to HR: legacy inline payroll/reports routes duplicate the newer sub-routers), the `clockIn`/`clockOut` arbitrary-`staffId` integrity gap noted above, Tier 3 (real-world product completeness: `isConsumable`/`weightPerUnit`/`volumePerUnit` UI, register/shift ceremony for lightweight tenants, storefront guest-checkout enforcement), and Tier 4 (remaining documentation-only fixes). All fixes in this log were verified via direct requests against the running backend after each change, with the process restarted cleanly and no new errors in logs each time.

---

## KDS ↔ Floor Plan ↔ Signage integration check (movie-theater scenario)

Prompted by a direct question: for a business like a movie theater — food ordered at a concession counter, routed to KDS, and the customer seated in an auditorium — do KDS, the visual floor-plan/seating system, and the nSign-class digital signage system actually work together, end to end?

**S12-1 | Critical bug, fixed | `kitchen_tickets` table did not exist in any live tenant database.** The table is created ad hoc by a demo seed script only — it was never part of any real migration or bootstrap path. Confirmed live against `dev-mariospizza`: `SHOW TABLES LIKE 'kitchen_tickets'` returned zero rows before the fix. This meant **KDS was completely non-functional for every real tenant** — every ticket insert/read (`pos.routes.js` cart finalize, `kds.routes.js` orders/bump/recall/ingest) would have thrown `ER_NO_SUCH_TABLE` the first time a real order tried to fire a kitchen ticket. This is as severe as S1-1 (the Nginx media bug) in kind — a fully-documented, seemingly-complete feature that was silently broken for every live tenant. **Fix:** added `ensureKitchenTicketsTable(conn)` (`backend/utils/posTenantQueries.js`) — an idempotent `CREATE TABLE IF NOT EXISTS` with the full schema actually used by the code (including `seatId`/`tableId`/`tableNumber`/`seatNumber` for the floor-plan tie-in below) — and called it at the top of every code path that touches `kitchen_tickets`: `posTenantQueries.getKitchenTicketByCartId`, `posTenantQueries.createKitchenTicket`, `pos.routes.js`'s `insertKitchenTicketsForCartStations`, and all four `kds.routes.js` handlers (`GET /orders`, `POST /orders/:id/bump`, `POST /orders/:id/recall`, `POST /ingest`). **Verified live:** ran the new helper directly against `dev-mariospizza` — table did not exist beforehand, exists with the correct schema afterward; backend restarted cleanly with no new errors.

**S12-2 | Accurate — confirmed working | KDS ↔ Floor Plan (seating layout) integration already exists at the data layer.** `seating_layouts` (admin-authored via `SeatingLayoutManager.jsx`, including cinema-oriented templates — small/medium/large cinema, theater, restaurant, with row/seat/aisle labels) defines stable `seatId`/`tableId` identities. When a POS cart carries `cart.metadata.seatId`/`tableId`/`tableNumber`/`seatNumber` (e.g., a concession order rung against a specific auditorium seat), `insertKitchenTicketsForCartStations` (`pos.routes.js`) persists all four fields onto the `kitchen_tickets` row, and `kds.routes.js`'s `formatTicket` returns them on every ticket. The web KDS (`KitchenDisplay.jsx`) renders these as "Table 5" / "Seat 12"-style chips. So for the movie-theater case: a runner at the concession KDS screen can already see which seat an order belongs to, sourced from the same floor-plan data the admin drew. This matches `documentation/OAO/09-kds-and-kitchen.html`'s own description almost exactly — that chapter is accurate, not drifted.

**S12-3 | Real gap, honestly documented as roadmap (not a false claim) | KDS ↔ Signage is not integrated.** `signage.routes.js`/`SignageAds`/`SignageDisplays` is purely a folder-and-playlist system for admin-curated ads/media (images, video, URL, HTML) — there is no concept anywhere in the schema, routes, or frontend of a live order-status board (e.g., "Order #42 — READY," or a visual mini floor-plan with seat highlighting) sourced from `kitchen_tickets`. `09-kds-and-kitchen.html` itself is candid about this: it explicitly frames a "read-only mini floor plan on the expo/signage screen" as a **"natural extension... not all built as a single map-on-KDS view yet"** and lists it under roadmap language, not as a current capability. Confirmed by direct route inspection — this is accurate self-disclosure, not documentation drift. **Real-world implication:** today, a movie theater (or any table-service/seat-based business) can route food orders into KDS and see the destination seat/table on the kitchen-side screen, but cannot put a customer-facing or lobby/serving-area screen showing live order status or a highlighted seat map without a new feature — nothing to "fix," but worth flagging explicitly as a scoped, not-yet-built product capability if this is important to the roadmap.

**Net assessment:** the core claim implicit in the user's question — "do these systems play together" — is **mostly true and now fully functional** (it wasn't, due to S12-1) for the KDS+floor-plan half, and **honestly not yet true** for the KDS+signage half, exactly as the existing documentation already says. No further code changes made for S12-3; flagging as a candidate Tier 3/4 roadmap item (a new signage "display type" backed by a `GET /api/kds/*` status feed) rather than building it unprompted, since it is a net-new feature rather than a bug fix.
