Routing and Rendering
FluxKit uses Next.js App Router with route groups for landing, auth, and dashboard surfaces, plus a request pre-processing proxy.
Route topology
- Root entry:
src/app/page.tsxdelegates to landing content. - Public landing group:
src/app/(landing)/.... - Auth group:
src/app/(auth)/.... - Protected app group:
src/app/(dashboard)/.... - API auth endpoint:
src/app/api/auth/[...all]/route.ts.
import { LandingPageContent } from "./(landing)/landing-page-content";
export default function HomePage() {
return <LandingPageContent />;
}Rendering model
src/app/layout.tsxis an async server component (token fetch on server).- Most feature pages are client components where Convex hooks run (
"use client"). - Metadata is declared per route where needed.
export const metadata: Metadata = {
title,
description,
keywords: ["saas", "react", "nextjs", "typescript", "tailwind css"],
openGraph: { title, description, type: "website" },
twitter: { card: "summary_large_image", title, description },
};
export default function LandingPage() {
return <LandingPageContent />;
}export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
let token: string | null = null;
try {
token = (await getToken()) ?? null;
} catch {
token = null;
}
return (
<ConvexClientProvider initialToken={token}>
<ThemeProvider defaultTheme="system" storageKey="nextjs-ui-theme">
{children}
</ThemeProvider>
</ConvexClientProvider>
);
}Request pre-processing (proxy)
src/proxy.ts handles redirect compatibility (/login -> /sign-in), protected-route auth cookie checks, and per-IP rate limiting for protected prefixes.
if (pathname === "/login") {
return NextResponse.redirect(new URL("/sign-in", request.url));
}
const isProtectedRoute = protectedPrefixes.some((prefix) =>
pathname.startsWith(prefix),
);
if (isProtectedRoute && !sessionCookie) {
const signInUrl = new URL("/sign-in", request.url);
signInUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(signInUrl);
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.(?:png|jpg|jpeg|gif|webp|svg|ico)).*)",
],
};Auth route handling path
- Browser calls
/api/auth/*. - Next route handler exports Better Auth handler methods.
- Convex HTTP router (
convex/http.ts) registers Better Auth routes and applies rate-limit middleware to selected auth endpoints.
const isRateLimited = RATE_LIMITED_PATHS.some((path) =>
options.path?.includes(path),
);
if (isRateLimited && originalHandler) {
options.handler = async (request: Request) => {
const ip =
request.headers.get("x-forwarded-for")?.split(",")[0].trim() ||
request.headers.get("x-real-ip") ||
"unknown";
const rateLimitResult = await checkRateLimit(authLimiter, ip);
if (!rateLimitResult.success) {
return new Response(JSON.stringify({ error: "Too many requests" }), {
status: 429,
});
}
return originalHandler(request);
};
}Data-fetch rendering behavior in dashboard
Dashboard shell blocks on auth state and conditionally renders the app scaffold.
<AuthLoading>
<div className="flex h-screen w-full items-center justify-center bg-background">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
</AuthLoading>
<Unauthenticated>
<RedirectToSignIn />
</Unauthenticated>
<Authenticated>{/* SidebarProvider + layout content */}</Authenticated>Last updated on