Skip to main content
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:
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:
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/):

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:
Benefits:
  • Automatic revalidation
  • Caching
  • Error handling
  • Loading states

Component organization

Three-tier structure

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/.
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:

SWR hook pattern


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:
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 (@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:
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:

Global token getter

API services automatically use Clerk tokens:

Type system

Organization

Types are organized by feature:

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:

Log levels


Feature organization

When creating a new feature:

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/