Next.js 16 (App Router) with React 19, Supabase backend at ../backend.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, file-based routing) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS v4 + CSS variables (light/dark) |
| UI Components | Radix UI primitives + shadcn-style (src/components/ui/) |
| State | React Query (server state) + React Context (client state) |
| Auth | Supabase Auth (GitHub, Google, Discord, Slack, Microsoft, email OTP) |
| Forms | React Hook Form + Zod validation |
| Data Fetching | Supabase client + supabase.functions.invoke() for Edge Functions |
| Realtime | Supabase subscriptions (tickets, messages) |
| Testing | Vitest + React Testing Library |
| Icons | Lucide React, Flaticon UIcons |
| Fonts | Open Sans (body), Geist Mono (mono) |
src/
├── app/ # Next.js App Router pages
│ ├── auth/ # Sign-in, confirmed
│ ├── helper/ # Helper views (tickets, profile, reports)
│ ├── helpers/ # Helpers list
│ ├── invite/[token]/ # Invite acceptance
│ ├── onboarding/ # Onboarding flow
│ ├── projects/[id]/ # Project pages
│ ├── reports/ # Reports
│ ├── settings/ # Settings
│ ├── slas/ # SLA management
│ ├── support/[slug]/ # Public support (chat, tickets, get-support)
│ └── tickets/ # Ticket views
├── components/
│ ├── ui/ # Reusable primitives (shadcn-style)
│ ├── auth/ # AuthGuard, invite acceptance
│ ├── brand/ # Logo
│ ├── drawers/ # Slide-out panels
│ ├── layout/ # Sidebar, Header, ProtectedLayout, notifications
│ ├── modals/ # Dialogs (image upload, AI rephrase, SLA)
│ ├── payment/ # Distribution preview
│ └── ticket-chat/ # Chat components
├── contexts/ # UserProvider, ProjectProvider
├── hooks/ # React Query hooks (useProject, useTickets, etc.)
├── lib/
│ ├── supabase/ # Client init + auth helpers
│ ├── react-query/ # QueryClient provider + defaults
│ ├── utils.ts # cn() helper (clsx + tailwind-merge)
│ ├── format.ts # Formatting utilities
│ └── constants.ts # App-wide constants
└── types/
└── database.ts # Supabase Database type (manually maintained)
- Path alias:
@/*maps to./src/* - Client components: Add
"use client"when using hooks, state, or browser APIs - Hooks: Named
use*.tsinsrc/hooks/, wrapping React QueryuseQuery/useMutation - Contexts: Named
*Providerinsrc/contexts/ - Class merging: Always use
cn()from@/lib/utilsfor conditional Tailwind classes - Component variants: Use
class-variance-authority(cva) for variant-driven components - Button variants:
default,destructive,outline,secondary,ghost,link,lavender - Data attributes:
data-slotfor styling hooks on UI primitives
| Command | What it does |
|---|---|
npm run dev |
Start Next.js dev server |
npm run build |
Production build |
npm run start |
Serve production build |
npm run lint |
ESLint check |
npm run lint:fix |
ESLint auto-fix |
npm run test |
Run Vitest (single run) |
npm run test:watch |
Run Vitest in watch mode |
- Provider chain:
ReactQueryProvider→UserProvider→ProjectProvider→ProtectedLayout - Auth guard:
AuthGuardcheckssupabase.auth.getSession()and redirects unauthenticated users to/auth/signin - Onboarding redirect:
useOnboardingStatussends new users to/onboardingor/onboarding/waiting - React Query defaults: staleTime 3 hours, retry 1, refetchOnWindowFocus enabled (see
src/lib/react-query/provider.tsx) - Realtime:
useRealtimeMessagesanduseRealtimeTicketssubscribe to Supabase channels and invalidate React Query caches on changes - Tab lock: Custom
inTabLockutility prevents Supabase auth deadlocks during tab suspension - Database types:
src/types/database.tsdefinesDatabase— access row types viaDatabase["public"]["Tables"]["table_name"]["Row"] - Edge Functions: Called via
supabase.functions.invoke("function-name", { body })— function source lives in../backend
Defined in .env.local (see .env.example):
NEXT_PUBLIC_SUPABASE_URL— Supabase project URLNEXT_PUBLIC_SUPABASE_ANON_KEY— Supabase public anon key
All client-accessible vars must use the NEXT_PUBLIC_ prefix.
- Tests use Vitest with jsdom environment and React Testing Library
- Test files:
*.test.{ts,tsx}colocated with source - Setup:
vitest.setup.tsloads jest-dom matchers and cleanup - Run:
npm test(single) ornpm run test:watch(watch mode)