> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundchecklive.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Web Architecture

> Architecture patterns and structure of the Soundcheck web app

This document covers the architecture patterns, conventions, and structure of the Soundcheck web application.

## Routing

### Next.js app router

The app uses Next.js App Router with file-based routing. There are **three route groups** (`(app)`, `(org-settings)`, `(portal)`) plus several ungrouped public routes:

```
src/app/
├── (app)/                 # Protected app (requires auth) — layout.tsx
│   ├── home/             # /home  (landing; replaced the retired /dashboard)
│   ├── gigs/             # /gigs, /gigs/[id], /gigs/sample
│   ├── venues/           # /venues, /venues/[id]
│   ├── engagements/      # /engagements
│   ├── org-engagements/  # /org-engagements
│   ├── availability/     # /availability, /availability/team, /availability/requests/[id]
│   ├── crew-offers/      # /crew-offers
│   ├── offers/           # /offers
│   ├── customers/        # /customers, /customers/[id]
│   ├── people/           # /people, /people/[id]
│   ├── teams/            # /teams
│   ├── tasks/            # /tasks
│   ├── payments/         # /payments
│   ├── contracts/        # /contracts/new
│   ├── imports/          # /imports, /imports/[jobId]
│   ├── inventory/        # /inventory
│   ├── sponsors/         # /sponsors
│   ├── leads/            # /leads
│   ├── setlist/          # /setlist, /setlist/[id], /setlist/new
│   ├── onboarding/org/   # /onboarding/org
│   ├── settings/         # /settings, /settings/billing, /settings/notifications
│   ├── profile/  docs/   # /profile, /docs
├── (org-settings)/        # Org admin — /settings/organization/* (profile, branding,
│                          #   members, positions, departments, forms, integrations,
│                          #   connections, notifications, onboarding) — own layout.tsx
├── (portal)/              # Client portal — /portal/* (dashboard, gigs, contracts,
│                          #   forms, invoices, messages, accept-invite, sign-in)
├── [slug]/                # Public white-label org page (dynamic, unauthenticated)
├── storefront/[identifier]/         # Public booking form (/lead-intake redirects here)
├── sponsor-intake/[organizationId]/  # Public sponsor intake form
├── sign-in/  sign-up/  forgot-password/   # Public auth (Clerk catch-all routes)
├── api/                   # Route handlers (attachments, spotify, invitations, members)
└── layout.tsx            # Root layout
```

> [!WARNING]
>
> There is **no** `(app)/dashboard` route — it was retired in favor of `home/`. Org settings live under the `(org-settings)` group, not a flat `/organization-details`.

### Route protection

* **Public routes**: `/sign-in`, `/sign-up`, `/forgot-password`, `/[slug]` (white-label org pages), `/storefront/*`, `/sponsor-intake/*`, and the `(portal)` sign-in
* **Protected routes**: everything under `(app)/` and `(org-settings)/` requires authentication (see `AppProviders` / `AuthenticatedProviders`)
* **Client portal**: `(portal)/portal/*` is a separate authenticated surface for external clients, with its own branding, navigation, and sidebar providers

***

## Provider hierarchy

The root layout (`src/app/layout.tsx`) wraps the tree in a `next-themes` `ThemeProvider` and then `AppProviders` (`src/lib/providers/AppProviders.tsx`). The rendered nesting is:

```text theme={null}
ThemeProvider → PostHogProvider → FeatureFlagOverrideProvider → ClerkProvider
└── ApiTokenGate
    ├── PathnameBridge (independent Suspense boundary)
    ├── Public booking: stable SWRConfig → public appearance → page
    └── SessionSWRProvider → InviteTermsGateProvider
        ├── Public auth: public appearance → page
        └── PrivateRouteProviders (dynamic)
            └── RoleEmulationProvider → ViewAsProvider → UserContextProvider
                └── DebugSubscriptionOverrideProvider → OrgProvider → BrandingProvider
                    └── ClerkAuthInitializer
                        └── WorkspaceProviders (dynamic, signed-in workspace routes)
                            └── OrgBootstrapProvider → AskCheckAIDock → page
```

`AuthenticatedProviders` classifies resolved paths before mounting private contexts. Sign-in and sign-up retain the session cache without downloading workspace providers. Portal and service-console routes use the private auth boundary with workspace data disabled; they do not mount workspace bootstrap, the assistant/Mintlify widget, or WebMcpTools. WebMcpTools is a sibling of the assistant inside workspace bootstrap.

Keep `ApiTokenGate` and both SWR cache owners above the dynamic, route Suspense, and Activity boundaries. Hiding an SWR cache owner can remove its global registration while retained consumers still reference it. Loading gates preserve children with Activity; token verification and organization bootstrap stay outside the boundary whose readiness they determine. Session, user, and organization changes replace the session cache, and scoped mutations cannot reach public or later-session caches.

Unclassified incoming children wait behind the loading shell. A validated private subtree retains its provider identity through pathname suspension, preserving drafts on same-route refreshes. Account forms retain their validated children in a hidden Activity boundary until the path resolves; incoming private children cannot render through that retained public branch. Public booking has its own stable cache and remains interactive while Clerk is unavailable; a retained public page stays hidden until its pathname is validated again.

Gig pages use `next/dynamic` for the calendar, maps, rich text, section drag grid, and heavy tab panels. The read-only rich-text viewer imports DOMPurify independently of the Tiptap editor; viewing notes must not request the editing engine. Preserve the existing conditional/deferred-tab mounting so inactive panels do not request their chunks. Skeletons use the existing UI primitives, and the travel tab warms its chunk on hover or focus. Temporal remains shared because public booking and appearance storage use it; coordinate timezone lookup is absent from sign-in's initial bundle.

***

## State management

### React context

Global state is managed with React Context (providers in `src/lib/context/`):

| Context | File | Purpose |
| - | - | - |
| `UserContextProvider` | `userContext.tsx` | User data, permissions, subscription |
| `GigsContext` | `gigsContext.tsx` | List of all gigs/events |
| `GigContext` | `gigContext.tsx` | Single gig detail state |
| `OrgProvider` | `org/OrgProvider.tsx` | Organization data, members, branding |
| `BrandingProvider` | `branding/BrandingProvider.tsx` | Org branding / theming |
| `RoleEmulationProvider` | `roleEmulationContext.tsx` | Role emulation / impersonation |
| `InviteTermsGateProvider` | `inviteTermsGateContext.tsx` | Gates invited users on terms acceptance |
| `SidebarContext` | `sidebarContext.tsx` | Sidebar open/close state |
| Portal providers | `portal/*` | Client-portal branding, navigation, sidebar |

### Cache Components vs SWR

`cacheComponents` and `partialPrefetching` are on in `web/next.config.mjs`. Next prerenders a static (or Partial Prerender) shell per route; Instant Navigations prefetch that shell in production. Almost every `(app)` page is still `'use client'` + SWR after Clerk hydration, and `useWorkspaceLoadingGate` still gates the protected chrome. Do not move auth or org data onto `'use cache'` — SWR remains the data layer.

Staff app paths live in `web/src/lib/auth/protectedAppRoutes.ts`. Middleware and `AppProviders` both use that list so a single-segment staff page cannot fall through to the public `[slug]` booking profile.

### SWR for data fetching

All API data fetching uses SWR:

```tsx theme={null}
// Example: lib/hooks/org/useSetlistsSWR.ts
export const useSetlistsSWR = (orgId: string) => {
  return useSWR(
    orgId ? `/organizations/${orgId}/setlists` : null,
    () => setlistService.getSetlists(orgId)
  );
};
```

Benefits:

* Automatic revalidation
* Caching
* Error handling
* Loading states

***

## Component organization

### Three-tier structure

```
components/
├── ui/                    # 1. UI Primitives
│   ├── button.tsx
│   ├── input.tsx
│   └── card.tsx
├── common/                # 2. Shared Business Components
│   ├── Gigs/
│   ├── Modal/
│   └── DatePicker/
└── static/                # Layout components
    ├── Header/
    └── Sidebar/
```

### Decision tree

* **UI component?** → `components/ui/`
* **Used in 2+ features?** → `components/common/`
* **Feature-specific?** → `app/(app)/[feature]/components/`

***

## Data fetching pattern

### Generated typed API client

The Axios client and request/response types are **generated from the OpenAPI spec** — do not hand-write them. `web/scripts/generate-api-client.mjs` runs `swagger-typescript-api` against `docs/openapi/soundcheck.json` and emits a typed, modular client into `web/src/lib/generated/api-client/`.

```bash theme={null}
npm run generate:api-client        # node scripts/generate-api-client.mjs
```

This runs automatically via the `predev`, `prebuild`, `pretypecheck`, and `pretest` hooks, and skips regeneration when the output is newer than the spec (pass `--force` or `FORCE_API_CODEGEN=1` to override). The generated client lives under `src/lib/generated/` and is **gitignored** — a fresh checkout must regenerate it (this happens automatically on `npm run dev`/`build`).

### Service layer

API calls go through a service layer that wraps the generated client:

```tsx theme={null}
// lib/services/apiService.ts
class ApiService {
  private client: AxiosInstance;

  async get<T>(url: string): Promise<T> {
    const response = await this.client.get(url);
    return response.data;
  }
}

// lib/services/eventService.ts
class EventService {
  async getEvents(): Promise<Event[]> {
    return apiService.get('/api/v1/events');
  }
}
```

### SWR hook pattern

```tsx theme={null}
// lib/hooks/org/useEventsSWR.ts
export const useEventsSWR = (orgId: string) => {
  return useSWR(
    orgId ? `/organizations/${orgId}/events` : null,
    () => eventService.getEventsByOrg(orgId)
  );
};

// Usage in component
function EventsList() {
  const { data: events, isLoading, error, mutate } = useEventsSWR(orgId);

  if (isLoading) return <Skeleton />;
  if (error) return <Error />;

  return <EventList events={events} />;
}
```

***

## Styling architecture

### Tailwind v4 (CSS-based config)

The app uses **Tailwind CSS v4**. There is **no `tailwind.config.*` file** — configuration lives in `src/app/globals.css` via `@import "tailwindcss"`, an `@custom-variant dark`, and an `@theme inline { … }` block that maps utility names to CSS variables (the shadcn v4 pattern). Prefer Tailwind utility classes:

```tsx theme={null}
import { cn } from "@/lib/utils";

<div className={cn(
  "base-classes",
  condition && "conditional-classes",
  className
)} />
```

One hand-written stylesheet is a sanctioned exception, to style a third-party widget:

* `components/common/Gigs/RichTextEditor/richtext.css` — Tiptap editor styling

Calendars (Gigs, My Availability) use the vendored ReUI event calendar in `components/reui/event-calendar/`, which is Tailwind-only; see `components/reui/README.md`.

(react-pdf also imports its own annotation-layer CSS for the PDF viewer.) Outside these, avoid adding `.css` files.

### Rich text editing

Rich text uses **headless [Tiptap](https://tiptap.dev)** (`@tiptap/react` + starter-kit, table, text-align, text-style extensions) in `components/common/Gigs/RichTextEditor/`. TinyMCE has been removed — the only remaining references are backward-compat comments for content authored by the old editor.

### Design tokens

Semantic color tokens are defined as CSS variables in `globals.css` (with `:root` light values and `.dark` overrides from the dark-mode redesign) and exposed as utilities:

| Token | Usage |
| - | - |
| `text-finance`, `bg-finance` | Financial positive (income, profit) |
| `text-success`, `bg-success` | General positive states |
| `text-destructive`, `bg-destructive` | Negative states (errors, delete) |
| `text-warning`, `bg-warning` | Caution states |
| `text-info`, `bg-info` | Informational states |

> [!WARNING]
>
> Avoid hardcoded colors like `text-green-500`. Use semantic tokens for stateful UI. See `web/docs/THEMING.md` for the full light/dark token contract.

***

## Authentication

### Clerk integration

Authentication is handled via Clerk middleware:

```tsx theme={null}
// src/middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();
```

### Global token getter

API services automatically use Clerk tokens:

```tsx theme={null}
// lib/providers/ClerkAuthInitializer.tsx
useEffect(() => {
  ApiService.setGlobalTokenGetter(async () => {
    return await getToken();
  });
}, [getToken]);
```

***

## Type system

### Organization

Types are organized by feature:

```
lib/types/
├── event.ts          # Event/gig types
├── user.ts           # User types
├── organization.ts   # Org types
├── api.ts            # API response types
└── index.ts          # Re-exports
```

### Conventions

* Use `interface` for object shapes
* Use `type` for unions, intersections
* Export from feature files, re-export from `index.ts`

***

## Logging

### Centralized logger

Never use `console.log` directly. Use the centralized logger:

```tsx theme={null}
import { logger } from "@/lib/logger";

// Or domain-specific loggers
import { apiLogger, eventLogger } from "@/lib/logger";

// Usage
logger.debug("Processing data", { userId, count });
logger.info("Event created successfully");
logger.warn("Duplicate request detected");
logger.error("Error fetching event", error);
```

### Log levels

| Level | Environment | Usage |
| - | - | - |
| `debug` | Development only | Debugging info |
| `info` | All | Significant operations |
| `warn` | All | Potential issues |
| `error` | All | Errors and exceptions |

***

## Feature organization

When creating a new feature:

```
src/app/(app)/[feature-name]/
├── [id]/                    # Dynamic routes
│   └── page.tsx
├── components/              # Feature components
│   ├── ComponentGroup/
│   │   ├── ComponentA.tsx
│   │   └── index.tsx
│   └── index.tsx
├── constants.ts             # Feature constants
├── hooks/                   # Feature hooks
├── utils/                   # Feature utilities
└── page.tsx                 # Main page
```

***

## Best practices

### Components

1. Prefer functional components with hooks
2. Use `const` for component definitions
3. Export default for pages, named for reusables

### Error handling

1. Use try/catch for async operations
2. Provide descriptive error messages
3. Use toast notifications for user-facing errors

### Performance

1. Use SWR for server state (automatic caching)
2. Lazy load heavy components
3. Memoize expensive computations

### Testing

1. Write tests for major functions
2. Co-locate unit tests with source files
3. E2E tests in `automated-tests/`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.