# Code Conventions

**Last updated:** 2026-05-14
**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source`
**Apps:** `apps/main`, `apps/external`, `apps/api`, `apps/admin`, `apps/chat-worker`, `apps/clients`
**Packages:** `packages/shared`, `packages/ui`, `packages/shared-types`, `packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`

## Linter & Formatter — Biome

The repo uses a single root [Biome](https://biomejs.dev) config: `biome.json` (Biome 2.2.5, pinned in root `package.json` devDependencies).

Formatting rules (from `biome.json`):
- `indentStyle: "space"`, `indentWidth: 2`
- `lineEnding: "lf"`, `lineWidth: 100`
- JS: `quoteStyle: "double"`, `jsxQuoteStyle: "double"`, `semicolons: "always"`, `trailingCommas: "es5"`, `arrowParentheses: "always"`, `bracketSpacing: true`, `bracketSameLine: false`
- JSON formatter enabled with same indent settings; parser allows comments but not trailing commas

Key lint rules turned on (Biome `recommended: false` — rules are explicit):
- `suspicious.noExplicitAny: "error"` (overridden to `off` for some files via overrides; warn in `xtablo-expo`)
- `correctness.noUnusedVariables`, `noUnusedImports`, `noUndeclaredVariables` — all `error`
- `style.useConst`, `useTemplate`, `noNamespace`, `noCommonJs` — all `error`
- `complexity.noBannedTypes`, `suspicious.noDebugger`, `suspicious.noEmptyBlockStatements` — all `error`

Per-package scripts wrap Biome:
- `pnpm lint` → `turbo lint` → each package runs `biome check .`
- `pnpm lint:fix` → `biome check --write .`
- `pnpm format` → `biome format --write .`

## TypeScript Conventions

Every package/app ships its own `tsconfig.json`; there is no root TS config.

Strictness — every config sets `strict: true`. Additional flags consistent across configs:
- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true`
- `noUncheckedIndexedAccess: true` for `packages/shared-types`, `packages/shared`
- `isolatedModules: true`, `moduleDetection: "force"`, `skipLibCheck: true`
- API uses `verbatimModuleSyntax: true` (`apps/api/tsconfig.json`) — type-only imports must be explicit

Module systems:
- Frontend apps use `module: ESNext` + `moduleResolution: "bundler"` (e.g. `apps/main/tsconfig.app.json`)
- API uses `module: NodeNext` (TypeScript compiled output via `tsc`)
- All packages declare `"type": "module"` in package.json

Path aliases (defined per app, resolved by Vite via `vite-tsconfig-paths`):
- `apps/main`: `@ui/*` → `./src/*`, `@xtablo/auth-ui` → `../../packages/auth-ui/src`, `@xtablo/ui/*` → `../../packages/ui/src/*` (see `apps/main/tsconfig.app.json`)
- `apps/main/tsconfig.json`: also `@external/*` → `src/external/*`

Package import discipline (per `CLAUDE.md`):
- No circular dependencies between packages
- `apps/api` may only import from `@xtablo/shared-types`
- Frontend apps may import from all shared packages
- `@xtablo/shared` and `@xtablo/ui` are **source-only** — TypeScript is consumed directly via `vite-tsconfig-paths`; no build step

Types-first workflow:
- Database types are auto-generated into `packages/shared-types/src/database.types.ts` via `npx supabase gen types typescript`
- Domain types in `@xtablo/shared-types` are derived from `database.types.ts` (nulls removed, refined shapes)
- API response types live in the same package so frontends and the API agree

## React Component Conventions

- Functional components only (no class components observed)
- TSX files use named exports for components, e.g. `export function CustomModal(...)` (see `apps/main/src/components/CustomModal.tsx`)
- Co-located unit tests: `Foo.tsx` + `Foo.test.tsx`
- Naming suffix conventions (per `CLAUDE.md`):
  - Dialogs / modals → `*Modal.tsx` (e.g. `CustomModal.tsx`)
  - Page sections → `*Section.tsx` (e.g. `TabloEventsSection.tsx`)
  - Card surfaces → `*Card.tsx` (e.g. `EventTypeCard.tsx`, `AvailabilityCard.tsx`, `EventTypeCard.test.tsx`)
- Shared primitives (Radix + Tailwind) live in `packages/ui/src`
- Cross-app business hooks/contexts live in `packages/shared/src`

## Hook Patterns

React Query (TanStack Query v5) is the primary server-state tool. Standard return shapes:

```ts
// Queries
const { data, isLoading, error } = useMyQuery();

// Mutations
const { mutate, isPending } = useMyMutation();
```

- Default cache time: 5 minutes (configured in `packages/shared`'s QueryClient)
- Mutations invalidate targeted keys explicitly rather than blowing away the whole cache

Zustand handles global client state (notably the authenticated user store):
- `useUser()` — throws if no session (use inside protected routes)
- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public surfaces)

## Query Key Conventions

Hierarchical keys, with the resource name first and identifiers cascading deeper:

```ts
["tablos"]                 // list
["tablos", tabloId]        // single
["tablo-files", tabloId]   // related collection
```

Invalidations should match the deepest key that needs to be refreshed.

## Error Handling

- User-facing: `toast.add({ ... })` (toast system in `packages/shared` / `packages/ui`) — messages should be friendly and actionable
- Technical: `console.error` for developer-only context (stack traces, raw API errors)
- API errors are caught at the React Query hook layer and surfaced via `error` from `useQuery`/`useMutation`; UI components branch on `isError`
- Server side (`apps/api`): Hono routers return `c.json({ error: ... }, statusCode)`; middleware handles auth failures with 401s (see `docs/MIDDLEWARE_TESTS.md`)

## Loading States — three levels

1. **Route level** — `ProtectedRoute` shows a full-page spinner while the session resolves
2. **Feature level** — React Query `isLoading` / `isPending` drives section-level skeletons or spinners
3. **Action level** — Buttons set `disabled` during the related mutation's `isPending`

Empty / error states are explicit branches (no silent fallbacks).

## Import / Export Patterns

- ESM throughout (`"type": "module"` in every package.json; Biome enforces `noCommonJs`)
- Source-only packages (`@xtablo/shared`, `@xtablo/ui`) export from `src/index.ts` and are consumed via TS path aliases — no `dist/` involved
- Compiled packages (`@xtablo/shared-types` and `apps/api`) emit `dist/` via `tsc`; `shared-types` includes `declaration: true` and `declarationMap: true`
- Type-only imports preferred where supported (`verbatimModuleSyntax` is on for the API)
- Biome's `noUnusedImports` rule will flag dead imports at lint time

## File Organization

```
apps/<app>/src/
  components/        # UI components, *.tsx + *.test.tsx
  contexts/          # React contexts (e.g. UpgradeBlockContext.tsx)
  providers/         # Store / provider wrappers (e.g. UserStoreProvider.tsx)
  hooks/             # App-specific hooks
  pages/ or routes/  # Route entry points
  lib/               # Utilities, route table, api client setup
  utils/testHelpers  # Render-with-providers wrappers for tests
packages/<pkg>/src/
  index.ts           # Single barrel export
  ...                # Domain folders mirror app layout
```

API layout (`apps/api/src`):
- `routers/` — Hono routers grouped by concern (`public.ts`, `authRouter.ts`, `tablo.ts`, `stripe.ts`, etc.)
- `middlewares/` — Auth, Supabase, R2, Stream, email injection
- `helpers/` — Pure logic (testable in isolation, e.g. `helpers/orgIcons.ts` + `orgIcons.test.ts`)
- `__tests__/` — Test-only fixtures, setup, globalSetup, route + middleware suites

## Type Safety

- `noExplicitAny` is on as `error` in the root config (relaxed to `warn` in `xtablo-expo` and `off` for select API/legacy file overrides)
- Prefer derived types from `@xtablo/shared-types` over inline shape literals
- `Database` table types are generated; domain types should narrow them rather than redeclare from scratch
- `noUncheckedIndexedAccess: true` in shared packages — index access returns `T | undefined`; handle the undefined branch explicitly
- API enforces `verbatimModuleSyntax`, so `import type { ... }` is required for type-only imports — relevant for compiled API code

## Reference files

- `biome.json` — single source for lint + format
- `apps/main/tsconfig.app.json` — canonical frontend TS config
- `apps/api/tsconfig.json` — canonical backend TS config
- `packages/shared-types/tsconfig.json` — type-only package config
- `CLAUDE.md` — high-level conventions (this doc expands it with verified specifics)
