# 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//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//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)