> Part of the PWA DevGuide (split from pwa_dev_style.md on 2026-07-02 — see that file for the index; ADR-099).

## 23. Client Portal Session Bridge Pattern

Pattern for cross-PWA authentication when a login portal (e.g. `/client/`) needs to hand off a session to a destination PWA (e.g. `/activity/`) without the destination needing its own login form.

### 23.1 Problem

Activity PWA needs to show activity data to clients. But the Activity PWA's own login was simplified to redirect-only. The client logs in at `/client/` instead — so how does Activity PWA recognise the client after login?

**Answer:** Both PWAs are on the same origin (`srv1111289.hstgr.cloud`), so they share `localStorage`. `/client/` writes the exact session key that `/activity/` reads, then redirects there. No tokens, no server session — plain localStorage sharing.

### 23.2 Session Keys Involved

| Key | Written by | Read by | Format | TTL |
|-----|-----------|---------|--------|-----|
| `lm360-activity-session` | Activity PWA login (old) · `/client/` company-code login (new) | Activity PWA `restoreSession()` | `{id:'client:X', name, company, phone, role, type:'client', activityId, clientAccessId, ts}` | 8 hours |
| `lm360-client-session` | `/client/` PIN login | `/client/` `restoreSession()` | `{id, displayName, contactName, loginAt}` | 30 days |
| `lm360-session` | Hub PWA | Activity PWA `restoreSession()` | `{empId, name, role, loginAt}` | 12 hours |

### 23.3 Login Flow (Company Code → Activity)

```
User opens /activity/ (no session)
  └── restoreSession() finds nothing
  └── shows redirect screen

User taps "Client Access" button
  └── browser navigates to /client/?next=/activity/

/client/ company-code login succeeds
  └── loginWithCode() writes lm360-activity-session with {id:'client:X', ..., ts:Date.now()}
  └── reads ?next= param from URL
  └── window.location.href = '/activity/'   (or ?next= value)

/activity/ loads
  └── restoreSession() finds lm360-activity-session (written seconds ago, within 8h TTL)
  └── session = parsed object (type:'client')
  └── showHome() → loadActivities() → fetches activityId from session
```

### 23.4 ?next= Redirect Pattern

Any portal PWA that can send users to another PWA after login should support `?next=`:

```js
// After successful login, before navigating home:
const next = new URLSearchParams(location.search).get('next');
if (next && next.startsWith('/')) {   // safety: only allow relative paths
  window.location.href = next;
  return;
}
await enterClientHome();   // default: show portal home
```

**Security note:** Always validate `next` starts with `/` to prevent open redirects to external URLs.

### 23.5 Redirect-Only Login Screen Pattern

When a PWA's login is delegated to another PWA, replace the login form with redirect buttons:

```html
<div id="scr-login" class="scr active">
  <div class="login-box">
    <div class="login-logo">...</div>
    <p style="text-align:center;color:var(--muted);font-size:.85rem;margin-bottom:24px">Sign in to continue</p>
    <a href="/hub/"
       style="display:block;text-align:center;padding:14px;background:var(--teal);color:#fff;
              border-radius:var(--radius);font-size:1rem;font-weight:600;text-decoration:none;margin-bottom:12px">
      👤 Employee Login
    </a>
    <a href="/client/?next=/activity/"
       style="display:block;text-align:center;padding:14px;background:var(--bg2);color:var(--text);
              border:1px solid var(--border);border-radius:var(--radius);font-size:1rem;font-weight:600;text-decoration:none">
      🏢 Client Access
    </a>
  </div>
</div>
```

`restoreSession()` still auto-logins from hub session (`lm360-session`) and activity session (`lm360-activity-session`) — no changes needed to session restore logic.

### 23.6 Two Parallel Client Systems (360LM specific)

The 360LM platform has two separate client identity systems that must NOT be confused:

| System | Tables | Auth | Session key | Used by |
|--------|--------|------|-------------|---------|
| **Portal clients** | `client.accounts` + `client.access_grants` | ID + 6-digit PIN | `lm360-client-session` | `/client/` home, responses viewer |
| **Activity clients** | `activity.companies` + `activity.client_access` | company code + individual access code | `lm360-activity-session` (type:'client') | `/activity/` |

Portal clients see responses inside `/client/` (no need to navigate to `/activity/`).
Activity clients (company-code) get redirected to `/activity/` with a bridged session.

These two systems are intentionally separate and serve different use cases. The company-code system is lighter (no account creation, just access codes distributed by admin).

---

