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")
| Component | Responsibilities |
|---|---|
RootLayout | Font loading (DM Sans, DM Serif Display, JetBrains Mono), metadata, nonce passthrough, theme/tooltip providers |
Page files (page.tsx) | getSession() call, auth redirect, metadata export |
DashboardPage | ensurePersonalOrganization(), ensureActiveOrganization(), wraps children in Providers + DashboardShell |
AuthLayout | Centered container for auth pages |
| Fumadocs layouts | Docs sidebar, search, page rendering |
Server components can:
- Access
headers()(needed forgetSession()and the CSP nonce) - Call
redirect()andnotFound() - 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")
| Component | Responsibilities |
|---|---|
Providers | Creates QueryClient (30s stale time) and tRPC client; wraps children in both providers |
DashboardShell | SidebarProvider, AppSidebar, DashboardTopBar, main content area |
AppSidebar | Navigation links with active state (usePathname()) |
ThemeProvider | next-themes wrapper with system default, class strategy, nonce for CSP |
| Content components | tRPC 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
- Server: Page calls
getSession()via@repo/auth/server-- session data includesuser.id,user.name,user.email,user.image,session.id, andsession.activeOrganizationId - Server:
DashboardPageensures the user has a personal organization and an active organization (both are idempotent database operations via@repo/auth/server) - Client:
Providersinitializes tRPC + TanStack Query clients - Client: Content components call tRPC hooks -- e.g.,
trpc.aiLog.list.useQuery()-- which send requests to/api/trpc - Server: The tRPC route handler creates a context from request headers (extracting the session), runs the procedure chain (
authorizedProcedureincludes auth, RLS, and CASL), and returns typed data - 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.