diff --git a/src/components/layout/auth-shell.tsx b/src/components/layout/auth-shell.tsx
index f9948a742..e8a56cb95 100644
--- a/src/components/layout/auth-shell.tsx
+++ b/src/components/layout/auth-shell.tsx
@@ -19,6 +19,7 @@ import { DemoBanner } from "./demo-banner";
import { OfflineBanner } from "./offline-banner";
import { SharedRecordBanner } from "./shared-record-banner";
import { SharedRecordUnavailable } from "./shared-record-unavailable";
+import { RecordScopeHydrationGate } from "./record-scope-hydration-gate";
import { SidebarNav } from "./sidebar-nav";
import { TopBar } from "./top-bar";
@@ -225,17 +226,12 @@ export function AuthShell({
);
}
- // Admin pages stay behind the auth gate while `/api/auth/me` is in
- // flight: the role is unknown until the payload lands, and mounting
- // `/admin/*` children early would fire admin queries that 403 for a
- // non-admin before the redirect effect can move them away.
- if (isLoading && (isAdminPage || pathname.startsWith("/settings"))) {
- return (
-
-
- {t("nav.loadingScreen")}
-
- );
+ // Every protected route waits for `/api/auth/me`, not only Settings and
+ // Admin. The payload resolves the active record as well as identity, so a
+ // child mounted before it could issue an actor-scoped read before the shell
+ // knows that a switched record must be refused or scoped differently.
+ if (isLoading) {
+ return ;
}
// Auth RESOLVED as unauthenticated — hold a spinner while the
@@ -249,16 +245,6 @@ export function AuthShell({
);
}
- // While `/api/auth/me` is still in flight the shell renders the full
- // app chrome + children immediately (the chrome components own their
- // null-user skeletons, pages own their data skeletons). This takes the
- // auth round-trip off the first-paint critical path: page-level
- // queries fire in parallel with `/api/auth/me` instead of behind it.
- // `src/proxy.ts` has already refused cookie-less requests to
- // protected routes, so the unauthenticated-flash window is limited to
- // expired/invalid sessions — those resolve into the redirect branch
- // above as soon as the 401 lands.
-
// Onboarding page — minimal shell, no sidebar/nav
if (isOnboardingPage) {
return (
diff --git a/src/components/layout/record-scope-hydration-gate.tsx b/src/components/layout/record-scope-hydration-gate.tsx
new file mode 100644
index 000000000..d4595c9d0
--- /dev/null
+++ b/src/components/layout/record-scope-hydration-gate.tsx
@@ -0,0 +1,19 @@
+import { Loader2 } from "lucide-react";
+
+/**
+ * Holds protected route trees until `/api/auth/me` has resolved the active
+ * record and its capabilities. Mounting a child before this point can issue a
+ * request for the actor's record before the shared-record scope is known.
+ */
+export function RecordScopeHydrationGate({ label }: { label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}