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:
export const metadata-- static metadata for the<title>tag (uses the%s | Trovellatemplate from the root layout)async function-- page is a server component (no"use client")getSession()-- callsauth.api.getSession({ headers: await headers() })under the hoodredirect()withcallbackUrl-- preserves the intended destination through the sign-in flow<DashboardPage session={session}>-- server component that runsensurePersonalOrganization+ensureActiveOrganization, then rendersProvidersandDashboardShell- 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
| Scenario | Pattern | Auth |
|---|---|---|
| New feature page (authenticated) | Dashboard page | getSession() + redirect |
| Feature not yet built | Stub page | getSession() + redirect + <ComingSoon> |
| Detail view with URL param | Dynamic route | getSession() + redirect + await params |
| Page with public + authenticated views | Conditional page | getSession() without redirect |
| Sign-in / sign-up | Auth page | getSession() + redirect if already signed in |
| JSON endpoint | API route handler | Varies (tRPC context, session check, signing key, or none) |