Routing¶
COORDINATOR uses Next.js App Router for file-based routing.
Directory Structure¶
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── api/
│ └── logs/
│ └── route.ts # Logging API endpoint
├── (marketing)/ # Route group for public pages
│ ├── about/
│ ├── contact/
│ └── pricing/
├── login/
│ ├── page.tsx
│ └── verify-code/
├── sign-up/
│ ├── page.tsx
│ └── verify-code/
├── reset-password/
├── profile/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── settings/
│ ├── organizations/
│ ├── invites/
│ └── teams/
└── portal/
├── page.tsx
└── [organizationSlug]/
├── layout.tsx
├── page.tsx
├── pay/
├── accept-invite/
├── settings/
└── [teamID]/
├── page.tsx
├── chat/
├── sessions/
├── context/
├── training/
└── settings/
Route Groups¶
Marketing Routes (Public)¶
(marketing)/
├── about/page.tsx → /about
├── contact/page.tsx → /contact
└── pricing/page.tsx → /pricing
Route groups (parentheses) don't affect the URL but help organize code.
Auth Routes¶
login/
├── page.tsx → /login
└── verify-code/ → /login/verify-code
sign-up/
├── page.tsx → /sign-up
└── verify-code/ → /sign-up/verify-code
reset-password/ → /reset-password
Profile Routes (Authenticated)¶
profile/
├── page.tsx → /profile
├── settings/ → /profile/settings
├── organizations/
│ ├── page.tsx → /profile/organizations
│ └── create/ → /profile/organizations/create
├── invites/ → /profile/invites
└── teams/ → /profile/teams
Portal Routes (Authenticated + Org Scoped)¶
portal/
├── page.tsx → /portal
└── [organizationSlug]/
├── page.tsx → /portal/my-org
├── pay/ → /portal/my-org/pay
├── accept-invite/[inviteID]/ → /portal/my-org/accept-invite/abc
├── settings/
│ ├── members/ → /portal/my-org/settings/members
│ ├── teams/ → /portal/my-org/settings/teams
│ └── billing/ → /portal/my-org/settings/billing
└── [teamID]/
├── page.tsx → /portal/my-org/team-1
├── chat/
│ ├── page.tsx → /portal/my-org/team-1/chat
│ └── [chatID]/ → /portal/my-org/team-1/chat/abc
├── sessions/
│ ├── page.tsx → /portal/my-org/team-1/sessions
│ ├── manage/ → /portal/my-org/team-1/sessions/manage
│ ├── new/ → /portal/my-org/team-1/sessions/new
│ ├── forms/ → /portal/my-org/team-1/sessions/forms
│ └── [sessionID]/ → /portal/my-org/team-1/sessions/abc
├── context/
│ ├── page.tsx → /portal/my-org/team-1/context
│ ├── upload/ → /portal/my-org/team-1/context/upload
│ └── manage/
│ ├── page.tsx → /portal/my-org/team-1/context/manage
│ └── [embeddingID]/ → /portal/my-org/team-1/context/manage/abc
├── training/
│ ├── page.tsx → /portal/my-org/team-1/training
│ ├── new/ → /portal/my-org/team-1/training/new
│ └── jobs/
│ ├── page.tsx → /portal/my-org/team-1/training/jobs
│ └── [jobID]/ → /portal/my-org/team-1/training/jobs/abc
└── settings/
└── team/ → /portal/my-org/team-1/settings/team
Dynamic Segments¶
| Segment | Purpose |
|---|---|
[organizationSlug] |
Organization identifier (e.g., "my-org") |
[teamID] |
Team UUID |
[sessionID] |
Session UUID |
[chatID] |
Chat UUID |
[embeddingID] |
Embedding UUID |
[jobID] |
Training job UUID |
[inviteID] |
Invitation UUID |
Accessing Parameters¶
In page components:
// Server Components
export default function Page({ params }: {
params: {
organizationSlug: string;
teamID: string;
}
}) {
const { organizationSlug, teamID } = params;
}
In client components:
"use client";
import { useParams } from "next/navigation";
export function MyComponent() {
const params = useParams();
const slug = params.organizationSlug as string;
}
Layouts¶
Root Layout¶
app/layout.tsx
Wraps entire app with providers:
export default function RootLayout({ children }) {
return (
<html>
<body>
<TanstackProvider>
<ConditionalLayout>
<Toaster />
{children}
</ConditionalLayout>
</TanstackProvider>
</body>
</html>
);
}
Profile Layout¶
app/profile/layout.tsx
Adds profile sidebar:
export default function ProfileLayout({ children }) {
return (
<div className="flex">
<ProfileSidebar />
<main>{children}</main>
</div>
);
}
Portal Layout¶
app/portal/[organizationSlug]/layout.tsx
Adds portal sidebar and role provider:
export default function PortalLayout({ children, params }) {
return (
<RoleProvider>
<div className="flex">
<PortalSidebar slug={params.organizationSlug} />
<main>{children}</main>
</div>
</RoleProvider>
);
}
Route Protection¶
Handled by proxy.ts middleware:
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|public).*)"
]
};
export async function proxy(request: NextRequest) {
const isProtectedRoute = ["/portal", "/pay", "/profile"].some(
path => request.nextUrl.pathname.startsWith(path)
);
const isAuthRoute = ["/login", "/signup"].some(
path => request.nextUrl.pathname.startsWith(path)
);
const session = await getSession(request);
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL("/login", request.url));
}
if (isAuthRoute && session) {
return NextResponse.redirect(new URL("/portal", request.url));
}
return NextResponse.next();
}
Navigation Patterns¶
Link Component¶
Programmatic Navigation¶
import { useRouter } from "next/navigation";
const router = useRouter();
router.push(`/portal/${slug}/${teamID}/sessions/${sessionId}`);
Prefetching¶
Next.js automatically prefetches visible links. For manual control:
API Routes¶
Only one API route for client logging:
All other API calls go to external services via fetchJSON.