Files
2026-07-12 10:17:17 -04:00

3.9 KiB

Same SPA Build, Different Behavior Per Domain

Scenario

Same build (single dist/ folder, same nginx server block) serves multiple domains. Feature works on domain A but fails on domain B with a Zod validation error like "Invalid input." No code or API differences exist.

Investigation Checklist

Before touching any code:

Build layer
├── Same dist/ folder? → check nginx `root` directive
└── Same .env baked in? → check VITE_ prefixed vars at build time

Runtime layer
├── Same API instance? → check proxy_pass targets
├── Same user data? → query the API for same record
└── Same auth token? → check cookies/localStorage

Client layer
├── localStorage (per origin) ← MOST COMMON CULPRIT
├── sessionStorage (per tab)
├── IndexedDB (per origin)
├── Service worker cache (per origin)
└── Cookies (per origin + path)

Common Root Cause: Stale Persisted Client State

App version N persisted a specific state shape to localStorage (Zustand persist middleware, Redux persist, or raw localStorage.setItem). Version N+1 changed the schema — added required fields, changed enum values, removed fields. The browser on domain B still has the old shape rehydrated, and it fails Zod validation.

Diagnosis Steps

1. Confirm code is identical across domains:

# Check nginx config — same root for all server_names?
cat /etc/nginx/sites-enabled/your-site | grep -E "server_name|root "

2. Check API — same backend?

Same proxy_pass target for all domains.

3. Trace the validation path in the failing handler:

// Look for schema validation like this:
const parsed = quoteWriteSchema.safeParse(data);
if (!parsed.success) {
  console.warn('Validation failed:', JSON.stringify(parsed.error.issues, null, 2));
  showToast(parsed.error.issues[0].message, 'error');
}

4. On the failing domain, open DevTools Console → trigger the action. Look for the validation warning — it names the exact field that's failing:

Quote schema validation failed: [{"code":"invalid_enum_value","path":["discountType"],...}]

5. Check localStorage for the persisted state key:

DevTools → Application → Local Storage → failing-domain.com → find the Zustand/Redux persist key (e.g., spq-quote).

Fields Most Likely to Fail with Stale State

Field type Schema rule Failure symptom
z.enum([...]) Required enum, no default "Invalid enum value" / "Invalid input"
z.string().min(1) Required non-empty "String must contain at least 1 character(s)"
z.number() Expected number, was string "Expected number, received string"
z.array().min(1) Non-empty array "At least one service is required"
Nested object New field added post-release "Required" on the new field

Fix (Short-term)

Clear localStorage for the failing domain:

localStorage.removeItem('spq-quote')  // or whatever the persist key is

Fix (Long-term — code)

Make the schema forward-compatible so old persisted data doesn't fail:

  • Use .optional().default('') or .optional().default(someValue) on fields added after initial release
  • Add a migration in the persist middleware's onRehydrateStorage callback
  • Strip nulls and fill defaults before Zod validation (like sanitizeServices())
  • Use z.union([z.string(), z.undefined()]).optional().default('') for fields that may be missing

Anti-patterns

  • Modifying source code thinking domains have different builds — always check nginx first
  • Adding environment-specific toggles — the code IS identical, the difference is client-side
  • Blaming the API — both domains hit the same backend instance

The correct first step: check nginx config → confirm same build → check localStorage on both domains → reproduce the validation failure in the console → read the Zod error path to identify which field is stale.