TanStack Router file-based routes:
- vite.config.ts: TanStackRouterVite plugin (target: react,
autoCodeSplitting: true) before the React plugin.
- .gitignore: src/routeTree.gen.ts (auto-generated).
- src/lib/query-client.ts: module-level QueryClient singleton.
- src/routes/__root.tsx: root route, QueryClientProvider, Suspense-
wrapped router + query devtools (dev-only via import.meta.env.DEV).
- src/routes/login.tsx: public route. validateSearch schema accepts
optional redirect. LoginRoute wraps <LoginPage> with onAuthenticated
navigating to redirect ?? '/'.
- src/routes/_authed/route.tsx: pathless protected layout. beforeLoad
redirects to /login on 'anonymous'; falls through on 'unknown' /
'authenticating' so hard reload doesn't flash the login page.
- src/routes/_authed/index.tsx: placeholder home with sign-out button.
Cross-tab logout effect bounces to /login on store transition.
- src/App.tsx: replaced — now creates the router and renders
<RouterProvider />. The interim status-branching logic from 1.6 is
gone (the route tree handles it).
- eslint.config.js: override for src/routes/** disabling
react-refresh/only-export-components — TanStack Router's file-based
pattern intentionally co-exports Route alongside components.
Code-splitting working: login chunk 37KB, home + auth 17KB, card
primitive 31KB, all loaded lazily; main bundle 353KB.
Deviations:
1. No top-level useRequireAuth hook — per-page useEffect on store
transitions is just as effective and only used in one place.
2. ESLint override for src/routes/** matches the existing pattern for
src/ui/primitives/**.
3. Login page's auto-navigate effect from 1.6 is removed — the route
wrapper owns navigation via onAuthenticated.
src/ui/pages/login.tsx — LoginPage component:
- Centred card on muted background.
- shadcn Form + FormField with react-hook-form + zodResolver.
FormMessage renders field-level zod errors automatically.
- Email + password with proper autocomplete attrs for password managers.
autoFocus on email.
- Submit calls useAuthStore.getState().login(); errors render in a
destructive Alert above the form.
- In-flight via the auth store's 'authenticating' status (cross-tab safe).
- onAuthenticated callback fires on store transition to authenticated;
1.7 wires this to a router redirect.
src/App.tsx branches on auth status:
- unknown / authenticating -> "Signing you in..." placeholder
(avoids flashing the login page on hard refresh while the boot probe
runs)
- anonymous -> <LoginPage />
- authenticated -> home placeholder (1.7 replaces with router shell)
Deviation: skipped src/routes/login.tsx — requires the router plugin
to have generated routeTree.gen.ts, which is 1.7's territory. The page
works standalone via App.tsx's status branch.
Search-param redirect (?redirect=...) also deferred to 1.7 — no router
yet to expose useSearch. onAuthenticated callback is the seam.
Six files under src/auth/:
- client.ts: createDirectus<Schema>().with(authentication('cookie', ...))
.with(rest(...)). Lazy singleton via initDirectusClient(url) +
getDirectus() + useDirectus() (React-side helper). DirectusUser type;
empty Schema (grows in Phase 2+).
- store.ts: Zustand store with discriminated AuthState
(unknown / anonymous / authenticating / authenticated). Actions:
initialize, login, logout, setUser. humanizeAuthError maps Directus
error codes (INVALID_CREDENTIALS / INVALID_OTP / USER_SUSPENDED) to
user-facing strings.
- guard.ts: useRequireAuth (redirect to /login on anonymous) and
useRequireRole (bounce to / on role mismatch). Uses TanStack Router's
useNavigate; effective once 1.7 wires the router.
- bootstrap.tsx: <AuthBootstrap> component runs initDirectusClient +
initialize() once after the runtime config is loaded.
- index.ts: barrel re-exports.
main.tsx wraps App in <RuntimeConfigProvider><AuthBootstrap>...
App.tsx shows the auth status as a smoke indicator before 1.6/1.7.
Deviations:
1. Split getDirectus into initDirectusClient(url) + getDirectus()
because the runtime config is a React context (per 1.4), not a
Zustand store. <AuthBootstrap> bridges from React land to the non-
React getter.
2. Added <AuthBootstrap> as a discrete component rather than putting
the bootstrap effect in App.tsx — App.tsx gets replaced by the
router in 1.7, but bootstrap needs to keep running.