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.
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.
outdoor_employees row.narongix@gmail.com oe_id=8, owner@lumiere-coffee.com oe_id=9 (also retroactively fixed owner@demo.com oe_id=10 via the test run on demo).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.
| Suite | Result | Notes |
|---|---|---|
| test-launchpad-fix-prod.mjs (this fix) | 8/8 | Pre + post DB checks + Pro + Starter login/auth/outdoor |
| test-r7-prod.mjs | 14/14 | Solo retry — first parallel run hit R8.2 single-session enforcement (parallel logins as narongix kicked each other) |
| test-r8-prod.mjs | 4/4 | Auth/security trio still green |
| test-phase1-prod.mjs | 11/11 | Solo retry — same R8.2 reason as R7 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 | SSO bridge regression green from first run |
| test-phase2-cafe-multishop-prod.mjs | 5/6 | PRE-EXISTING — see follow-up below |
48/49 effective — only failure is unrelated to this fix (see below).
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.
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.
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.
loading…