# ADR-086: Isolated PWA Architecture — When to Build Standalone Apps with Zero Cross-Access

## Status

**Accepted** (2026-06-27)

## Status History

```yaml
- 2026-06-27:
    status: Accepted
    decision_maker: hkl
    rationale: Health Tracker instantiates the isolated PWA pattern; architecture codified to guide future standalone apps
    changed_via: adr-kit (360lm)
```

## Context

The 360lm platform has historically been a single suite of integrated Employee Resource Planning (ERP) PWAs sharing:
- Unified authentication via the Hub (ADR-012)
- Shared `lm360` and `lm360_prod` PostgreSQL databases
- Cross-schema data access via proxy routing (ADR-074)
- Coordinated service worker cache versioning (ADR-005)
- Common styling and accessibility standards (ADR-002, ADR-081, ADR-082)
- Deployment via `deploy-prod.sh` and shared backup cron jobs

However, not all applications built on the infrastructure should integrate into the ERP suite. **Health Tracker** (launched 2026-06-14) is the first **isolated PWA** — a completely standalone application with its own database, domain, roles, authentication model, and deployment pipeline. It serves a different user population (patients and caregivers, not 360lm employees), handles sensitive personal health data requiring separate access control, and uses username/password + email magic-link authentication instead of PIN-based hub auth.

**Current state:**
- **Health Tracker:** `/var/www/Others/health/`, database `health`, domain `health.srv1111289.hstgr.cloud`, roles `health_user`/`health_admin`/`health_caregiver`, separate PostgREST instance, no `hub.pwa_registry` entry
- **Paperclip AI platform (ADR-078):** Similar isolation pattern (separate DB role, zero lm360 access) but runs on the same VPS within 360lm's Docker network
- **No documented decision criteria** for when a new app should be isolated vs. ERP-integrated

This ADR codifies the decision framework: which applications belong in the ERP suite (ERP PWAs) and which should be built as isolated standalone apps.

## Decision

**An application MUST be built as an isolated PWA if ALL of the following criteria are met:**

| Criterion | Description |
|---|---|
| **Different user population** | Users are NOT 360lm employees (e.g., patients, external clients, caregivers) authenticated via the hub |
| **Data isolation required** | The data must NOT be accessible to OpenClaw, other 360lm integrations, or hub-authenticated employees by default |
| **Auth model differs** | The app does NOT use PIN-based hub authentication; uses username/password, magic link, OAuth, caregiver links, or other non-PIN scheme |
| **Standalone domain** | The business domain has no shared schemas, no cross-PWA RPCs, and no data dependencies on `expense.employees`, `hub.*`, or other shared tables |

**An application MUST be built as an ERP PWA if ANY of the following apply:**

| Criterion | Description |
|---|---|
| **Employee users** | Users are 360lm employees authenticated via Hub PIN (ADR-012) |
| **Shared schemas** | The app reads/writes to shared tables (e.g., `expense.employees`, `hub.pwa_registry`, `custodian.payees`) |
| **Hub catalog** | The app is listed in the Hub PWA registry and accessible via the Hub interface |
| **Cross-PWA RPCs** | The app calls RPCs or shared helpers that other PWAs depend on |

**Isolated PWA Standards (not bound by ERP PWAs ADRs):**

1. **Database:**
   - Own PostgreSQL database (e.g., `health`, `[app_name]`)
   - Own database role with NO grants to `lm360` or `lm360_prod` schemas (follow ADR-078 Paperclip pattern)
   - Zero cross-schema access; all data lives in the app's own schema

2. **Authentication & Authorization:**
   - Own auth model (username/password, magic link, OAuth, caregiver/custodian patterns, etc.)
   - Own role system (independent of 360lm's role_slug structure)
   - Does NOT use `hub.verify_pin()` RPC
   - Does NOT register in `hub.pwa_registry`

3. **API & Data Access:**
   - Own PostgREST instance or sidecar (separate from 360lm's shared PostgREST)
   - No reliance on 360lm's proxy service for cross-schema queries
   - HTTP endpoints and RPCs are app-specific; no shared RPC namespace

4. **Infrastructure & Deployment:**
   - Own Traefik route on a subdomain or separate domain (ADR-073 label conventions and middleware chaining still apply)
   - Own `docker-compose.yml` (separate from `root/360lm-web/docker-compose.yml`)
   - Own deploy script (not `deploy-prod.sh`)
   - Own backup schedule (not dependent on lm360 backup cron; can run independently)

5. **UI & Styling:**
   - May implement any visual theme (dark UI, custom branding, etc.)
   - NOT bound by ADR-082 forced-light-mode rule (applies only to ERP PWAs)
   - SHOULD still use ADR-076 mobile-first viewport standard and ADR-081 safe-area insets
   - SHOULD link `safe-bottom.css` from shared/ if it has sticky bottom bars (ADR-002 principle applies universally)

6. **Service Worker & Caching:**
   - Own SW cache namespace (e.g., `health-v1`, not shared with lm360 versioning)
   - Own CACHE_VER strategy (ADR-005 pattern applies: use string bumps, not skipWaiting())
   - SW may have different cache strategies than ERP PWAs (e.g., network-first for real-time data)

7. **Still Subject To:**
   - ADR-069 production hotfix protocol (adapted: bugfix tested in isolated app's dev environment, merged to master, deployed via separate hotfix script)
   - ADR-066 session locking protocol (if running on same VPS and accessing shared resources like Docker or filesystem)
   - ADR-073 Traefik label conventions and middleware chaining patterns
   - Code review, testing, and commit discipline equivalent to ERP PWAs

## Implementation Notes

### Checklist for Creating a New Isolated PWA

- [ ] **Naming & Location:** Create app under `/var/www/Others/[app_name]/` (outside the 360lm tree)
- [ ] **Database:**
  - [ ] Create database `[app_name]` in shared PostgreSQL instance
  - [ ] Create role `[app_name]_app` or `[app_name]_user` (no superuser/createdb grants)
  - [ ] Create schema `[app_name]` owned by the role
  - [ ] Document role + database in memory file (`memory/[app_name].md`)
- [ ] **Docker:**
  - [ ] Create `docker-compose.yml` at `/var/www/Others/[app_name]/docker-compose.yml`
  - [ ] Use unique container name (e.g., `[app_name]-app`)
  - [ ] Place on shared Docker network (`root_default` or equivalent) only if needed for internal communication
  - [ ] Export environment (DATABASE_URL, secrets) via `.env` file (mode 600, root only)
- [ ] **API Layer:**
  - [ ] Set up PostgREST or custom API sidecar in compose
  - [ ] Document API endpoints and auth scheme in project README
- [ ] **Authentication:**
  - [ ] Implement app-specific login (no PIN verification, no hub session reading)
  - [ ] Document role hierarchy and access control rules
  - [ ] If users need to upload/manage data: implement audit logging at DB trigger or app level
- [ ] **Frontend:**
  - [ ] Single HTML file or full framework (no restriction; isolated PWAs not bound by ADR-013)
  - [ ] Mobile-first viewport (ADR-076)
  - [ ] Safe-area insets for notched devices (ADR-081)
  - [ ] SW cache versioning (ADR-005 pattern with app-specific namespace)
- [ ] **Traefik Routing:**
  - [ ] Add labels to docker-compose for Traefik auto-discovery
  - [ ] Use `Host()` rule pointing to app subdomain (e.g., `health.srv1111289.hstgr.cloud`)
  - [ ] Register middleware (TLS cert resolver, auth if needed)
  - [ ] Test routing: `curl -H 'Host: [app].srv1111289.hstgr.cloud' http://localhost`
- [ ] **Backups:**
  - [ ] Create backup script (`/usr/local/bin/[app_name]-backup.sh`)
  - [ ] Schedule via system cron (separate from lm360 backup window)
  - [ ] Document retention policy and restore procedure
- [ ] **Monitoring & Logging:**
  - [ ] Set up log directory and rotation (`/var/log/[app_name]/`)
  - [ ] Document health check endpoint (or ping container)
- [ ] **Testing:**
  - [ ] Create Playwright spec at `/var/www/360lm/tests/[app_name].spec.js` (reference isolated app from 360lm test harness) OR create separate test file in app's repo
  - [ ] Test user registration, login, core workflows
- [ ] **Documentation:**
  - [ ] Create `memory/[app_name].md` (location, DB, domain, roles, status, pending items)
  - [ ] Add one-line entry to this ADR's "Related Decisions" or "References" pointing to the app's memory
- [ ] **Deployment:**
  - [ ] Create deploy script (`/usr/local/bin/deploy-[app_name]-prod.sh`) OR use standard git pull + compose restart
  - [ ] Document deployment runbook
- [ ] **ADR Compliance:**
  - [ ] Check this ADR (ADR-086) — confirm isolated pattern is appropriate
  - [ ] Check ADR-069 (hotfix protocol) — adapt if needed
  - [ ] Check ADR-066 (session locking) — confirm no shared zone conflicts

### Reference Implementation: Health Tracker

**Location:** `/var/www/Others/health/`

**Key files:**
- `public/index.html` — ~1340-line PWA (single HTML file, auth + UI)
- `public/sw.js` — service worker, cache namespace `health-v1`
- `server/` — Node/Express API (auth, OCR, sync)
- `docker-compose.yml` — standalone compose, image `health-app:1.0.0`, port 3200 (dev) or 3300 (prod-sidecar)
- `.env` — DATABASE_URL pointing to `health` DB, JWT_SECRET, OCR chain keys (mode 600)
- `db/migrate_health_v1.sql` — schema: users, bp_readings, sugar_readings, caregiver_links, admin_audit_log

**Database:**
- Role: `health_app` (created as non-superuser)
- Database: `health` (separate from `lm360`, `lm360_prod`)
- Zero access to any lm360 schema (verified via `\dp` in psql)

**Authentication:**
- Username + password (bcrypt 12-round hash)
- Email magic-link for new user signup or forgot-password
- Caregiver links: patient self-grants OR admin assigns
- JWT tokens (90-day expiry)
- Per-user local encryption (WebCrypto AES-GCM, key derived from PIN via PBKDF2)

**Domain & Routing:**
- Subdomain: `https://health.srv1111289.hstgr.cloud`
- Traefik labels in compose: Host rule, websecure entrypoint, TLS cert resolver

**Backup:**
- External cron job (`/usr/local/bin/health-backup.sh`) at 3 AM daily
- Outputs: SQL dump + files tarball to `/backup/` with 14-day retention

**Status (2026-06-14):** LIVE. Playwright spec exists; Traefik routing verified; DB isolation confirmed.

**Memory:** `/root/.claude/projects/-var-www-360lm/memory/health_tracker.md`

## Alternatives Considered

### 1. Build everything as an ERP PWA (all apps share hub auth, shared DB)

**Rejected:**
- Mixes business domains (employee payroll, HR, finance data with patient health data)
- Health Tracker users (patients, caregivers) do NOT have 360lm employee accounts or PIN authentication
- Shared `lm360` schema would require health-specific tables visible to all employees — compliance/privacy violation
- Backup strategy would tie health data to 360lm's backup window — creates risk of cross-domain data loss
- Decommissioning one app becomes risky when schemas are entangled (schema deletion could orphan foreign keys from other apps)

### 2. Run isolated apps on a completely separate infrastructure (separate VPS, separate DB server)

**Rejected:**
- Unnecessary cost for a small app (DevOps overhead, separate VPS subscription, separate backups)
- Coordination overhead increases (two SSH targets, two monitoring dashboards)
- Traefik and Docker already handle multi-app orchestration well on a single VPS
- No security advantage (API keys still in environment; network compromise still possible)
- Maintenance burden (separate cert renewal, separate log rotation, separate health checks)
- **Current approach (same VPS, isolated database role) is acceptable:** zero cross-schema access is enforced by PostgreSQL permissions, separate backups are easy via cron scripts

### 3. Create a shared "multi-tenant" schema in lm360 for isolated apps

**Rejected:**
- Violates ADR-009 (each PWA owns its schema)
- Requires complex row-level security (RLS) policies to prevent cross-tenant leakage — hard to audit, easy to misconfigure
- Schema evolution is tied to lm360's migration schedule, making isolated app deployment dependent on hub/main pipeline
- Backups still entangled; PITR or point-in-time rollback of one app affects others

### 4. Use a separate PostgreSQL instance for isolated apps (e.g., managed RDS)

**Rejected:**
- Significant cost increase (RDS licensing, data transfer fees, managed backup fees)
- Adds network latency if RDS is in a different region
- Requires separate credentials management (separate pgpass, separate Traefik middleware for auth)
- No architectural benefit over isolated role + schema on the same instance
- Upgrade and maintenance complexity increases (two PostgreSQL versions to track)

## Consequences

### Positive

1. **Data isolation:** Isolated PWAs cannot accidentally leak data to 360lm employees or other apps (enforced by PostgreSQL role permissions)
2. **Auth flexibility:** Apps can use any authentication model (OAuth, magic link, federated identity, etc.) without forcing PIN-based hub auth
3. **Deployment independence:** Isolated apps can be deployed, restarted, or updated independently without affecting hub or other ERP PWAs
4. **User experience:** Different user populations (employees vs. patients vs. clients) see custom-built interfaces tuned to their domain
5. **Compliance & privacy:** Health data stays in a separate database with separate audit logs; GDPR/HIPAA-style data residency easier to enforce
6. **Backup strategy:** Independent backup schedules reduce blast radius if a restore is needed
7. **Reusability:** The same infrastructure can host multiple isolated applications without refactoring hub or shared schemas

### Trade-Offs

1. **Operational complexity:** Each isolated app adds a new Docker container, backup script, and deploy procedure to manage
2. **Code duplication (possible):** Shared helpers (e.g., date formatting, number formatting per ADR-071) may not be used across isolated apps; utilities may need to be re-implemented or vendored
3. **API access (future):** If a future ERP PWA needs to read from an isolated app (e.g., hub dashboard pulling health metrics), explicit API endpoints must be built; no shortcut direct-DB queries across databases
4. **Monitoring coverage:** Each app needs separate health checks; no unified app-health dashboard across all services

### Risks and Mitigations

| Risk | Mitigation |
|---|---|
| Isolated app backup fails silently; no copy exists | Monitor `/var/log/[app_name]-backup.log` daily; set up cron alert on non-zero exit code; verify backup files exist and are non-empty |
| PostgreSQL role privilege escalation or grant-injection breach isolation | Audit role grants quarterly via `\dp` in psql; document any cross-database grants as explicit exceptions in an ADR; use parameterized queries (PostgREST + RPC) — no string concatenation |
| Isolated app runs out of disk space (database bloat) | Set up filesystem monitoring (nagios, Prometheus); document table vacuum/reindex procedures in app's runbook |
| Isolated app is deployed with hardcoded secrets (API keys, JWT secret) in `index.html` | Enforce `.env` file use via compose; require code review before merge; add pre-commit hook to detect plaintext secrets in source files |
| Hub or shared infra changes break isolated app's Traefik routing | Test Traefik labels in isolated app's docker-compose.yml before deploying; verify routing with curl after each hub update |

## Related Decisions

- **ADR-012 (Hub as SSO Gateway):** Hub only authenticates 360lm employees (PIN auth). Isolated PWAs use their own auth models.
- **ADR-015 (Dev/Prod Two Stacks Same VPS):** Isolated PWAs follow the same physical infrastructure model (single VPS, multiple services) but with stronger data isolation via separate database roles.
- **ADR-069 (Production Hotfix Protocol):** Isolated apps adapt the hotfix protocol: bugfix tested in app's dev environment, merged to app's master branch, deployed via app-specific hotfix script.
- **ADR-073 (Traefik Docker Labels):** Isolated PWAs use the same Traefik label conventions and middleware chaining as ERP PWAs.
- **ADR-074 (PostgREST Accept-Profile Header):** Isolated PWAs can use PostgREST with a single schema (no need for multi-schema routing via headers).
- **ADR-076 (Mobile-First Viewport Standard):** Isolated PWAs are still mobile-first; viewport and meta tags apply universally.
- **ADR-078 (Paperclip Isolation):** Paperclip is an isolated service with the same role-based isolation pattern; this ADR extends that pattern to isolated PWAs.
- **ADR-079 (Shared Helper Governance):** Isolated PWAs may or may not adopt shared helpers from `/shared/`; decision is per-app based on domain relevance.
- **ADR-082 (Dark Mode & Theming):** Isolated PWAs are exempt from forced-light mode rule (applies only to ERP PWAs).
- **ADR-067 (Cross-PWA Change Safety Gate):** Isolated PWAs are separate from the cross-PWA safety gate (no shared infrastructure change required).

## References

- **Memory:** `/root/.claude/projects/-var-www-360lm/memory/health_tracker.md` — Health Tracker live status, DB schema, OCR chain, Playwright spec
- **Memory:** `/root/.claude/projects/-var-www-360lm/memory/infra_vps.md` — VPS infrastructure, Docker network, Traefik setup
- **Memory:** `/root/.claude/projects/-var-www-360lm/memory/paperclip.md` — Paperclip isolation pattern (database role, zero lm360 access, independent backups)
- **Implementation:** `/var/www/Others/health/` — Health Tracker source code and deployment config
- **Related ADRs:** ADR-012, ADR-015, ADR-069, ADR-073, ADR-074, ADR-078

---

**Decision maker:** hkl  
**Changed via:** adr-kit (360lm)  
**Date:** 2026-06-27  
**Last reviewed:** 2026-06-27
