The main product lives in apps/web -- a Next.js 16 application using the App Router.
The app/ directory uses Next.js route groups to separate authenticated and public areas:
app/
├── (app)/ # Authenticated -- requires a logged-in user
│ ├── layout.tsx # Auth boundary + providers + app chrome
│ ├── page.tsx # Home (/)
│ ├── clients/ # /clients, /clients/[id], ...
│ ├── templates/ # /templates, /templates/chart-review, ...
│ ├── settings/ # /settings
│ └── ... # contacts, organization, template-editor
├── (auth)/ # Public auth routes
│ ├── layout.tsx # Centered layout with branding
│ ├── login/ # /login
│ ├── signup/ # /signup
│ └── ... # forgot-password, reset-password, unauthorized
└── api/ # Route handlers
├── trpc/[trpc]/ # tRPC endpoint
└── auth/ # Auth HTTP handlers
Authentication is enforced server-side in the (app) layout, not via Edge middleware:
// apps/web/app/(app)/layout.tsx
import { requireUser } from "@repo/auth-next";
export default async function RootLayout({ children }: React.PropsWithChildren) {
const user = await requireUser();
return (
<Providers>
<UserStoreProvider user={user}>
<AppLayout>{children}</AppLayout>
</UserStoreProvider>
</Providers>
);
}
requireUser() reads the session from cookies. If no valid session exists, it redirects to /login. The authenticated User object is then passed down to the client via the UserStoreProvider.
The user is stored in a Zustand store (apps/web/lib/user.store.tsx) seeded from the server layout. Client components access it via:
const user = useCurrentUser();
The provider hierarchy is defined in apps/web/lib/providers.tsx:
ThemeProvider (Chakra + next-themes)
└── AnalyticsProvider
└── QueryClientProvider (TanStack React Query)
└── TRPCProvider (tRPC HTTP batch link -> /api/trpc)
└── {children}
React Query Devtools are included in development mode (unless NEXT_PUBLIC_DISABLE_REACT_QUERY_DEVTOOLS=true).
Routes are defined with type-safe parameters and search params using defineRoute from @repo/route-kit:
// apps/web/lib/routes.ts
import { z } from "zod";
import { zPatientId } from "@repo/db/schema";
import { defineRoute } from "@repo/route-kit";
export const clientRoute = defineRoute({
path: ({ id }) => `/clients/${id}`,
params: z.object({ id: zPatientId }),
});
export const clientSessionNotesRoute = defineRoute({
path: ({ id }) => `${clientRoute.href({ id })}/session-notes`,
params: clientRoute.params.extend({}),
});
export const contactsRoute = defineRoute({
path: () => "/contacts",
search: z.object({ type: z.string().optional() }),
});
Usage in components:
// Navigate with type-safe params
clientRoute.href({ id: patientId }) // "/clients/123"
contactsRoute.href({ type: "providers" }) // "/contacts?type=providers"
defineRoute produces objects with a type-safe .href() method that validates params and search via Zod schemas.
@repo/routes-kit-next provides useRouteParams to parse Next.js dynamic route segments against the Zod schema from a route definition:
import { useRouteParams } from "@repo/routes-kit-next";
import { clientRoute } from "~/lib/routes";
const { id } = useRouteParams(clientRoute); // typed as PatientId
The web app tsconfig defines two aliases:
| Alias | Maps To | Usage |
|---|---|---|
~/ |
apps/web/ (app root) |
import { Providers } from "~/lib/providers" |
~tests |
apps/web/__tests__/ |
import { render } from "~tests" |
Feature code is organized by colocation:
page.tsx in a components/ subfolder.lib/ subfolder next to the feature (e.g. useAuditTemplateForm.ts, clientsTableColumns.tsx).apps/web/shared/ when they are imported by multiple route features.apps/web/lib/ (providers, routes, tRPC context, user store).Every route feature follows the same repeating shape:
<feature>/
├── page.tsx # Next.js page (route entry point)
├── layout.tsx # Optional nested layout
├── components/ # UI components scoped to this feature
│ ├── FeatureTable.tsx
│ └── FeatureTable.test.tsx # Tests colocated with source
├── lib/ # Hooks, queries, helpers scoped to this feature
│ ├── useFeatureQuery.ts
│ ├── useFeatureForm.ts
│ └── featureTableColumns.tsx
└── [id]/ # Dynamic nested route (optional)
├── page.tsx
├── components/
└── lib/
clients/Here is the real clients/ feature (test files omitted for brevity -- they sit next to each source file):
clients/
├── page.tsx # /clients
├── components/
│ ├── AddClientDrawer.tsx
│ ├── ClientsFilter.tsx
│ ├── ClientsFilterBadges.tsx
│ └── ClientsTable.tsx
├── lib/
│ ├── clientsTableColumns.tsx
│ ├── useAddClientForm.ts
│ ├── useClientsFilters.ts
│ ├── useClientsQuery.ts
│ ├── useCreateClient.ts
│ └── useFacilityFilterPanelProps.ts
└── [id]/
├── layout.tsx # Client detail tabs
├── page.tsx # /clients/[id]
├── components/
│ └── ClientDetail.tsx
└── session-notes/
└── page.tsx # /clients/[id]/session-notes
shared/When components or hooks are used by multiple route features, they move to apps/web/shared/<domain>/. Each shared module follows the same components/ + lib/ shape with a barrel index.ts:
shared/
├── patients/
│ ├── index.ts # Barrel export
│ ├── components/
│ │ └── PatientCombobox.tsx
│ └── lib/
│ └── usePatientCombobox.tsx
├── templates/
│ ├── index.ts
│ ├── components/
│ │ ├── ChartReviewTemplateContent.tsx
│ │ └── TemplateCombobox.tsx
│ └── lib/
│ ├── useTemplateByIdQuery.ts
│ └── useTemplateCombobox.ts
└── start-session/
├── index.ts
├── components/
│ ├── StartSessionButton.tsx
│ ├── StartSessionDrawer.tsx
│ └── RecordPanel.tsx
└── lib/
└── useStartSessionForm.ts
subgraph nestedRoute ["Nested Route (e.g. clients/[id]/)"] NestedPage["page.tsx"] NestedComponents["components/"] NestedLib["lib/ (optional)"] end
subgraph sharedModule ["shared/ modules"] SharedIndex["index.ts (barrel)"] SharedComponents["components/"] SharedLib["lib/"] end
subgraph appLib ["apps/web/lib/"] AppUtils["providers, routes, trpc context, user store"] end
routeFeature --> nestedRoute sharedModule -.->|imported by| routeFeature sharedModule -.->|imported by| nestedRoute appLib -.->|imported by| routeFeature appLib -.->|imported by| nestedRoute
subgraph nestedRoute ["Nested Route (e.g. clients/[id]/)"] NestedPage["page.tsx"] NestedComponents["components/"] NestedLib["lib/ (optional)"] end
subgraph sharedModule ["shared/ modules"] SharedIndex["index.ts (barrel)"] SharedComponents["components/"] SharedLib["lib/"] end
subgraph appLib ["apps/web/lib/"] AppUtils["providers, routes, trpc context, user store"] end
routeFeature --> nestedRoute sharedModule -.->|imported by| routeFeature sharedModule -.->|imported by| nestedRoute appLib -.->|imported by| routeFeature appLib -.->|imported by| nestedRoute
graph TD
subgraph routeFeature ["Route Feature (e.g. clients/)"]
PageTsx["page.tsx"]
LayoutTsx["layout.tsx (optional)"]
Components["components/"]
Lib["lib/"]
end
subgraph nestedRoute ["Nested Route (e.g. clients/[id]/)"]
NestedPage["page.tsx"]
NestedComponents["components/"]
NestedLib["lib/ (optional)"]
end
subgraph sharedModule ["shared/ modules"]
SharedIndex["index.ts (barrel)"]
SharedComponents["components/"]
SharedLib["lib/"]
end
subgraph appLib ["apps/web/lib/"]
AppUtils["providers, routes, trpc context, user store"]
end
routeFeature --> nestedRoute
sharedModule -.->|imported by| routeFeature
sharedModule -.->|imported by| nestedRoute
appLib -.->|imported by| routeFeature
appLib -.->|imported by| nestedRoute
shared/ when reused. If two or more route features import the same component or hook, move it to shared/<domain>/ with a barrel index.ts.components/ for UI and lib/ for hooks, queries, and helpers.ClientsTable.tsx + ClientsTable.test.tsx).The tRPC endpoint is at app/api/trpc/[trpc]/route.ts. It uses the fetch adapter from @repo/trpc/server with a context built from the session:
// apps/web/lib/trpc.context.ts
import { getLoggedInUser } from "@repo/auth-next";
export async function createTRPCContext() {
const user = await getLoggedInUser();
return { user };
}
Auth endpoints are standalone HTTP handlers under app/api/auth/:
login, signup, oauth, google -- authentication flowsforgot-password, reset-password, verify-otp -- password recoverychange-password, update-password -- password managementThese use request handlers from @repo/auth-next/api.
Key settings in apps/web/next.config.js:
| Setting | Value | Purpose |
|---|---|---|
reactCompiler |
true |
Enables the React Compiler (automatic memoization) |
reactStrictMode |
false |
Disabled for compatibility |
optimizePackageImports |
["@repo/ui", "@chakra-ui/react"] |
Tree-shake these packages |
assetPrefix |
"/v2" (production) |
Serves assets under /v2 for coexistence with legacy app |
| CORS headers | LEGACY_APP_URL |
Allows cross-origin requests from the legacy app |
Next: Data Layer