Mersi

State Management

Zustand stores, TanStack Query keys, Ky client configuration, and required environment variables.

The app combines Zustand for client-owned UI state with TanStack Query for server-owned data. The Ky HTTP client handles all REST requests through the Next.js /api proxy.

Zustand Stores

StoreFilePersistenceWhat it holds
useSessionStorelib/session-store.tslocalStorage (purch-session)sessionId, clientId, _hasHydrated flag
useCartStorelib/cart-store.tsNone (hydrated from backend on mount)CartItem[], isOpen
useOrdersStorelib/orders-store.tsNoneisOpen (sidebar visibility only)
useProductDetailStorelib/product-detail-store.tsNoneproduct: Product | null (detail sheet)

useSessionStore

Persisted with zustand/middleware/persist. On rehydration, clientId is initialised to a new crypto.randomUUID() if not already set, and _hasHydrated is set to true. The chat page waits for _hasHydrated before attempting to load a session.

useCartStore

Not persisted. The hydrate(items) action merges incoming backend items with any pending local items (those without a backendId) so optimistic additions made before the hydration response are not lost.

useProductDetailStore

Opened by product cards inside ChatMessage tool-result renders. ProductDetailSheet reads product from this store and renders a slide-in sheet with images, price, and add-to-cart action.

TanStack Query

QueryClient is created in app/providers.tsx with a global staleTime of 30 000 ms.

Query keyHookAPI callUsed by
['sessions']useListSessionsGET /api/sessions?limit=100ChatSidebar
(invalidated via useInvalidateSessions)ChatPage after session creation
['onboarding-status']useOnboardingStatusGET /api/onboarding/statusLogin page redirect, onboarding page
['orders'] (or similar)Per-component queryGET /api/ordersOrdersSidebar

Mutations for onboarding steps use useMutation from lib/api/onboarding.ts.

Ky HTTP Client — lib/api/client.ts

export const apiClient = ky.create({
  prefixUrl: '/api',      // all requests route through the Next.js rewrite proxy
  credentials: 'include', // session cookies forwarded automatically
  hooks: {
    beforeError: [
      async (error) => {
        // Redirect to /onboarding when backend returns { code: 'ONBOARDING_INCOMPLETE' }
      },
    ],
  },
})

All functions in lib/api/ import apiClient rather than constructing fetch calls directly. The chat send path is the only exception — it calls fetch('/api/chat', ...) directly to get access to the raw ReadableStream for SSE parsing.

Backend Proxy

next.config.ts rewrites all /api/:path* requests to BACKEND_URL (or NEXT_PUBLIC_API_URL), falling back to a default Railway URL.

// next.config.ts
rewrites() {
  return [{ source: '/api/:path*', destination: `${BACKEND}/api/:path*` }]
}

Environment Variables

Prop

Type

Copy frontend/.env.example to frontend/.env.local and fill in NEXT_PUBLIC_CROSSMINT_API_KEY and NEXT_PUBLIC_API_URL before running locally.

How is this guide?

On this page