← All tests

SSO launchpad fix PROD

2026-05-08 on prod. Open issue from Narong end-of-day 2026-05-08: "the SSO launchpad doesn't work." Recon proved the launchpad SPA is fine (2 cards, 200 on /me). Real bug: clicking the NIX Outdoor Sales tile bounced back to the launchpad. Tenant owners created via Cpanel / seed skip both invite paths that create the outdoor_employees bridge row, so Outdoor's SSO middleware fails the lookup, falls through to passport-jwt, returns 401, and the SPA redirectToCommerceLogins — Commerce sees the user is already authed and bounces them back to / (launchpad). Pure flicker symptom. Not R8 fallout — pre-existing bug Narong only just noticed.

Fix shipped + 8/8 prod test green. Lazy-create the bridge in tryNixSessionCookie, gated to active outdoor_sales sub + (owner role OR explicit nix_user_product_roles[outdoor_sales]). Race-safe via ON CONFLICT (tenant_id, email) DO UPDATE SET tenant_user_id — also covers the "row exists by email but bridge column NULL" backfill case.

Commits

69b2fd2 (backend) — fix(backend SSO): lazy-create outdoor_employees bridge on /auth/me probe so launchpad Outdoor card works for owners

Files:
  src/user/user.dao.ts
    + UserDAO.ensureBridgeForTenantUser(tenantUserId)
  src/authentication/auth.middleware.ts
    + tryNixSessionCookie calls ensureBridge when findByTenantUserId returns null
    + logger.info trace on auto-create

No migration. No schema change. Cafe / Commerce / Outdoor frontends untouched.
Failure mode unchanged from today (any short-circuit returns null → falls through
to passport-jwt → 401), so deploy is low-risk for users who never hit the new path.

Regression sweep

SuiteResultNotes
test-launchpad-fix-prod.mjs (this fix)8/8Pre + post DB checks + Pro + Starter login/auth/outdoor
test-r7-prod.mjs14/14Solo retry — first parallel run hit R8.2 single-session enforcement (parallel logins as narongix kicked each other)
test-r8-prod.mjs4/4Auth/security trio still green
test-phase1-prod.mjs11/11Solo retry — same R8.2 reason as R7
test-phase2-sso-outdoor-prod.mjs6/6SSO bridge regression green from first run
test-phase2-cafe-multishop-prod.mjs5/6PRE-EXISTING — see follow-up below

48/49 effective — only failure is unrelated to this fix (see below).

NEW LESSON — solo-retry policy now has a concrete cause. R8.2 single-session-per-user enforcement (shipped today) means any prod test that re-logs as narongix@gmail.com rotates the JWT sid and invalidates every other concurrent test holding the prior cookie. The "first-parallel-hit flakes, solo retry green" pattern that's been in the playbook since phase4 is now guaranteed, not transient: node test-phase4 & + node test-r7 & sharing get-coffee creds will mutually kick each other every time. Action item: regression sweeps should run sequentially when sharing creds, or use distinct-tenant test accounts. Update project_session_workflow_detailed.md Step 12.
PRE-EXISTING follow-up — multishop "shop selector hidden ≤1 shops" fails on demo. The dashboard page (app/(authed)/dashboard/page.tsx:80-85) renders its own <ShopSelector> unconditionally — gated only on shopCtx, not on shops.length > 1 || isStarter like the layout-level selector does. Demo has 1 shop on cafe_pro, so the layout selector hides correctly, but the page-level one doesn't, and the test catches the page-level button. Likely introduced by R7's dashboard refactor (commit f616460). Backend-only fix in this commit can't have caused this — surface for next session as a 5-line gate fix.

Lazy-create gating logic

ensureBridgeForTenantUser(tenantUserId):
  1. tenant_users.is_active=true                       — disabled users get null
  2. subscriptions WHERE product_code='outdoor_sales'
       AND status IN ('active','trialing')             — no sub → null
  3. tenant_users.role='owner'                         — implicit access (matches Cafe / Commerce)
     OR nix_user_product_roles for outdoor_sales       — explicit access
  4. INSERT outdoor_employees ON CONFLICT (tenant_id, email)
       DO UPDATE SET tenant_user_id = excluded.tenant_user_id
  5. Return findByTenantUserId(tenantUserId)

Failure mode is identical to today: any short-circuit returns null, the
middleware falls through to passport-jwt, and a Bearer-less request 401s.
No regression risk for users who never hit the new path.
PRO 1Launchpad after login
Two product cards (NIX Cafe + NIX Outdoor Sales). Welcome header shows narongix.
PRO 2/outdoor/ destination
Pre-fix: bounced back to launchpad (URL=tenant root). Post-fix: lands on Outdoor's authed route.
STARTER 1Launchpad after login
Welcome shows Sokha (lumiere owner full_name).
STARTER 2/outdoor/ destination
Same — no longer bounces; loads the Outdoor SPA.

Probe output

loading…