Repository Architecture
Polar Edge — Application Architecture
This document describes the structural conventions used across the Polar Edge monorepo. Follow these patterns when adding features, refactoring, or creating new apps.
Table of Contents
-
Layout
Architecture
Conventions
Anatomy
Pattern
Caching
Client
Routes
Components
Rules
Monorepo Layout
The repository is a [Turborepo](https://turbo.build)Turborepo monorepo managed with [pnpm workspaces](https://pnpm.io/workspaces)workspaces.
polar-edge/Key rules:**
- Business logic lives in
`apps/— packages are pure utilities with no app-specific logic.` - Shared packages are consumed as workspace dependencies (
`@repo/,ui`ui`@repo/tba-, etc.).sdk`sdk - Run tasks through Turbo:
`turbo,build`build`turbo dev --filter=, etc. Don't run app scripts directly unless you have a reason.scouting`scouting
Feature-First Architecture
Both Next.js apps (`scouting`scouting and `basecamp-) use a fe`fe**feature-first**first layout. The `src/app/ directory is intentionally thin — it contains only Next.js routing files. All business logic, data access, and feature-specific components live in ``src/features/.`
src/
```text
src/
├── app/ ← Routing only. Pages delegate to features.
├── features/ ← Primary home for all business logic.
│ └── <feature>/
│ ├── actions.ts # Server actions
│ ├── queries.ts # Database reads
│ ├── types.ts # Feature-specific types
│ ├── components/ # Feature-specific React components
│ ├── hooks/ # Feature-specific client hooks
│ └── contexts/ # Feature-specific React contexts
├── components/ ← Shared UI used across multiple features (nav, layout, etc.)
└── lib/ ← Pure utilities and library config. No feature logic.
```
The benefit: when you need to find or change anything about the "picklist" feature, you go to `src/features/picklist/. You don't have to hunt across ``src/app/, ``src/utils/, ``src/api/, etc.`
App Router Conventions
Pages are thin
Pages (`page.) should authenticate, load data, and compose feature components. They should not contain business logic themselves.tsx`tsx
// ✅ Good — app/admin/members/page.tsxThe data-fetching and rendering logic lives in `MembersContent`MembersContent, which is an async RSC imported from `src/features/org/members/.`
Async Server Components for data
Prefer async RSCs for data fetching. Place the async component in `features/ and import it into the page:`
// features/org/members/components/MembersContent.tsxSuspense boundaries
Wrap async data-fetching components in `<Suspense> in the page. Provide a meaningful skeleton as the fallback — not just a spinner. This enables streaming and prevents the whole page from blocking.`
<Suspense fallback={<LoadingTable />}>Feature Module Anatomy
Each feature in `src/features/<feature>/ follows this file structure. Not every file is required — only create what the feature needs:`
| File / Directory | Purpose |
|---|---|
actions.ts |
Next.js Server Actions ("use server"). Mutations only — auth, validate, write DB. |
queries. |
Database reads. Always import "server-only". Use "use cache" for expensive reads. |
types. |
TypeScript types and interfaces specific to this feature. |
components/ |
React components used only by this feature. |
hooks/ |
Client-side React hooks used only by this feature. |
contexts/ |
React context providers used only by this feature. |
*.test. |
Tests co-located with the code they test. |
Example — `picklist`picklist feature:**
src/features/picklist/Example — nested features (`org`org):**
When a domain has sub-features with distinct concerns, nest them:
src/features/org/Server Actions Pattern
Server actions live in `features/<feature>/actions. and always follow this order:ts`ts
"use server";Rules:
- Always return
`{ error: string }or``{ success: true, ... }— never throw.` - Use
`db.transaction()when writing to multiple tables atomically.` - Validate every external input with Zod before touching the database.
- Do not perform mutations from
`queries.— keep reads and writes separate.ts`ts
Data Queries & Caching
Database reads live in `features/<feature>/queries.. Always mark this file as server-only:ts`ts
import "server-only";This prevents accidental import in client components and gives a clear build-time error if it happens.
Use `"use cache"` for expensive reads
For reads that are called frequently or are expensive, use Next.js's `"use cache" directive with ``cacheLife`cacheLife and `cacheTag`cacheTag:
import "server-only";Cache tag centralization
All cache tag strings are defined in `src/lib/cache.. Never hardcode tag strings at the call site — always use the ts`ts`cacheTags`cacheTags object:
// src/lib/cache.tsWhen a mutation invalidates cached data, call `revalidateTag(cacheTags.leaderboardStand(orgId)) in the relevant server action.`
Parallel data fetching
When a page or component needs multiple independent pieces of data, fetch them in parallel with `Promise.:all`all
const [organization, members] = await Promise.all([RSC Boundaries — Server vs Client
Next.js components are **Server Components by default**default. Only add `"use client" when a component needs browser APIs, state, or event handlers.`
Server Components (default)
- Fetch data directly (async/await)
- Access cookies, headers, environment variables
- Import server-only code (DB, auth, etc.)
- Cannot use
`useState`useState,`useEffect`useEffect, or browser APIs
Client Components (`"use client"`)
- Interactive UI — event handlers, state, effects
- Browser APIs (
`window`window,`localStorage`localStorage, timers) - Third-party client libraries
- React context providers that hold state
Minimize the client boundary
Push `"use client" as deep into the component tree as possible. A good pattern: keep the page and data-fetching wrapper as RSCs, and only mark leaf components (buttons, forms, interactive widgets) as client:`
// ✅ Good — page stays server, only the interactive button is clientContext providers
Context providers must be client components (they use React state internally). The pattern: create a `<FeatureProvider> client component that wraps its children, and mount it as high as needed in the tree without making parent RSCs into client components.`
// features/totp/contexts/TOTPContext.tsxCentralized Routes
All route paths are defined in `src/lib/routes.. Never hardcode route strings in components or actions — always import from ts`ts`routes`routes.
// src/lib/routes.tsUsage:
import { routes } from "@/lib/routes";Why:** Refactoring a URL means changing one place. TypeScript catches broken references at build time.
Parallel Routes (Conditional UI)
When a page needs to show fundamentally different UI based on auth state (or another condition), use Next.js [parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes)routes rather than conditional rendering in a single component.
The `basecamp- app uses this for its auth gate:fe`fe
app/
```text
app/
├── layout.tsx # Renders both slots: {props.auth} and {props.unauth}
├── @auth/
│ └── page.tsx # Shown when authenticated
└── @unauth/
└── page.tsx # Shown when unauthenticated
```
// app/layout.tsxEach slot independently validates auth state and returns `null`null if not applicable:
// app/@auth/page.tsxWhen to use parallel routes:**
- Auth gates where the two states are visually distinct pages (not just a hidden button)
- Dashboard layouts with independently loading panels
- Modal overlays with intercepting routes
When NOT to use them:** Simple show/hide toggles — use conditional rendering in a single component.
Shared UI Components
@repo/ui`ui — cross-app design system
Radix UI primitives with Tailwind v4 styling. Import from the package path:
import { Button } from "@repo/ui/components/button";Use `@repo/ components as building blocks. Don't recreate primitives (buttons, inputs, dialogs) from scratch.ui`ui
src/components/` — cross-feature app components
Components that are used by multiple features within the same app but aren't generic enough for `@repo/:ui`ui
Rule:** If a component is only used by one feature, it belongs in `src/features/<feature>/components/, not ``src/components/.`
Lib Directory Rules
src/lib/ is for pure utilities and library configuration. It has no feature logic.`
| Path | Contents |
|---|---|
lib/auth. |
Better Auth configuration |
lib/auth-client. |
Client-side auth helpers |
lib/permissions. |
Super admin checks |
lib/routes. |
Centralized route definitions |
lib/cache. |
Cache tag constants |
lib/utils. |
Generic utility functions (cn, etc.) |
lib/compress-image. |
Image compression utility |
lib/database/ |
Drizzle client, schema tables, relations, views, types |
lib/server/ |
Server-side helpers: auth guards, org helpers, storage, TBA, tokens |
lib/offline/ |
Network status, offline queue context, toasts |
What does NOT go in `lib/:`**
- Feature-specific actions, queries, or components
- Anything that imports from
`features/`
Where Does My Code Go?
Use this decision tree when adding new code:
Is it a Next.js routing file (page, layout, loading, error)?Quick Reference
| Convention | Rule |
|---|---|
| Default RSC | All components are server by default; "use client" only when needed |
| Data fetching | Async RSC + queries. with "use cache" for expensive reads |
| Mutations | Server Actions in actions.; always auth → validate → transact → return |
| Routes | All paths in src/lib/routes.; never hardcode strings |
| Cache tags | All tags in src/lib/cache. via cacheTags object |
| Feature isolation | Each feature is self-contained; no cross-feature imports |
| Client boundary | Push "use client" as deep as possible |
server- |
All queries. files start with import "server-only" |
| UI primitives | Use @repo/ — don't reinvent buttons, dialogs, tables |
| Suspense | Wrap async RSCs in <Suspense> with skeleton fallbacks |