Technical Architecture
Crocante is built with a modern, scalable architecture using Next.js 16, TypeScript, and React 19. This guide provides a comprehensive overview of the technical design, architectural patterns, and implementation details.
High-Level Architecture
Crocante follows a Backend-for-Frontend (BFF) architecture pattern with clear separation of concerns:
┌─────────────┐
│ Browser │
│ (Client) │
└──────┬──────┘
│ HTTP (cookies)
▼
┌─────────────────────────────────┐
│ Next.js Application (BFF) │
│ ┌──────────────────────────┐ │
│ │ Client Components │ │ Port: 3000
│ │ (React 19) │ │
│ └────────┬─────────────────┘ │
│ │ │
│ ┌────────▼─────────────────┐ │
│ │ API Routes │ │
│ │ (Proxy + Auth) │ │
│ └────────┬─────────────────┘ │
│ │ │
└───────────┼──────────────────────┘
│ HTTP + Bearer Token
▼
┌─────────────────────────────────┐
│ Backend API Gateway │
│ (External Service) │
└─────────────────────────────────┘
Key Architectural Decisions
BFF Pattern All external API calls are proxied through Next.js API routes, keeping tokens server-side and providing a security layer
Domain-Driven Design Code is organized by business domain (portfolio, staking, credit) rather than technical layers
React Query Declarative data fetching with automatic caching, background refetching, and optimistic updates
Type Safety End-to-end TypeScript with Zod schema validation for runtime type checking
Project Structure
Crocante follows a domain-driven directory structure:
/workspace/source/
├── app/ # Next.js App Router
│ ├── (dashboard)/ # Dashboard route group
│ │ ├── portfolio/page.tsx # Portfolio route
│ │ ├── staking/page.tsx # Staking route
│ │ ├── credit/page.tsx # Credit route
│ │ ├── activity/page.tsx # Activity route
│ │ ├── layout.tsx # Dashboard layout with Shell
│ │ └── loading.tsx # Loading UI
│ ├── api/ # BFF API routes
│ │ ├── auth/ # Authentication endpoints
│ │ │ ├── login/route.ts # POST /api/auth/login
│ │ │ ├── logout/route.ts # POST /api/auth/logout
│ │ │ ├── renew/route.ts # POST /api/auth/renew
│ │ │ └── session/route.ts # GET /api/auth/session
│ │ └── proxy/[...path]/ # Proxy to backend API
│ ├── layout.tsx # Root layout
│ └── page.tsx # Root page (redirects)
├── components/ # Shared UI components
│ ├── core/ # Core component library
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── modal.tsx
│ │ ├── table.tsx
│ │ └── ...
│ ├── auth/ # Authentication components
│ │ ├── auth-modal.tsx
│ │ ├── login-modal.tsx
│ │ └── register/
│ │ ├── register-modal.tsx
│ │ └── steps/ # Registration steps
│ ├── layout/ # Layout components
│ │ ├── shell.tsx # Main app shell
│ │ ├── nav-bar.tsx # Sidebar navigation
│ │ └── header.tsx # Top header
│ └── index.ts # Component exports
├── domain/ # Business domains
│ ├── portfolio/
│ │ ├── portfolio.tsx # Main portfolio component
│ │ ├── portfolio-section.tsx # Page wrapper
│ │ ├── components/ # Portfolio-specific components
│ │ │ ├── header.tsx
│ │ │ ├── asset-breakdown.tsx
│ │ │ ├── asset-allocation.tsx
│ │ │ ├── tabs-section.tsx
│ │ │ └── header-actions/ # Action modals
│ │ └── hooks/ # Portfolio business logic
│ │ └── use-portfolio-data.tsx
│ ├── staking/
│ │ ├── components/
│ │ └── hooks/
│ ├── credit/
│ │ ├── components/
│ │ └── hooks/
│ └── activity/
│ ├── components/
│ └── hooks/
├── services/ # Shared services layer
│ ├── api/ # API client
│ │ ├── http-service.ts # Axios wrapper
│ │ ├── utils.ts # Axios instances
│ │ ├── auth/ # Auth services
│ │ │ ├── login-service.ts
│ │ │ └── schemas.ts
│ │ └── errors/ # Error handling
│ ├── hooks/ # Shared React Query hooks
│ │ ├── use-user.ts # User data hook
│ │ ├── use-portfolio.ts # Portfolio data hook
│ │ ├── use-activity.ts # Activity data hook
│ │ ├── mutations/ # Mutation hooks
│ │ └── types/ # Response types
│ ├── react-query/ # Query client config
│ │ └── query-client.ts
│ └── zod/ # Schema validation
│ └── utils.ts
├── context/ # React Context providers
│ ├── providers-wrapper.tsx # Root provider wrapper
│ ├── session-provider.tsx # Session management
│ ├── toast-provider.tsx # Toast notifications
│ ├── custom-header-context.tsx # Custom header state
│ └── auth-expired-listener.tsx # Auth expiry handling
├── hooks/ # Shared custom hooks
│ ├── use-modal.ts # Modal state management
│ ├── use-is-mobile.ts # Responsive detection
│ ├── use-mounted.ts # Client-side mount detection
│ └── use-session-mode.ts # Session mode state
├── lib/ # Utility libraries
│ ├── utils.ts # General utilities
│ ├── network.ts # Network utilities
│ └── auth/ # Auth utilities
│ └── cookies.ts # Cookie management
├── config/ # Configuration
│ ├── constants.ts # App constants
│ ├── envParsed.ts # Environment variables
│ └── localStorage.ts # LocalStorage manager
└── package.json
Core Technologies
Frontend Stack
// package.json dependencies
{
"next" : "^16.1.6" , // React framework with App Router
"react" : "19.2.0" , // UI library
"react-dom" : "19.2.0" ,
"typescript" : "^5" , // Type safety
"@tanstack/react-query" : "^5.89.0" , // Data fetching & caching
"axios" : "^1.13.2" , // HTTP client
"zod" : "3.25.76" , // Schema validation
"react-hook-form" : "^7.60.0" , // Form management
"@hookform/resolvers" : "^3.10.0" , // Form validation integration
"tailwindcss" : "^4.1.9" , // Utility-first CSS
"@radix-ui/*" : "..." , // Accessible UI primitives
"lucide-react" : "^0.454.0" , // Icon library
"viem" : "^2.38.3" , // Ethereum utilities
"recharts" : "2.15.4" // Charting library
}
Next.js Configuration
// next.config.ts (simplified)
export default {
reactStrictMode: true ,
// App Router enabled by default
experimental: {
typedRoutes: true , // Type-safe routing
} ,
} ;
Authentication & Session Management
BFF Authentication Flow
Crocante implements secure authentication using the BFF pattern:
Client Login Request
User submits credentials through the login form // Login service: services/api/auth/login-service.ts:16
async login ( payload : LoginRequest ): Promise < { success : true } > {
await authApi.post( "/api/auth/login" , payload);
LocalStorageManager.setItem(LocalStorageKeys. SESSION_MODE , "real" );
window.dispatchEvent(new CustomEvent( "session-mode-changed" ));
return { success: true };
}
BFF Authenticates with Backend
Next.js API route validates credentials and calls backend // BFF login route: app/api/auth/login/route.ts:12
export async function POST ( req : NextRequest ) {
// 1. Validate request body with Zod
const parsed = LoginRequestSchema . safeParse ( body );
if ( ! parsed . success ) {
return NextResponse . json ({ error: "Invalid request" }, { status: 400 });
}
// 2. Call backend API
const loginUrl = getBackendAuthUrl ( env . API_GATEWAY , env . EP_AUTH_LOGIN );
const res = await fetch ( loginUrl , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ( parsed . data ),
});
// 3. Extract token from backend response
const data = await res . json ();
const token = data ?. data ?. token ?? data ?. access_token ;
// 4. Set HttpOnly cookie
const response = NextResponse . json ({ success: true });
setSessionCookie ( response , token ); // Cookie: session=<token>; HttpOnly; Secure
return response ;
}
Session Cookie Stored
BFF sets HttpOnly cookie that’s automatically included in subsequent requests // Cookie utility: lib/auth/cookies.ts
export function setSessionCookie ( response : NextResponse , token : string ) {
response . cookies . set ( 'session' , token , {
httpOnly: true , // Not accessible via JavaScript
secure: true , // HTTPS only
sameSite: 'strict' , // CSRF protection
maxAge: 60 * 60 * 24 // 24 hours
});
}
API Proxy Injects Token
All API calls go through BFF proxy which injects Bearer token // Proxy route: app/proxy/[...path]/route.ts (conceptual)
export async function GET ( req : NextRequest ) {
const sessionCookie = req . cookies . get ( 'session' );
const token = sessionCookie ?. value ;
// Proxy to backend with Bearer token
const backendResponse = await fetch ( backendUrl , {
headers: {
'Authorization' : `Bearer ${ token } ` ,
'Content-Type' : 'application/json'
}
});
return NextResponse . json ( await backendResponse . json ());
}
Session Provider
The session is managed by React Context:
// Session context: context/session-provider.tsx:18
type SessionContextType = {
isSignedIn : boolean ;
user : User | null ;
isLoading : boolean ;
logout : () => void ;
setToken : ( token : string ) => void ;
};
export function SessionProvider ({ children } : { children : React . ReactNode }) {
// Poll user data every 5 minutes
const { data : user , isLoading , isError } = useUser (
POLL_USER_DATA_INTERVAL // 5 minutes
);
const isSignedIn = !! user && ! isError ;
const logout = useCallback ( async () => {
await LoginService . logout ();
queryClient . removeQueries ({ queryKey: [ "user" , "me" ] });
}, []);
// Listen for auth-expired events
useEffect (() => {
const handler = () => {
LocalStorageManager . clearLocalStorage ();
queryClient . removeQueries ({ queryKey: [ "user" , "me" ] });
};
window . addEventListener ( "auth-expired" , handler );
return () => window . removeEventListener ( "auth-expired" , handler );
}, []);
return (
< SessionContext . Provider value = {{ isSignedIn , user , isLoading , logout , setToken }} >
< SessionExpiryManager />
{ children }
</ SessionContext . Provider >
);
}
Data Fetching Architecture
React Query Setup
Crocante uses TanStack React Query for all server state management:
// Query client: services/react-query/query-client.ts
import { QueryClient } from '@tanstack/react-query' ;
export const queryClient = new QueryClient ({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5 , // 5 minutes - data fresh for 5min
gcTime: 1000 * 60 * 10 , // 10 minutes - cache retention
refetchOnWindowFocus: true , // Refetch on tab focus
refetchOnReconnect: true , // Refetch on network reconnect
retry: 3 , // Retry failed requests 3 times
},
},
});
Data Fetching Hooks
Each domain has dedicated hooks for data fetching:
// Portfolio data hook: services/hooks/use-portfolio.ts:10
export function usePortfolio ( userId : string , pollInterval : number ) {
const { data : netWorthData } = useNetWorth ( userId , pollInterval );
const { sessionMode } = useSessionMode ();
return useQuery < PortfolioDataResponse >({
queryKey: [ "portfolioData" , userId ],
queryFn : async () => {
if ( sessionMode === "mock" || ! netWorthData ) {
return getMockedPortfolioData (); // Return mock data
}
return getFormattedPortfolioData ( netWorthData ); // Transform real data
},
enabled: !! netWorthData ,
staleTime: 1000 * 60 * 5 ,
refetchInterval: pollInterval , // Poll every 15 seconds
refetchIntervalInBackground: true , // Continue polling in background
});
}
// User data hook: services/hooks/use-user.ts:17
export function useUser (
pollIntervalMs : number ,
fallbackToMockOnNonAuthError = true
) {
const { EP_CLIENT } = envParsed ();
const { sessionMode } = useSessionMode ();
return useQuery < User | null >({
queryKey: [ "user" , "me" , sessionMode ],
queryFn : async () => {
// 1. Explicit mock mode
if ( currentSessionMode === "mock" ) {
return getMockedDefaultUserData ();
}
// 2. No session mode: throw 401
if ( currentSessionMode === "none" ) {
throw new ServiceError ({
message: "Not authenticated" ,
code: "NOT_AUTHENTICATED" ,
status: 401 ,
});
}
// 3. Real mode: fetch from backend
try {
const clientResponse = await getValidated < ClientResponse >(
` ${ EP_CLIENT } ` ,
clientResponseSchema
);
return getFormattedClientResponse ( clientResponse );
} catch ( err ) {
// Auth errors: bubble up for global handler
if ( err instanceof ServiceError && ( err . status === 401 || err . status === 403 )) {
throw err ;
}
// Non-auth errors: optionally fallback to mock
if ( fallbackToMockOnNonAuthError ) {
return getMockedDefaultUserData ();
}
throw err ;
}
},
meta: { authSensitive: true }, // Mark for auth error handling
staleTime: 1000 * 60 * 5 ,
refetchInterval: pollIntervalMs , // Poll every 5 minutes
refetchIntervalInBackground: true ,
refetchOnMount: "always" ,
});
}
Polling Intervals
Different data types have different polling intervals:
// Polling configuration: config/constants.ts
export const POLL_USER_DATA_INTERVAL = 1000 * 60 * 5 ; // 5 minutes
export const POLL_PORTFOLIO_DATA_INTERVAL = ( 1000 * 60 * 1 ) / 4 ; // 15 seconds
export const POLL_ACTIVITY_DATA_INTERVAL = 1000 * 60 * 5 ; // 5 minutes
export const POLL_STAKING_DATA_INTERVAL = 1000 * 60 * 5 ; // 5 minutes
export const POLL_QUOTE_INTERVAL = 1000 * 5 ; // 5 seconds
export const POLL_TOKEN_CONVERSION_INTERVAL = 1000 * 5 ; // 5 seconds
Domain-Driven Design
Each business domain is self-contained with its own components, hooks, and logic:
Portfolio Domain Example
domain/portfolio/
├── portfolio-section.tsx # Page-level wrapper
├── portfolio.tsx # Main component
├── components/ # Portfolio-specific components
│ ├── header.tsx # Portfolio header
│ ├── left-section/
│ │ ├── asset-breakdown.tsx # Asset totals
│ │ ├── currency-table.tsx # Fiat currencies
│ │ └── tokens-table.tsx # Cryptocurrencies
│ ├── right-section/
│ │ └── asset-allocation.tsx # Pie chart
│ ├── bottom-section/
│ │ ├── tabs-section.tsx # Custodian tabs
│ │ ├── banks-table.tsx
│ │ ├── custodians-table.tsx
│ │ ├── exchanges-table.tsx
│ │ ├── internal-wallets-table.tsx
│ │ └── otc-desks-table.tsx
│ └── header-actions/ # Action modals
│ ├── deposit-modal.tsx
│ ├── send-modal.tsx
│ ├── swap-modal.tsx
│ └── stake-modal.tsx
└── hooks/
└── use-portfolio-data.tsx # Portfolio business logic
// Portfolio main component: domain/portfolio/portfolio.tsx:8
import {
AssetAllocation ,
AssetBreakdown ,
Header ,
TabsSection ,
} from "@/domain/portfolio/components" ;
export default function Portfolio () {
return (
< div className = "space-y-8" >
{ /* Consolidated View */ }
< div className = "bg-card rounded-lg p-6" >
< Header />
< div className = "grid grid-cols-1 lg:grid-cols-3 gap-8" >
{ /* Left Section - Asset breakdown */ }
< AssetBreakdown />
{ /* Right Section – Asset Allocation Pie Chart */ }
< AssetAllocation />
</ div >
</ div >
{ /* Tabs and Table */ }
< TabsSection />
</ div >
);
}
Domain Hook Pattern
// Portfolio data hook: domain/portfolio/hooks/use-portfolio-data.tsx:26
export function usePortfolioData () {
const { user } = useSession ();
const userId = user ?. id . toString () || "" ;
// Fetch portfolio data with polling
const { data : portfolioData , isLoading } = usePortfolio (
userId ,
POLL_PORTFOLIO_DATA_INTERVAL
);
// Derive tokens for dropdowns
const { tokens , tokensOptions } = useMemo (() => {
if ( ! portfolioData ?. cryptocurrenciesData ) {
return { tokens: undefined , tokensOptions: [] };
}
const tokensRecord : Record < string , TokenType > = {};
const options : Array < SelectOption > = [];
portfolioData . cryptocurrenciesData . forEach (( currency ) => {
const token : TokenType = {
symbol: currency . code ,
icon: < img src ={ getTokenLogo (currency.code)} />,
};
tokensRecord [ currency . code ] = token ;
options . push ({
label: currency . code ,
id: currency . code ,
value: currency . available ,
icon: token . icon ,
});
});
return { tokens: tokensRecord , tokensOptions: options };
}, [ portfolioData ?. cryptocurrenciesData ]);
// Return formatted data for components
return {
isLoading ,
totalBalance: formatCurrency ( portfolioData ?. totalBalance ),
cryptocurrencies: formatCurrency ( portfolioData ?. cryptocurrencies ),
currencies: formatCurrency ( portfolioData ?. currencies ),
banksData: portfolioData ?. banksData ,
custodiansData: portfolioData ?. custodiansData ,
tokens ,
tokensOptions ,
// ... more derived data
};
}
Layout & Shell Architecture
App Shell Component
The app uses a consistent shell layout:
// Shell component: components/layout/shell.tsx:12
export default function Shell ({
children ,
activeMenu ,
sidebarOpen ,
setSidebarOpen ,
customAdditionalHeader ,
} : ShellProps ) {
const getPageTitle = () => {
const item = MENU_ITEMS . find (( m ) => m . id === activeMenu );
return item ?. label || "Dashboard" ;
};
return (
< div className = "flex h-screen bg-neutral-50" >
{ /* Sidebar Navigation */ }
< NavBar
activeMenu = { activeMenu }
sidebarOpen = { sidebarOpen }
setSidebarOpen = { setSidebarOpen }
/>
{ /* Main Content Area */ }
< div className = "flex-1 flex flex-col overflow-hidden" >
{ /* Top Header */ }
< Header
title = { getPageTitle ()}
customAdditionalHeader = { customAdditionalHeader }
/>
{ /* Scrollable Content */ }
< div className = "flex-1 overflow-auto bg-white" >
{ children }
</ div >
</ div >
</ div >
);
}
Dashboard Layout
// Dashboard layout: app/(dashboard)/layout.tsx:10
export default function DashboardLayout ({ children } : { children : React . ReactNode }) {
const pathname = usePathname ();
const [ customAdditionalHeader , setCustomAdditionalHeader ] = useState (<></>);
const { isOpen : sidebarOpen , setIsOpen : setSidebarOpen } = useModal ( true );
const isMobile = useIsMobile ();
// Close sidebar on mobile
useEffect (() => {
if ( isMobile ) setSidebarOpen ( false );
}, [ isMobile ]);
// Extract active menu from path
const activeMenu = pathname ?. split ( "/" ). filter ( Boolean )[ 0 ] || "portfolio" ;
return (
< CustomHeaderProvider setCustomAdditionalHeader = { setCustomAdditionalHeader } >
< Shell
activeMenu = { activeMenu }
sidebarOpen = { sidebarOpen }
setSidebarOpen = { setSidebarOpen }
customAdditionalHeader = { customAdditionalHeader }
>
{ children }
</ Shell >
</ CustomHeaderProvider >
);
}
HTTP Service Layer
All HTTP requests go through a centralized service:
// HTTP service: services/api/http-service.ts:4
import { api } from "./utils" ;
export const HttpService = {
async get < T >( url : string , config ?: AxiosRequestConfig ) : Promise < T > {
const res = await api . get < T >( url , config );
return res . data ;
},
async post < T >( url : string , body ?: unknown , config ?: AxiosRequestConfig ) : Promise < T > {
const res = await api . post < T >( url , body , config );
return res . data ;
},
async put < T >( url : string , body ?: unknown , config ?: AxiosRequestConfig ) : Promise < T > {
const res = await api . put < T >( url , body , config );
return res . data ;
},
async delete < T >( url : string , config ?: AxiosRequestConfig ) : Promise < T > {
const res = await api . delete < T >( url , config );
return res . data ;
},
};
Axios Configuration
// Axios instances: services/api/utils.ts:8
export const api = axios . create ({
baseURL: "/proxy" , // All requests go through BFF proxy
withCredentials: true , // Include HttpOnly cookies
headers: {
"Content-Type" : "application/json" ,
"Accept" : "application/json" ,
},
});
// Error interceptor
api . interceptors . response . use (
( res ) => res ,
( err ) => Promise . reject ( ServiceError . fromAxiosError ( err ))
);
// Separate instance for auth routes (no /proxy prefix)
export const authApi = axios . create ({
baseURL: "" , // Direct to BFF auth routes
withCredentials: true ,
headers: {
"Content-Type" : "application/json" ,
"Accept" : "application/json" ,
},
});
Type Safety with Zod
Runtime validation ensures type safety:
// Zod validation utility: services/zod/utils.ts
import { z } from 'zod' ;
import { HttpService } from '../api/http-service' ;
export async function getValidated < T >(
url : string ,
schema : z . ZodSchema < T >
) : Promise < T > {
const data = await HttpService . get ( url );
return schema . parse ( data ); // Validates and throws if invalid
}
// Login schema: services/api/auth/schemas.ts
import { z } from 'zod' ;
export const LoginRequestSchema = z . object ({
email: z . string (). email (),
password: z . string (). min ( 8 ),
});
export type LoginRequest = z . infer < typeof LoginRequestSchema >;
Context Providers
Global state is managed through React Context:
// Root providers: context/providers-wrapper.tsx:13
function ProvidersInner ({ children } : { children : React . ReactNode }) {
const mounted = useMounted ();
const { isOpen : isOpenAuthModal , open : openAuthModalAction , close : closeAuthModal } = useModal ( false );
return (
<>
< AuthExpiredListener onOpen = { openAuthModalAction } isOpen = { isOpenAuthModal } />
< SessionProvider >
< ToastProvider >
{ children }
</ ToastProvider >
</ SessionProvider >
{ mounted && (
< AuthModal
isOpenAuthModal = { isOpenAuthModal }
onCloseAuthModal = { closeAuthModal }
onOpenAuthModal = { openAuthModalAction }
/>
)}
</>
);
}
export default function ProvidersWrapper ({ children } : { children : React . ReactNode }) {
return (
< QueryClientProvider client = { queryClient } >
< ProvidersInner >{ children } </ ProvidersInner >
</ QueryClientProvider >
);
}
Mock Mode Implementation
Crocante includes a full-featured mock mode for development:
// Session mode management: hooks/use-session-mode.ts
export function useSessionMode () {
const [ sessionMode , setSessionMode ] = useState < SessionMode >(
LocalStorageManager . getItem ( LocalStorageKeys . SESSION_MODE ) ?? "none"
);
useEffect (() => {
const handler = () => {
const mode = LocalStorageManager . getItem ( LocalStorageKeys . SESSION_MODE ) ?? "none" ;
setSessionMode ( mode );
};
window . addEventListener ( "session-mode-changed" , handler );
return () => window . removeEventListener ( "session-mode-changed" , handler );
}, []);
return { sessionMode };
}
Mock mode features:
Full UI Access : All pages and features available
Simulated Data : Realistic portfolio, staking, and activity data
No Backend Required : Completely client-side simulation
Additional Features : Custody, Invest, Governance, Reports (not in real mode)
Error Handling
Service Error Class
// Custom error class: services/api/errors/service-error.ts
export default class ServiceError extends Error {
status : number ;
code : string ;
details ?: unknown ;
static fromAxiosError ( err : AxiosError ) : ServiceError {
return new ServiceError ({
message: err . response ?. data ?. message || err . message ,
code: err . response ?. data ?. code || "UNKNOWN_ERROR" ,
status: err . response ?. status || 500 ,
details: err . response ?. data ,
});
}
}
Auth Error Handling
// Auth expiry listener: context/auth-expired-listener.tsx
export function AuthExpiredListener ({ onOpen , isOpen } : Props ) {
useEffect (() => {
const handler = () => {
if ( ! isOpen ) onOpen (); // Show auth modal
};
window . addEventListener ( "auth-expired" , handler );
return () => window . removeEventListener ( "auth-expired" , handler );
}, [ onOpen , isOpen ]);
return null ;
}
React Query Caching
Stale Time : 5 minutes - data is considered fresh
GC Time : 10 minutes - unused data kept in cache
Background Refetching : Continues polling even when tab is inactive
Optimistic Updates : UI updates immediately, revalidates in background
Code Splitting
Next.js automatically code-splits by route:
Each page bundle is loaded on-demand
Shared components are automatically extracted
Dynamic imports for heavy components
Image Optimization
Next.js <Image> component provides:
Automatic lazy loading
Responsive images
WebP conversion
Blur placeholder support
Security Considerations
HttpOnly Cookies Session tokens stored in HttpOnly cookies, inaccessible to JavaScript
BFF Proxy All API calls proxied through Next.js, tokens never exposed to client
CSRF Protection SameSite cookie attribute prevents cross-site request forgery
Input Validation Zod schema validation on all API inputs for type safety and security
Deployment Architecture
┌─────────────────────────────────────┐
│ CDN (Vercel Edge Network) │
│ - Static assets │
│ - Image optimization │
└────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Next.js App (Vercel Serverless) │
│ - Server Components │
│ - API Routes (BFF) │
│ - Session management │
└────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Backend API Gateway │
│ - Authentication │
│ - Business logic │
│ - Database access │
└─────────────────────────────────────┘
Testing Strategy
Mock Mode Testing
Use mock mode for UI/UX testing
All features testable without backend
Realistic data scenarios
Type Safety
TypeScript catches errors at compile time
Zod validates runtime data
End-to-end type safety from API to UI
Next Steps
Authentication API Explore authentication endpoints and session management
Component Library Browse reusable UI components and their usage
User Guides Learn how to use the platform features
Portfolio API Access portfolio data and asset information
Best Practices
Domain Isolation : Keep domain logic isolated within domain folders. Shared logic goes in services/ or hooks/.
Type Safety : Always use Zod schemas for API responses. This provides runtime validation and prevents type drift.
React Query Keys : Use consistent query key patterns: ["resource", id, filters] for proper cache invalidation.
Security : Never expose session tokens or API keys to the client. All sensitive operations must go through the BFF.