Trovella Wiki

Page Patterns

Standard patterns for writing authenticated pages, stub pages, dynamic routes, and API route handlers.

Every page in Trovella follows one of a small number of established patterns. This guide shows each pattern with real code from the codebase.

Authenticated Dashboard Page

The standard pattern for any page that requires a session. Used by all dashboard and admin pages.

// apps/web/src/app/settings/page.tsx
import { redirect } from "next/navigation";

import { DashboardPage } from "@/components/dashboard/dashboard-page";
import { SettingsContent } from "@/components/settings/settings-content";
import { getSession } from "@/lib/auth-server";

export const metadata = { title: "Settings" };

export default async function SettingsPage() {
  const session = await getSession();
  if (!session) redirect("/auth/sign-in?callbackUrl=/settings");

  return (
    <DashboardPage session={session}>
      <SettingsContent />
    </DashboardPage>
  );
}

Key elements:

  1. export const metadata -- static metadata for the <title> tag (uses the %s | Trovella template from the root layout)
  2. async function -- page is a server component (no "use client")
  3. getSession() -- calls auth.api.getSession({ headers: await headers() }) under the hood
  4. redirect() with callbackUrl -- preserves the intended destination through the sign-in flow
  5. <DashboardPage session={session}> -- server component that runs ensurePersonalOrganization + ensureActiveOrganization, then renders Providers and DashboardShell
  6. Content component -- a "use client" component that uses tRPC hooks for data fetching

Stub Page (Coming Soon)

Placeholder pages for features not yet built. Same auth pattern, but renders a generic ComingSoon component.

// apps/web/src/app/discover/page.tsx
import { redirect } from "next/navigation";

import { ComingSoon } from "@/components/dashboard/coming-soon";
import { DashboardPage } from "@/components/dashboard/dashboard-page";
import { getSession } from "@/lib/auth-server";

export const metadata = { title: "Discover" };

export default async function DiscoverPage() {
  const session = await getSession();
  if (!session) redirect("/auth/sign-in?callbackUrl=/discover");

  return (
    <DashboardPage session={session}>
      <ComingSoon feature="Discover" />
    </DashboardPage>
  );
}

Currently four pages use this pattern: Discover, Create, Chat, and Memory.

Dynamic Route Page

Pages with URL parameters use the params promise pattern (Next.js 16).

// apps/web/src/app/admin/research-plans/[planId]/page.tsx
export default async function PlanDetailPage({ params }: { params: Promise<{ planId: string }> }) {
  const session = await getSession();
  if (!session) redirect("/auth/sign-in?callbackUrl=/admin/research-plans");

  const { planId } = await params;

  return (
    <DashboardPage session={session}>
      <ResearchPlanDetail planId={planId} />
    </DashboardPage>
  );
}

Note: In Next.js 16, params is a Promise that must be awaited. The callbackUrl redirects to the parent list page, not the detail page, since the user may not have access to the specific resource.

Conditional Public/Authenticated Page

The home page (/) renders different content based on auth state without redirecting.

// apps/web/src/app/page.tsx
export default async function Home() {
  const session = await getSession();

  if (session) {
    return (
      <DashboardPage session={session}>
        <DashboardContent user={session.user} />
      </DashboardPage>
    );
  }

  return <LandingPage />;
}

This is currently the only page that uses this pattern. All other pages either require auth (redirect) or are fully public.

Public Auth Page

Auth pages check for an existing session and redirect away if already authenticated.

// apps/web/src/app/auth/sign-in/page.tsx
export default async function SignInPage() {
  const session = await getSession();
  if (session) redirect("/");

  return (
    <Card>
      <CardHeader className="text-center">
        <CardTitle>Welcome to Trovella</CardTitle>
        <CardDescription>Sign in to continue</CardDescription>
      </CardHeader>
      <CardContent>
        <GoogleSignInButton />
      </CardContent>
    </Card>
  );
}

The auth layout (auth/layout.tsx) wraps children in a centered container -- no sidebar, no DashboardPage.

API Route Handler

API routes export named HTTP method functions. Each has its own auth strategy.

// apps/web/src/app/api/auth/[...all]/route.ts -- Better Auth catch-all (no manual auth)
export const { GET, POST } = toNextJsHandler(auth);

// apps/web/src/app/api/health/route.ts -- public, no auth
export async function GET() {
  const [database, redis, typesense] = await Promise.all([...]);
  return NextResponse.json(body, { status: anyHealthy ? 200 : 503 });
}

// apps/web/src/app/api/trpc/[trpc]/route.ts -- auth via tRPC context
function handler(req: Request) {
  return fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: ({ req }) => createContext({ headers: req.headers }),
    onError({ error, path, type }) { /* Sentry reporting */ },
  });
}
export { handler as GET, handler as POST };

The getSession() Helper

All page-level auth checks use getSession() from @/lib/auth-server:

// apps/web/src/lib/auth-server.ts
export async function getSession() {
  return auth.api.getSession({ headers: await headers() });
}

This returns the full session object (with user and session fields) or null. It calls await headers() internally, which is required in Next.js 16 server components for async header access.

When to Use Each Pattern

ScenarioPatternAuth
New feature page (authenticated)Dashboard pagegetSession() + redirect
Feature not yet builtStub pagegetSession() + redirect + <ComingSoon>
Detail view with URL paramDynamic routegetSession() + redirect + await params
Page with public + authenticated viewsConditional pagegetSession() without redirect
Sign-in / sign-upAuth pagegetSession() + redirect if already signed in
JSON endpointAPI route handlerVaries (tRPC context, session check, signing key, or none)

On this page