This file provides guidance to Claude Code when working with this codebase.
DO NOT create random .md documentation files when implementing features.
- Provide explanations in chat messages
- Add comments in code where necessary
- Update existing documentation (CLAUDE.md, README.md) only if required
- Only create documentation files if explicitly requested
A T3 Stack application for team management with visual organization canvases, role & metric tracking, and multi-tenant architecture.
Core Stack:
- Next.js 15 (App Router)
- tRPC 11 with TanStack Query
- Prisma 6 with PostgreSQL (Accelerate caching)
- WorkOS AuthKit authentication
- React Flow for canvas visualizations
- Zustand for local state
- Tailwind CSS with shadcn/ui
# Development
pnpm dev # Start dev server with Turbo
pnpm build # Build for production
pnpm preview # Build and start production server
# Code Quality
pnpm check # Run linting and type checking
pnpm lint:fix # Auto-fix ESLint issues
pnpm format:write # Format code with Prettier
# Database
pnpm db:generate # Generate Prisma client
pnpm db:push # Push schema changes
pnpm db:studio # Open Prisma Studio
# Testing
pnpm exec playwright test # Run all tests
pnpm exec playwright test --project=chromium # Chromium only
pnpm exec playwright test tests/auth-authenticated.spec.ts # Specific filesrc/
βββ app/ # Next.js pages
β βββ _components/ # Root-level page components (landing)
β βββ api/ # API routes (cron, callbacks)
β βββ dashboard/[teamId]/ # Metrics dashboard
β βββ docs/ # MDX documentation
β βββ integration/ # Integration management
β βββ metric/_components/ # Metric dialogs (per provider)
β βββ org/ # Organization settings
β βββ public/ # Public-facing views
β βββ teams/[teamId]/ # Team canvas (React Flow)
β
βββ components/ # Shared UI
β βββ ui/ # shadcn/ui components (54 files)
β βββ charts/ # Recharts wrappers
β βββ navbar/ # Navigation
β βββ react-flow/ # BaseNode, BaseHandle, ZoomSlider
β
βββ lib/ # Utilities
β βββ canvas/ # Reusable React Flow library
β βββ integrations/ # Provider configurations
β βββ metrics/ # Transformer types
β βββ helpers/ # Helper functions
β
βββ server/ # Server-only code
β βββ api/
β β βββ routers/ # tRPC routers (12 total)
β β βββ services/ # Business logic
β β βββ utils/ # Authorization, caching
β βββ db.ts # Prisma singleton
β
βββ trpc/ # tRPC client setup
βββ providers/ # React context providers
βββ hooks/ # Shared hooks
βββ middleware.ts # Auth middleware
WorkOS middleware runs on all routes. Public routes: /, /docs, /public/*.
// tRPC: protectedProcedure validates ctx.user
// No manual auth checks needed in components
// NavBar uses try-catch for graceful auth handling// Server Components: Direct calls (10x faster)
import { api } from "@/trpc/server";
const data = await api.team.getById({ id });
// Client Components: React hooks with TanStack Query
import { api } from "@/trpc/react";
const { data } = api.team.getById.useQuery({ id });- Create router in
src/server/api/routers/[name].ts - Use
protectedProcedureorworkspaceProcedure - Add to
appRouterinsrc/server/api/root.ts - Add authorization checks using utils from
authorization.ts
import {
getMetricAndVerifyAccess,
getRoleAndVerifyAccess,
getTeamAndVerifyAccess,
} from "@/server/api/utils/authorization";
// Always verify resource belongs to user's organization
const team = await getTeamAndVerifyAccess(db, teamId, userId, workspace);The team canvas (/teams/[teamId]) is a React Flow-based visualization with 30 files.
page.tsx (Server)
β Prefetch: role.getByTeamId, organization.getMembers
β enrichNodesWithRoleData(storedNodes)
β <HydrateClient>
β <TeamStoreProvider> (Zustand)
β <ChartDragProvider>
β <TeamCanvas> (React Flow)
| Type | Data Stored | Display Source |
|---|---|---|
| role-node | { roleId } |
TanStack Query cache |
| text-node | { text, fontSize } |
Direct node.data |
| chart-node | { dashboardMetricId } |
Database via props |
| freehand | { points } |
Session only (not saved) |
Role nodes store ONLY roleId. Display data fetched from TanStack Query cache:
// use-role-data.tsx
export function useRoleData(roleId: string) {
const { data: roles } = api.role.getByTeamId.useQuery({ teamId });
return useMemo(() => roles?.find((r) => r.id === roleId), [roles, roleId]);
}// Zustand + Context pattern in team-store.tsx
const TeamStoreContext = createContext<StoreApi<TeamStore> | null>(null);
// Access in components
const nodes = useTeamStore((state) => state.nodes);
const storeApi = useTeamStoreApi(); // For callbacks (avoids stale closures)Canvas changes β markDirty() β Debounce 2s β serializeNodes/Edges β tRPC mutation
β
beforeunload: sendBeacon fallback
Two cache layers: TanStack Query (client) and Prisma Accelerate (server).
User updates role
β onMutate: Optimistic update (instant UI feedback)
β Server mutation runs
β onSuccess:
1. setData(updatedRole) β Critical: use server response
2. invalidate() β Background refresh
β onError: Rollback to previousData
Why setData before invalidate? Prisma Accelerate cache may not propagate immediately. If we only call invalidate(), the refetch might return stale data. Setting cache with server response ensures correct data.
Key files:
src/hooks/use-optimistic-role-update.ts- Shared hook for all role mutationssrc/app/teams/[teamId]/hooks/use-update-role.tsx- Canvas-specific wrapper (adds markDirty)
Reusable patterns for React Flow canvases:
src/lib/canvas/
βββ store/create-canvas-store.tsx # Generic store factory
βββ hooks/use-auto-save.ts # Debounced save hook
βββ components/save-status.tsx # Save indicator UI
βββ edges/edge-action-buttons.tsx # Edge interaction buttons
βββ edges/floating-edge-utils.ts # Edge path calculations
βββ freehand/ # Drawing mode components
Stage 1: API β DataPoints
fetchData() β DataIngestionTransformer (AI-generated) β MetricDataPoint[]
Stage 2: DataPoints β ChartConfig
MetricDataPoint[] β ChartTransformer (AI-generated) β ChartConfig
Stage 3: ChartConfig β UI
ChartConfig β DashboardMetricChart (Recharts)
- Metric: Core metric with integrationId, templateId, pollFrequency
- MetricDataPoint: Time-series data (unique on metricId + timestamp)
- DashboardChart: Chart configuration linked to Metric
- DataIngestionTransformer: AI code for API β DataPoints
- ChartTransformer: AI code for DataPoints β ChartConfig
Cron (/api/cron/poll-metrics) runs every 15 minutes for metrics with nextPollAt <= now().
Poll frequencies: frequent (15m), hourly, daily, weekly, manual
- Create provider config in
src/lib/integrations/ - Add metric dialog in
src/app/metric/_components/[provider]/ - Create
[Provider]MetricDialog.tsx+[Provider]MetricContent.tsx - Register in
src/app/metric/_components/index.ts
The dashboard (/dashboard/[teamId]) displays metric charts with role assignments.
Uses dashboard.getDashboardCharts query which includes nested role data.
Role-metric assignment changes
β onMutate: Update both role + dashboard caches optimistically
β Server mutation
β onSuccess:
1. setData for role cache (server response)
2. invalidate both role + dashboard caches
Role assignments appear in two places:
- Role cache:
role.getByTeamId- role hasmetricIdfield - Dashboard cache:
dashboard.getDashboardCharts- chart.metric.roles array
When linking/unlinking roles to metrics, both caches must be updated for consistent UI.
Key files:
src/hooks/use-optimistic-role-update.ts- Updates both cachessrc/components/metric/role-assignment.tsx- Role assignment UI in metric drawer
Required (see .env.example):
DATABASE_URL # PostgreSQL connection
WORKOS_API_KEY # WorkOS API key
WORKOS_CLIENT_ID # WorkOS client ID
WORKOS_COOKIE_PASSWORD # 32-char session secret
NEXT_PUBLIC_WORKOS_REDIRECT_URI # OAuth callback
TEST_USER_EMAIL # Playwright test user
TEST_USER_PASSWORD # Playwright test password
Validated via @t3-oss/env-nextjs in src/env.js.
Pre-commit hooks (Husky + lint-staged):
- ESLint auto-fix
- Prettier formatting
- Runs on staged files only
Import sorting: @trivago/prettier-plugin-sort-imports with inline type imports.
Located in src/components/ui/. Use CLI for new components:
npx shadcn@latest add [component-name]Shared in src/components/react-flow/:
BaseNode- Styled node containerBaseHandle- Styled handlesZoomSlider- Zoom controls
Each integration in src/app/metric/_components/:
github/
βββ GitHubMetricDialog.tsx # Dialog wrapper
βββ GitHubMetricContent.tsx # Form content
linear/
βββ LinearMetricDialog.tsx
βββ LinearMetricContent.tsx
Uses shared MetricDialogBase from base/.
Playwright E2E tests in tests/.
- Global setup handles authentication
- Use
authenticatedPagefixture for auth-required tests - Tests run against
http://localhost:3000
- Create node component in
teams/[teamId]/_components/ - Add type to
TeamNodeunion intypes/canvas.ts - Register in
nodeTypesinteam-canvas.tsx - Update serialization in
canvas-serialization.ts
- Add procedure to appropriate router
- Use
workspaceProcedurefor org-scoped operations - Call authorization helpers for resource verification
- Invalidate dashboard cache after mutations:
import { invalidateDashboardCache } from "@/server/api/utils/cache-strategy"; await invalidateDashboardCache(ctx.db, organizationId, teamId);
MembersList: Shared component at src/components/member/member-list.tsx
- Used by canvas sidebar (
canvas-side-panels.tsx) and org page (MembersListClient.tsx) - Includes
getMemberDisplayInfo()utility for initials/name logic
Role Enrichment: Shared utilities in src/server/api/utils/organization-members.ts
enrichRolesWithUserNames()- for flat role arraysenrichChartRolesWithUserNames()- for nested chart roles- Both use shared internal helpers to avoid duplication
User Display Names:
- Client-side (sync):
getUserDisplayName(userId, members)from@/lib/helpers/get-user-name - Server-side (async WorkOS):
fetchUserDisplayName(userId)from@/server/api/utils/get-user-display-name
MetricApiLogwrites on every fetch (debugging overhead in production)- Double data point fetching on metric refresh
- Goal calculation utility is 271 lines (could be 50)