Trovella Wiki

Component Boundaries

The server/client component split, the provider tree, and how data flows from page-level auth through client-side tRPC hooks.

Trovella uses React 19 Server Components as the default. The "use client" boundary is pushed as far down as possible -- pages and the dashboard wrapper are server components, while interactive content lives below the Providers boundary.

The Component Tree

RootLayout (Server)
  |-- ThemeProvider (Client, next-themes)
  |-- TooltipProvider (Client, shadcn)
  |
  +-- Page (Server) -- calls getSession(), redirects if needed
        |
        +-- DashboardPage (Server)
        |     |-- ensurePersonalOrganization()  [server-side, idempotent]
        |     |-- ensureActiveOrganization()    [server-side, idempotent]
        |     |
        |     +-- Providers (Client, "use client")
        |           |-- trpc.Provider (tRPC client)
        |           |-- QueryClientProvider (TanStack Query)
        |           |
        |           +-- DashboardShell (Client, "use client")
        |                 |-- AppSidebar (Client)
        |                 |-- DashboardTopBar (Client)
        |                 |-- main content area
        |                       |
        |                       +-- Content Component (Client, "use client")
        |                             |-- trpc.routerName.endpoint.useQuery()
        |                             |-- Interactive UI
        |
        +-- Auth pages (Server, no DashboardPage)
        |
        +-- Fumadocs pages (Server, DocsLayout)

What Runs Where

Server Components (no "use client")

ComponentResponsibilities
RootLayoutFont loading (DM Sans, DM Serif Display, JetBrains Mono), metadata, nonce passthrough, theme/tooltip providers
Page files (page.tsx)getSession() call, auth redirect, metadata export
DashboardPageensurePersonalOrganization(), ensureActiveOrganization(), wraps children in Providers + DashboardShell
AuthLayoutCentered container for auth pages
Fumadocs layoutsDocs sidebar, search, page rendering

Server components can:

  • Access headers() (needed for getSession() and the CSP nonce)
  • Call redirect() and notFound()
  • Await async data (database queries, auth checks)
  • Import from @repo/auth/server

Server components cannot:

  • Use React hooks (useState, useEffect, etc.)
  • Access browser APIs
  • Handle user interactions

Client Components ("use client")

ComponentResponsibilities
ProvidersCreates QueryClient (30s stale time) and tRPC client; wraps children in both providers
DashboardShellSidebarProvider, AppSidebar, DashboardTopBar, main content area
AppSidebarNavigation links with active state (usePathname())
ThemeProvidernext-themes wrapper with system default, class strategy, nonce for CSP
Content componentstRPC hooks (useQuery, useMutation), form handling, interactive UI

The Provider Tree

Providers (apps/web/src/components/providers.tsx) is the "use client" boundary that enables tRPC hooks in all descendant components:

"use client";

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () => new QueryClient({ defaultOptions: { queries: { staleTime: 30_000 } } }),
  );
  const [trpcClient] = useState(makeTRPCClient);

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}

The tRPC client is configured with httpBatchLink({ url: "/api/trpc" }) and communicates with the tRPC route handler at /api/trpc/[trpc].

Default stale time is 30 seconds. TanStack Query considers data fresh for 30 seconds before refetching in the background. This balances responsiveness with request volume.

Data Flow Pattern

  1. Server: Page calls getSession() via @repo/auth/server -- session data includes user.id, user.name, user.email, user.image, session.id, and session.activeOrganizationId
  2. Server: DashboardPage ensures the user has a personal organization and an active organization (both are idempotent database operations via @repo/auth/server)
  3. Client: Providers initializes tRPC + TanStack Query clients
  4. Client: Content components call tRPC hooks -- e.g., trpc.aiLog.list.useQuery() -- which send requests to /api/trpc
  5. Server: The tRPC route handler creates a context from request headers (extracting the session), runs the procedure chain (authorizedProcedure includes auth, RLS, and CASL), and returns typed data
  6. Client: TanStack Query caches the response and triggers re-renders

The session object is passed from the page server component to DashboardPage as a prop but is not passed to client components as a prop. Client components that need user info receive it through the DashboardShell user prop (just id, name, email, image). Full session/auth context on the client side comes from tRPC procedures that include the session in their server-side context.

Gotchas

Hydration Mismatches

Server components render on the server where browser APIs are unavailable. Hooks like useIsMobile return a default value on the server (false) but may return a different value on the client (true), causing a hydration mismatch. Use suppressHydrationWarning on the <html> tag (already set in the root layout) and handle initial states carefully in client components.

getSession() Requires await headers()

In Next.js 16, headers() is async. The getSession() helper handles this internally, but any code that reads request headers in a server component must use await headers().

shadcn/ui Components

shadcn/ui components are generated with rsc: true but many use "use client" internally (they need browser APIs for interaction). This is fine -- the client boundary propagates from the component, not from the consumer. Import them in either server or client components without worry.

On this page