# Frontend Setup & Configuration Guide

> Comprehensive guide to frontend setup, authentication, routing, state management, styling, and API integration

## Table of Contents

1. [Authentication](#authentication)
2. [Routing & Navigation](#routing--navigation)
3. [State Management](#state-management)
4. [API Integration with tRPC](#api-integration-with-trpc)
5. [Styling with Tailwind CSS](#styling-with-tailwind-css)

---

## Authentication

Better Auth integration and route protection patterns for Izri.

### Overview

The web app uses Better Auth for session management with server-first validation in React Router loaders. Authentication status is provided at the root, and organization-scoped routes are protected by an auth layout and org validation.

**Key points:**
- Server-side session via Better Auth (auth.api.getSession)
- Root loader returns user to all routes
- Auth pages redirect if already authenticated
- Organization routes require authentication and valid org membership

### Core Files

- apps/web/app/lib/auth-server.ts — getAuthenticatedUser(request)
- apps/web/app/lib/auth-loader.ts — requireAuth(request), redirectIfAuthenticated(request)
- apps/web/app/lib/organization-loader.ts — requireOrganization(), requireOrganizationAdmin()
- apps/web/app/root.tsx — root loader publishes user
- apps/web/app/routes/_auth.tsx — auth layout (redirect if authenticated)
- apps/web/app/routes/_organizations.tsx — protected org layout

### Server-Side Session

File: apps/web/app/lib/auth-server.ts

```ts
import { auth } from '@izri/auth'
import type { AuthenticatedUser } from '@izri/shared'

export async function getAuthenticatedUser(request: Request): Promise<AuthenticatedUser | null> {
  try {
    const sessionData = await auth.api.getSession({ headers: request.headers })
    if (!sessionData?.user) return null
    return {
      id: sessionData.user.id,
      email: sessionData.user.email,
      name: sessionData.user.name,
      image: sessionData.user.image,
    }
  } catch {
    return null
  }
}
```

### Loader Helpers

File: apps/web/app/lib/auth-loader.ts

```ts
import { redirect } from 'react-router'
import { getAuthenticatedUser } from './auth-server'

export async function requireAuth(request: Request) {
  const user = await getAuthenticatedUser(request)
  if (!user) throw redirect('/auth/login')
  return user
}

export async function redirectIfAuthenticated(request: Request) {
  const user = await getAuthenticatedUser(request)
  if (user) throw redirect('/organizations')
  return null
}
```

### Root Loader

File: apps/web/app/root.tsx

```ts
export async function loader({ request }: Route.LoaderArgs) {
  const user = await getAuthenticatedUser(request)
  return { user }
}
```

All routes can read the root user via useRouteLoaderData('root') or through props provided by layouts.

### Organization Access Checks

File: apps/web/app/lib/organization-loader.ts

```ts
import { redirect } from 'react-router'
import { createServerTRPC } from './trpc-server'

export async function requireOrganization(request: Request, slug: string, user: AuthenticatedUser) {
  try {
    const trpc = await createServerTRPC(request)
    const result = await trpc.organizations.getBySlug({ slug, userEmail: user.email })
    return result.organization
  } catch {
    throw redirect('/organizations')
  }
}

export function requireOrganizationAdmin(org: { slug: string; role: string }) {
  if (!['OWNER', 'ADMIN'].includes(org.role)) {
    throw redirect(`/organizations/${org.slug}`)
  }
}
```

### Notes

- tRPC client uses cookie-based auth; requests include credentials: 'include'
- Prefer server-side checks in loaders for access control
- Avoid client-only guards for protected data

---

## Routing & Navigation

Complete guide to routing patterns and setup in Izri.

### React Router v7 Overview

Izri uses React Router v7 for routing, SSR data loading, and layouts.

**Why React Router v7?**
- File-based routing with typed loaders
- SSR by default with optional SPA mode
- Data loaders and actions at the route level
- Automatic code splitting
- Vite-powered dev experience

### Project Structure

```
apps/web/
├── app/
│   ├── root.tsx               # Root layout
│   ├── routes.ts              # Route config (authoritative)
│   ├── routes/                # Route files
│   │   ├── home.tsx           # Homepage (/)
│   │   ├── _auth.tsx          # Auth layout
│   │   ├── _organizations.tsx # Organizations layout (auth-protected)
│   │   ├── auth/
│   │   │   ├── login.tsx      # /auth/login
│   │   │   ├── register.tsx   # /auth/register
│   │   │   └── logout.tsx     # /auth/logout
│   │   └── organizations/
│   │       ├── index.tsx                      # /organizations
│   │       ├── new.tsx                        # /organizations/new
│   │       ├── $slug.tsx                      # layout for slugged routes
│   │       └── $slug/
│   │           ├── index.tsx                  # /organizations/:slug
│   │           ├── dashboard.tsx              # /organizations/:slug/dashboard
│   │           ├── stats.tsx                  # /organizations/:slug/stats
│   │           ├── projects/
│   │           │   ├── index.tsx              # /organizations/:slug/projects
│   │           │   ├── new.tsx                # /organizations/:slug/projects/new
│   │           │   └── $id.tsx                # /organizations/:slug/projects/:id
│   │           ├── settings.tsx               # /organizations/:slug/settings
│   │           ├── members.tsx                # /organizations/:slug/members
│   │           └── invite.tsx                 # /organizations/:slug/invite
│   └── app.css                # Global styles
├── react-router.config.ts     # React Router config
├── vite.config.ts             # Vite config
└── package.json
```

### Configuration

**File:** apps/web/react-router.config.ts

```ts
import type { Config } from '@react-router/dev/config'

export default {
  ssr: true,
} satisfies Config
```

**File:** apps/web/vite.config.ts

```ts
import { reactRouter } from '@react-router/dev/vite'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [reactRouter(), tsconfigPaths()],
  server: { port: 5173 },
})
```

### Route Configuration

File: apps/web/app/routes.ts

```ts
import { index, layout, type RouteConfig, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('auth/logout', 'routes/auth/logout.tsx'),

  layout('routes/_auth.tsx', [
    route('auth/login', 'routes/auth/login.tsx'),
    route('auth/register', 'routes/auth/register.tsx'),
  ]),

  layout('routes/_organizations.tsx', [
    route('organizations', 'routes/organizations/index.tsx'),
    route('organizations/new', 'routes/organizations/new.tsx'),

    layout('routes/organizations/$slug.tsx', [
      route('organizations/:slug', 'routes/organizations/$slug/index.tsx'),
      route('organizations/:slug/dashboard', 'routes/organizations/$slug/dashboard.tsx'),
      route('organizations/:slug/stats', 'routes/organizations/$slug/stats.tsx'),
      route('organizations/:slug/projects', 'routes/organizations/$slug/projects/index.tsx'),
      route('organizations/:slug/projects/new', 'routes/organizations/$slug/projects/new.tsx'),
      route('organizations/:slug/projects/:id', 'routes/organizations/$slug/projects/$id.tsx'),
      route('organizations/:slug/settings', 'routes/organizations/$slug/settings.tsx'),
      route('organizations/:slug/members', 'routes/organizations/$slug/members.tsx'),
      route('organizations/:slug/invite', 'routes/organizations/$slug/invite.tsx'),
    ]),
  ]),
] satisfies RouteConfig
```

### Current Routes

```
/                               → routes/home.tsx (public)
/auth/login                     → routes/auth/login.tsx (public)
/auth/register                  → routes/auth/register.tsx (public)
/auth/logout                    → routes/auth/logout.tsx (protected)
/organizations                  → routes/organizations/index.tsx (protected)
/organizations/new              → routes/organizations/new.tsx (protected)
/organizations/:slug            → routes/organizations/$slug/index.tsx (protected)
/organizations/:slug/dashboard  → routes/organizations/$slug/dashboard.tsx (protected)
/organizations/:slug/projects   → routes/organizations/$slug/projects/index.tsx (protected)
/organizations/:slug/settings   → routes/organizations/$slug/settings.tsx (protected)
/organizations/:slug/members    → routes/organizations/$slug/members.tsx (protected)
/organizations/:slug/invite     → routes/organizations/$slug/invite.tsx (protected)
```

### Data Loading Patterns

**Auth Protection:**

```ts
import { Outlet } from 'react-router'
import { requireAuth } from '@/lib/auth-loader'

export async function loader({ request }: Route.LoaderArgs) {
  const user = await requireAuth(request)
  return { user }
}

export default function OrganizationsLayout() {
  return <Outlet />
}
```

**Organization Validation:**

```ts
import { requireOrganization } from '@/lib/organization-loader'
import { requireAuth } from '@/lib/auth-loader'

export async function loader({ request, params }: Route.LoaderArgs) {
  const user = await requireAuth(request)
  const organization = await requireOrganization(request, params.slug!, user)
  return { user, organization }
}
```

---

## State Management

Managing state and data in Izri frontend.

### State Categories

**1. Server State (tRPC + TanStack Query)**
- Data from the backend (projects, users, organizations)
- Tools: tRPC, TanStack Query

**2. Route State (React Router)**
- URL parameters, route loaders
- Tools: React Router loaders, useParams, useSearchParams

**3. Component State (React)**
- UI state (modals, forms, toggles)
- Tools: useState, useReducer

**4. Global State (Context)**
- App-wide state (auth, theme, current org)
- Tools: React Context

### Preferred Pattern

Use tRPC + React Router loaders for most data. Component state for temporary UI state. Context only for truly global app state.

---

## API Integration with tRPC

Using tRPC with TanStack Query for type-safe API calls in the web app.

### Overview

The web app uses tRPC for end-to-end type safety and TanStack Query for caching, loading states, and mutations.

**Key points:**
- Single HTTP endpoint at /trpc
- Cookie-based auth; fetch uses credentials: 'include'
- React-side context via createTRPCContext and TRPCProvider wrapper

### Files

- app/lib/trpc.ts — creates the TRPC React context utilities
- app/providers/trpc-provider.tsx — configures the tRPC client and QueryClient
- app/root.tsx — mounts TRPCProvider at the root

### Setup

**File:** app/lib/trpc.ts

```ts
import type { AppRouter } from '@izri/trpc/routers'
import { createTRPCContext } from '@trpc/tanstack-react-query'

export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>()
```

**File:** app/providers/trpc-provider.tsx

```tsx
import type { AppRouter } from '@izri/trpc/routers'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import { type ReactNode, useState } from 'react'

import { TRPCProvider as BaseTRPCProvider } from '@/lib/trpc'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { staleTime: 60_000 },
    },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (typeof window === 'undefined') return makeQueryClient()
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

export function TRPCProvider({ children }: { children: ReactNode }) {
  const queryClient = getQueryClient()
  const [trpcClient] = useState(() =>
    createTRPCClient<AppRouter>({
      links: [
        httpBatchLink({
          url: '/trpc',
          fetch(url, options) {
            return fetch(url, { ...options, credentials: 'include' })
          },
        }),
      ],
    }),
  )

  return (
    <QueryClientProvider client={queryClient}>
      <BaseTRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
        {children}
      </BaseTRPCProvider>
    </QueryClientProvider>
  )
}
```

### Queries

**Basic list query:**

```tsx
import { useTRPC } from '@/lib/trpc'
import { useQuery } from '@tanstack/react-query'

export function OrganizationsList({ userEmail }: { userEmail: string }) {
  const trpc = useTRPC()
  const { data, isPending, error } = useQuery({
    ...trpc.organizations.list.queryOptions({ userEmail }),
    enabled: !!userEmail,
  })

  if (isPending) return <div>Loading...</div>
  if (error) return <div>Error: {(error as Error).message}</div>

  return (
    <ul>
      {data?.organizations.map((o) => (
        <li key={o.id}>{o.name}</li>
      ))}
    </ul>
  )
}
```

### Mutations

**Create with cache invalidation:**

```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useTRPC } from '@/lib/trpc'

export function CreateOrganizationButton() {
  const trpc = useTRPC()
  const qc = useQueryClient()

  const createOrg = useMutation({
    ...trpc.organizations.create.mutationOptions(),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: trpc.organizations.list.queryKey() })
    },
  })

  return (
    <button onClick={() => createOrg.mutate({ name: 'New Org' })}>
      Create Organization
    </button>
  )
}
```

---

## Styling with Tailwind CSS

Tailwind CSS v4 styling practices in Izri.

### Setup

**File:** apps/web/tailwind.config.ts

```typescript
import type { Config } from 'tailwindcss'

export default {
  content: ['./app/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
      },
    },
  },
} satisfies Config
```

**File:** apps/web/app/app.css

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
  }

  * {
    @apply border-border;
  }

  body {
    @apply bg-background text-foreground;
  }
}
```

### Utility Classes

**Layout:**

```tsx
<div className="container mx-auto px-4">
  <div className="flex items-center justify-between">
    Content
  </div>
</div>
```

**Component Styling with CVA:**

```tsx
import { cva, type VariantProps } from 'class-variance-authority'

const buttonVariants = cva(
  'rounded-lg px-4 py-2 font-medium transition-colors',
  {
    variants: {
      variant: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
        danger: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'text-sm px-3 py-1.5',
        md: 'text-base px-4 py-2',
        lg: 'text-lg px-6 py-3',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)

interface ButtonProps extends VariantProps<typeof buttonVariants> {
  className?: string
}

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return (
    <button className={buttonVariants({ variant, size, className })} {...props} />
  )
}
```

### Responsive Design

```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <div className="p-4">Item 1</div>
  <div className="p-4">Item 2</div>
  <div className="p-4">Item 3</div>
</div>
```

**Breakpoints:** sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px)

### Dark Mode

```tsx
export default {
  darkMode: 'class',
}
```

---

**Related:** [Components & Forms](./components-and-forms.md) | [Testing](./testing-frontend.md) | [README](./README.md)
