Frontend Structure
The frontend is a Next.js 15 application using the App Router. All routes live under apps/web/src/app/.
Route Groups
Route groups (parentheses in directory names) don't affect URLs — they're organizational only.
app/
├── (auth)/ # /login, /register, /forgot-password, /reset-password, /verify-email, /2fa
├── (public)/
│ ├── (marketing)/ # /, /services, /how-it-works, /pricing, /faq, /contact
│ └── (legal)/ # /privacy, /terms, /cookies
├── (account)/ # /account/claim (claim code flow for new customers)
├── (portal)/ # /portal/* (authenticated customer portal)
│ ├── orders/
│ │ ├── page.tsx # Order list
│ │ └── [id]/page.tsx # Order detail
│ ├── claim/page.tsx # Post-login claim redirect
│ └── settings/page.tsx
├── staff/
│ ├── (auth)/ # /staff/login, /staff/first-login/*
│ │ └── first-login/
│ │ ├── change-password/
│ │ ├── verify-email/
│ │ └── totp-setup/
│ └── (portal)/ # /staff/portal/* (authenticated staff portal)
│ └── portal/
│ ├── orders/ # Order list + detail + create
│ ├── builds/ # Build list + detail (photos, QA, timeline, booklets)
│ ├── components/ # Component catalog management
│ ├── operating-systems/
│ ├── peripherals/
│ ├── services/
│ ├── shipping-options/
│ ├── software/
│ ├── warranties/
│ ├── users/ # User management (ban/unban/role)
│ └── settings/ # Staff profile settings
├── quote/
│ └── [claimCode]/page.tsx # Public quote view (unauthenticated)
├── payment/
│ ├── success/page.tsx # Viva redirect after payment — triggers verify
│ └── failure/page.tsx # Viva redirect on payment failure
├── coming-soon/page.tsx
└── api/ # Next.js Route Handlers (thin proxies)
API Route Handlers
All app/api/* route handlers are thin proxies to NestJS. They:
- Forward the incoming request (including session cookie) to
${API_URL}/... - Return the NestJS response unchanged
Why proxies? Keeps API_URL (internal NestJS URL) server-side only. The browser always calls /api/* on the same origin — no CORS issues, no API URL exposed to the browser.
Exception: app/api/2fa-verify/route.ts — not a standard proxy. Manually signs the session cookie post-2FA to work around a better-call bug. See Auth docs.
Layout Hierarchy
app/layout.tsx # Root: HTML skeleton, fonts, Sentry, cookie consent
├── (public)/layout.tsx # Public navbar + footer
├── (auth)/layout.tsx # Centered auth card layout
├── (portal)/layout.tsx # Portal shell with sidebar
└── staff/
├── (auth)/layout.tsx # Staff auth layout (CTS branding)
└── (portal)/layout.tsx # Staff portal shell with sidebar
Server vs Client Components
Default: Server components. Fetch data server-side, pass to client components as props.
Client components ('use client') are used for:
- Anything with
useState,useEffect, event handlers - Forms (React Hook Form)
- Real-time polling (order status refresh)
- shadcn/ui interactive components
Pattern for data-heavy pages:
// page.tsx (server) — fetches data server-side
export default async function OrdersPage() {
const session = await getServerSession();
const orders = await fetch(`${API_URL}/orders`, {
headers: { cookie: cookies().toString() },
});
return <OrdersClient initialOrders={orders} session={session} />;
}
// orders-client.tsx (client) — interactive with polling
'use client';
export function OrdersClient({ initialOrders, session }) { ... }
Forms
Forms use React Hook Form + Zod v4 for validation:
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
getServerSession()
Available in any server component or route handler:
import { getServerSession } from "@/lib/auth-server";
const session = await getServerSession();
// Returns { user, session } or null
Uses API_URL (internal) to avoid public network round-trips. Always cache: 'no-store' — session state must always be fresh.
Auth Client
Browser-side auth operations use the Better Auth client from src/lib/auth-client.ts:
export const authClient = createAuthClient({
plugins: [adminClient(), twoFactorClient()],
});
No baseURL needed — the browser calls relative /api/auth/* paths which Next.js rewrites to NestJS.