Circle V2 API Docs
    Preparing search index...

    Web App Architecture

    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-level components live next to their page.tsx in a components/ subfolder.
    • Feature-local hooks and helpers live in a lib/ subfolder next to the feature (e.g. useAuditTemplateForm.ts, clientsTableColumns.tsx).
    • Cross-feature modules live in apps/web/shared/ when they are imported by multiple route features.
    • App-wide utilities live in 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/

    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

    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
    %%{init:{"theme":"dark"}}%% 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

    %%{init:{"theme":"default"}}%% 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

    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

    • Colocate by default. Keep code next to the route that uses it unless it's reused elsewhere.
    • Move to 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.
    • Consistent shape. Every feature folder -- whether a route or a shared module -- uses components/ for UI and lib/ for hooks, queries, and helpers.
    • Colocate tests. Tests live next to the file they test (e.g. 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 flows
    • forgot-password, reset-password, verify-otp -- password recovery
    • change-password, update-password -- password management

    These 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