A focused audit of every Anything Engine BFF wrapper that touches user state. Two of them hardcoded Robert's WorkOS user id "15", silently attributing Mark's dogfood writes to Robert's account. The bug was masked because both dogfooded from the same dev environment. This is the full forensic.
Two files in the codebase had user_id: "15" hardcoded — that’s Robert’s WorkOS id. /chat dispatched with it; /upload-files form-appended it. Anything Mark did from those code paths got tagged to Robert in Xano. Per-user memory continuity was structurally broken in the chat-shell + deck-upload subsystems.
The W13 SANDBOX-PARITY-AUDIT (May 11) flagged that the canvas was sending user_id: "" to ep 8506 — meaning Zep memory was session-ephemeral per page-load, not per user. That was the entry point. The fix wired useAuth().user.id into the canvas dispatch payload (commit 64a847c).
That alone wasn’t the bug. The audit prompted a wider sweep across all 9 AE BFF wrappers to find sister sites that needed the same wire-up. Halfway through that sweep:
// chat-shell.tsx, line 1495 — the gut punch const result = (await dispatchAction({ data: { query, thread_id: threadIdRef.current, user_id: "15", // ← Robert's WorkOS id, hardcoded ...(classOverride ? { class_override: classOverride } : {}), count: requestedN, ...(suggestionRequestIdRef.current ? { suggestion_request_id: suggestionRequestIdRef.current } : {}), ...(toolCalls ? { tool_calls: toolCalls } : {}), }, })) as DispatchResult;
Then later, deeper in the upload pipeline:
// upload-files-fn.ts, line 67 — the second one const form = new FormData(); form.append("suggestion_request_id", String(data.suggestion_request_id)); form.append("user_id", "15"); // ← same id, different file form.append("mode", "suggestion_request"); for (const f of data.files) { const bytes = Uint8Array.from(atob(f.base64), (c) => c.charCodeAt(0)); const blob = new Blob([bytes], { type: f.mime_type }); form.append("files[]", blob, f.filename); }
Both Robert and Mark dogfooded the app from the same machine, both authenticated as their own WorkOS users. The frontend showed each of them their own UI — auth worked correctly. But the dispatch payload to Xano carried Robert’s id regardless of who was logged in. Backend processing then attributed memory writes / suggestion_request rows / uploaded decks to Robert. Mark would never see his own data because his data didn’t exist on his account.
The fix is the same shape everywhere: import useAuth from the WorkOS adapter, read user.id with an empty-string fallback, pass it through. The empty-string fallback preserves dev-without-auth behavior and keeps existing test setups working.
import { useAuth } from "@workos/authkit-tanstack-react-start/client"; // inside the component: const { user } = useAuth(); const authedUserId = user?.id ?? ""; // every BFF call: const result = (await dispatchAction({ data: { query, thread_id: threadIdRef.current, user_id: authedUserId, // ← from useAuth, not hardcoded ... }, })) as DispatchResult;
useCallback dependency arrays were updated everywhere this lands so React doesn’t close over a stale id when a different user signs in.
Every Anything Engine BFF wrapper, what Xano endpoint it hits, whether it touches user state, and the user_id status before / after this audit.
| BFF wrapper | Xano ep | Touches user state? | Before | After |
|---|---|---|---|---|
| openui05DispatchFn | 8506 (find-X) | Zep memory write per dispatch | × | ✓ |
| dispatchFn (legacy) | 8399 | Zep memory write per dispatch | × (hardcoded "15") | ✓ |
| interviewFn | 8411 | Zep memory write per turn | × | ✓ |
| findTalentInterviewFn | 8484 | Zep memory write per turn | × | ✓ |
| startOutcomeFn | 8417 | Creates suggestion_request row | × | ✓ |
| uploadFilesFn | 8420 | Creates suggestion_request_file + pitch_profile | × (hardcoded "15") | ✓ |
| classifyFn | 8400 | Stateless (no DB write) | N/A | N/A |
| buildSummaryFn | 8505 | Synthesis only (no DB write) | N/A | N/A |
| pitchProfileFn | 8420 (GET) | Read-only, keyed on suggestion_request_id | N/A | N/A |
All 6 BFFs that touch user state pass authenticated WorkOS user.id from useAuth(). The 3 stateless BFFs (classify, buildSummary, pitchProfile) don’t need it.
Xano discards unknown body keys, so sending user_id on endpoints that don’t yet accept it is a safe extension. Mark wires the server-side handlers at his pace; when they land, every call from the frontend will already be sending the right id.