Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md
T
Lucas Berger 1f8775e7b6 docs(03-08): scaffold Gate 2 results + record production build status
- PWA build: CLEAN — 1818 modules, sw.js + workbox generated (build SHA 40dfbb4)
- API build: CLEAN — tsc passed, no errors
- 03-GATE2-RESULTS.md created with deploy header (URL TBD), full Gate 2 checklist
  (all rows marked PENDING — operator/device), and operator-setup section covering:
  Authelia OIDC client registration, OIDC_AUTH_EXTERNAL_URL, Pangolin/Newt Mode A rig,
  DB schema push, and /health tunnel verification
2026-06-05 18:51:37 -04:00

8.5 KiB

Phase 3 Gate 2 — Live Verification Results

Header

Field Value
Deploy URL TBD — operator must configure (see Operator Setup below)
Build SHA 40dfbb4
Build date 2026-06-05T22:50:16Z
PWA build CLEAN — 1818 modules, dist/sw.js + workbox generated
API build CLEAN — tsc passed, no errors

Operator Setup Required Before Gate 2

The following steps require operator credentials/access and cannot be automated by the executor. Complete all steps before proceeding to the checklist below.

1. Register FamilySync as an Authelia OIDC confidential client

Generate a hashed client secret:

authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72
# Record BOTH the plaintext (for OIDC_CLIENT_SECRET) and the hash (for Authelia config).

Add to Authelia configuration.yml under identity_providers.oidc.clients:

identity_providers:
  oidc:
    clients:
      - client_id: 'familysync-dev'          # Use 'familysync' for Unraid prod (Mode B)
        client_name: 'FamilySync'
        client_secret: '$pbkdf2-sha512$...'  # The HASH from the command above
        public: false
        authorization_policy: 'one_factor'
        redirect_uris:
          - 'https://familysync-dev.DOMAIN/callback'  # Replace DOMAIN; Mode B: familysync.DOMAIN
        scopes: [openid, profile, email]
        response_types: [code]
        grant_types: [authorization_code, refresh_token]
        token_endpoint_auth_method: client_secret_basic
        require_pkce: true
        pkce_challenge_method: S256

Reload Authelia: docker restart authelia (or your reload mechanism).

2. Set OIDC_AUTH_EXTERNAL_URL in the app's .env

OIDC_AUTH_EXTERNAL_URL is mandatory behind Pangolin. Without it, @hono/oidc-auth builds redirect_uri from the internal container hostname, which will not match the registered URI and will cause a 400 from Authelia.

OIDC_AUTH_EXTERNAL_URL=https://familysync-dev.DOMAIN   # Mode A test rig
# (Mode B: https://familysync.DOMAIN)
OIDC_CLIENT_ID=familysync-dev
OIDC_CLIENT_SECRET=<plaintext from the crypto hash step>
OIDC_REDIRECT_URI=https://familysync-dev.DOMAIN/callback

Also ensure:

  • NODE_ENV=production is set in the container — this forces devBypassActive=false in apps/api/src/index.ts, mounting the OIDC guard unconditionally.
  • DEV_AUTH_BYPASS is absent (or unset) from the production environment block. Even if accidentally present, NODE_ENV=production suppresses it at the first conditional in devBypass.ts, but leave it out to keep the config unambiguous.

3. Expose via Pangolin / Newt (Mode A local rig)

# Run Newt on your dev box pointing at the Pangolin site token issued for this host:
docker run -d --name newt --restart unless-stopped \
  -e PANGOLIN_ENDPOINT=https://pangolin.DOMAIN \
  -e NEWT_ID=<site-id> -e NEWT_SECRET=<site-secret> \
  fosrl/newt:latest

In Pangolin, create a route:

  • Host: familysync-dev.DOMAIN
  • Upstream: http://<api-host>:3000
  • Pangolin's own auth: OFF — FamilySync does Authelia OIDC at the app layer.
  • Response buffering: OFF; idle/read timeout: >= 120s (required for SSE).

4. Apply database schema (first deploy only)

docker compose up -d mariadb
DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=familysync DB_NAME=familysync DB_PASSWORD=<value> \
  pnpm --filter @familysync/api exec drizzle-kit push
# Verify: SHOW TABLES; -> users, member_credentials, calendars, calendar_events

5. Bring up the app and confirm /health over the tunnel

docker compose up -d --build
# Local sanity:
curl -s http://localhost:3000/health   # expect: {"ok":true,"db":"up"}
# Through the tunnel (record this result in the checklist below):
curl -s https://familysync-dev.DOMAIN/health   # expect: {"ok":true,"db":"up"}

Update the Deploy URL at the top of this file once confirmed.


Gate 2 Checklist

Run the checklist from an external network (phone on cellular is ideal). Mark each row PASS or FAIL and add notes. On failure, apply the indicated remedy and retest.

Part A — Auth, Session, Colors (Task 2)

# Ref Check Result Notes
A1 AUTH-01 Open https://familysync-dev.DOMAIN → redirects to Authelia → login completes → land on the app with name, color, and at least one cached event [ ] PENDING — operator
A2 AUTH-02 Fully close + reopen browser → revisit the URL → no re-login prompted (session persists) [ ] PENDING — operator
A3 AUTH-03 Second member logs in on a separate device → distinct stable color assigned (different from first member's color) [ ] PENDING — operator/device

Part B — iOS PWA Standalone Login (Task 2) — LOAD-BEARING CHECK

This is the most critical row. Pitfall 2: If the OIDC redirect breaks out of standalone mode (user lands in Safari instead of the app), the non-technical member cannot log in. Confirm this passes before recording any other rows as done.

Remedy if it fails: Verify manifest.webmanifest has scope: "/" and start_url: "/"; confirm /callback is in the service worker denylist (apps/pwa/src/sw-denylist.ts) and is not intercepted by Workbox; redeploy and retest.

# Ref Check Result Notes
B1 iOS PWA Open https://familysync-dev.DOMAIN in Safari on iPhone → in-app install walkthrough appears → tap "Add to Home Screen" [ ] PENDING — device
B2 iOS PWA Launch FamilySync from Home Screen → opens full-screen with no Safari browser chrome (standalone mode) [ ] PENDING — device
B3 iOS PWA (Pitfall 2) Complete Authelia OIDC login from standalone mode → redirect does NOT break out of standalone (user stays in the app, not dropped to Safari) [ ] PENDING — device Load-bearing check
B4 PWA-01 Installed PWA on iOS opens full-screen with no browser chrome [ ] PENDING — device
B5 PWA-02 Installed PWA on Android opens full-screen with no browser chrome [ ] PENDING — device

Part C — SSE Smoke Test (Gate before Phase 4)

# Ref Check Result Notes
C1 SSE Hold stream open 5+ min without it being cut (see curl command below) [ ] PENDING — operator
# Get the session cookie from browser DevTools → Application → Cookies (oidc-auth=<value>)
curl -N -H "Cookie: oidc-auth=<value>" https://familysync-dev.DOMAIN/api/sse/heartbeat
# PASS: heartbeat event received ~every 10s for 5+ minutes
# FAIL: stream cut early → adjust Pangolin idle-timeout; if still failing, record as Phase 4 constraint

Part D — Create / Edit / Delete Fastmail Round-trips (Task 3)

# Ref Check Result Notes
D1 CAL-04 Create a timed event → "Syncing…" toast → "Saved" toast → event appears in native Fastmail app on next sync [ ] PENDING — operator
D2 CAL-07 Create an all-day event → same Syncing→Saved flow → appears in Fastmail [ ] PENDING — operator
D3 CAL-04 Create a weekly recurring event → appears in Fastmail [ ] PENDING — operator
D4 CAL-05 Edit an existing event's title and time → Syncing→Saved → change persists in Fastmail [ ] PENDING — operator
D5 CAL-06 Delete an event via the two-tap confirmation dialog → Syncing→Saved → event disappears from all views on next sync [ ] PENDING — operator
D6 D-08 (Optional) Trigger a 412 conflict by editing the same event in Fastmail first → conflict toast appears in the app → calendar re-fetches [ ] PENDING — operator Optional

/health Tunnel Verification

Record the curl result through the public URL here:

URL tested:    https://familysync-dev.DOMAIN/health
Result:        [ ] PENDING — operator
Response body: <fill in>

Summary

Section Status
Production builds (PWA + API) CLEAN (automated, 2026-06-05)
Operator infra setup PENDING
A — Auth / session / colors PENDING
B — iOS standalone login (load-bearing) PENDING
C — SSE smoke test PENDING
D — Fastmail write round-trips PENDING

Gate 2 is complete when all rows are PASS. Record final status here:

Gate 2 outcome: [ ] PENDING