Formatting
Prettier. Configuration in .prettierrc:
- Single quotes
- Semicolons required
- Trailing commas (ES5)
- Print width 100
- Arrow parens: avoided (when possible)
- 2-space indentation
Run pnpm format to format all files. The pre-commit hook (husky + lint-staged) runs Prettier automatically on staged files with prettier --write --ignore-unknown. You do not need to run Prettier manually before committing — the hook handles it.
Linting
ESLint via flat config (eslint.config.mjs) using eslint-config-next (core-web-vitals + TypeScript). Run pnpm lint. Ignores: .next/, out/, build/, dist/, .vercel/, node_modules/, public/_pagefind/.
TypeScript
- Strict mode is enabled.
- Prefer explicit return types on exported functions.
- Avoid
any. Useunknownfor truly unknown inputs and narrow with guards. - Use
import type { ... } from './foo'for type-only imports. - Keep runtime code and type definitions separate. Types live in
types/.
Path alias
@/ maps to the project root (configured in tsconfig.json → paths: { '@/*': ['./*'] }).
Use @/ for all internal imports. Never use relative paths that go more than one level up (e.g. avoid ../../lib/foo).
'server-only' guard
Any module that:
- Imports Node.js-only APIs (crypto, fs, etc.)
- Reads environment secrets
- Calls the Supabase service-role client
- Accesses
'server-only'imports from other modules
must add import 'server-only'; as the first non-comment line. This prevents accidental client-bundle inclusion and produces a clear build error if imported in a client context.
Tailwind + className
Use cn() from @/lib/utils for all conditional or merged class strings:
import { cn } from '@/lib/utils';
<div className={cn('base-class', isActive && 'active-class', className)} />;cn() wraps clsx + tailwind-merge. Never string-concatenate Tailwind classes — the merge will not deduplicate conflicting classes.
Database access pattern
All database access goes through typed repositories in lib/supabase/. Route handlers import functions from lib/supabase/ and call them. Never write createSupabaseAdminClient().from('table').select() directly in a route handler.
Every repository module maps to specific tables:
| Module | Tables |
|---|---|
user-repository.ts | users |
organization-repository.ts | organizations, organization_memberships |
repository.ts | repositories |
installation-repository.ts | github_installations, installation_repositories |
signals.ts | signals, users (via ensureUsersExist), repositories |
ingest-state.ts | ingest_log |
repo-cache.ts | repository_sync_state, repositories, organizations |
team-cache-repository.ts | teams, team_members, team_repositories |
leaderboard-db.ts | scoring_presets (+ 7 child tables), leaderboard_materializations, computed_scores |
Use the Supabase admin client obtained via createSupabaseAdminClient() from lib/supabase/server.ts. This uses the service-role key — do not use the anon/publishable key for server-side operations.
Authorization pattern
Use the guard chain in order. Each guard short-circuits:
requireApiSession → requireOrganizationAccess → requireOrganizationAdmin → resolveInstallationForOrganizationGuard locations:
| Guard | File |
|---|---|
requireApiSession | lib/api/server.ts |
requireOrganizationAccess | lib/api/server.ts |
requireOrganizationAdmin | lib/api/server.ts |
resolveInstallationForOrganization | lib/leaderboard/resolve-installation.ts |
requireDebugAccess | lib/auth/debug.ts |
getRequestSession | lib/auth/request-session.ts |
Do not inline session/org/admin checks in route handlers — use the standard guards. Exceptions where inline checks exist (bulk-role, teams/bulk-add) are a known issue.
API responses
- Use
NextResponse.json()for successful responses. - Use
apiError(message, status)fromlib/api/server.tsfor error responses. - Use
parseJsonBody(request, { schema })for validated JSON body parsing (Zod schema integration). - Use
jsonErrorResponsefromlib/auth/response.tsfor auth-specific errors.
React components
- Client components (using browser APIs, hooks, state, effects): add
'use client'directive at the top. - Server components: default (no directive). Prefer server components for leaf pages that only pass data to client components.
- Server components import and render client components — not the reverse.
Query hooks
- Use the
queryKeysfactory fromhooks/query-keys.tsfor all new query keys. Do not write inline literal arrays for query keys. - Set
staleTimeusing theQUERYconstants fromlib/constants/time.tsrather than hardcoded values. - Match the
queryKeyto the endpoint exactly — if a query parameter is part of the request, include it in the key to avoid stale cache issues.
Naming
- Files: kebab-case for
lib/,components/,hooks/directories, but PascalCase for React component files. - Functions: camelCase (
getUserProfile,runIngest). - Types and interfaces: PascalCase (
AuthSession,ScoringRuleset). - Constants: SCREAMING_SNAKE_CASE (
INGEST_COOLDOWN_MS,USER_GITHUB_GRAPHQL_OPERATIONS). - Database tables: snake_case (
auth_sessions,leaderboard_materializations). - Environment variables: SCREAMING_SNAKE_CASE (
GITHUB_APP_ID,SUPABASE_SERVICE_ROLE_KEY).
Docs MDX
Every documentation file in content/ must have frontmatter with title and description. Use order (number) if sidebar position matters beyond alphabetical.
Use Mermaid code blocks for diagrams:
```mermaid
flowchart TD
A --> B
```Update the relevant _meta.ts when adding a new file. The _meta.ts key must match the filename without extension.
Package manager
pnpm only. Never use npm install or yarn add. Update pnpm-lock.yaml by running pnpm install. The CI enforces this with --frozen-lockfile.