8.3 KiB
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 config: biome.json (Biome 2.2.5, pinned in root package.json devDependencies).
Formatting rules (from biome.json):
indentStyle: "space",indentWidth: 2lineEnding: "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 toofffor some files via overrides; warn inxtablo-expo)correctness.noUnusedVariables,noUnusedImports,noUndeclaredVariables— allerrorstyle.useConst,useTemplate,noNamespace,noCommonJs— allerrorcomplexity.noBannedTypes,suspicious.noDebugger,suspicious.noEmptyBlockStatements— allerror
Per-package scripts wrap Biome:
pnpm lint→turbo lint→ each package runsbiome 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: truenoUncheckedIndexedAccess: trueforpackages/shared-types,packages/sharedisolatedModules: 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 viatsc) - 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/*(seeapps/main/tsconfig.app.json)apps/main/tsconfig.json: also@external/*→src/external/*
Package import discipline (per CLAUDE.md):
- No circular dependencies between packages
apps/apimay only import from@xtablo/shared-types- Frontend apps may import from all shared packages
@xtablo/sharedand@xtablo/uiare source-only — TypeScript is consumed directly viavite-tsconfig-paths; no build step
Types-first workflow:
- Database types are auto-generated into
packages/shared-types/src/database.types.tsvianpx supabase gen types typescript - Domain types in
@xtablo/shared-typesare derived fromdatabase.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(...)(seeapps/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)
- Dialogs / modals →
- 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:
// 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:
["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 inpackages/shared/packages/ui) — messages should be friendly and actionable - Technical:
console.errorfor developer-only context (stack traces, raw API errors) - API errors are caught at the React Query hook layer and surfaced via
errorfromuseQuery/useMutation; UI components branch onisError - Server side (
apps/api): Hono routers returnc.json({ error: ... }, statusCode); middleware handles auth failures with 401s (seedocs/MIDDLEWARE_TESTS.md)
Loading States — three levels
- Route level —
ProtectedRouteshows a full-page spinner while the session resolves - Feature level — React Query
isLoading/isPendingdrives section-level skeletons or spinners - Action level — Buttons set
disabledduring the related mutation'sisPending
Empty / error states are explicit branches (no silent fallbacks).
Import / Export Patterns
- ESM throughout (
"type": "module"in every package.json; Biome enforcesnoCommonJs) - Source-only packages (
@xtablo/shared,@xtablo/ui) export fromsrc/index.tsand are consumed via TS path aliases — nodist/involved - Compiled packages (
@xtablo/shared-typesandapps/api) emitdist/viatsc;shared-typesincludesdeclaration: trueanddeclarationMap: true - Type-only imports preferred where supported (
verbatimModuleSyntaxis on for the API) - Biome's
noUnusedImportsrule 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 injectionhelpers/— Pure logic (testable in isolation, e.g.helpers/orgIcons.ts+orgIcons.test.ts)__tests__/— Test-only fixtures, setup, globalSetup, route + middleware suites
Type Safety
noExplicitAnyis on aserrorin the root config (relaxed towarninxtablo-expoandofffor select API/legacy file overrides)- Prefer derived types from
@xtablo/shared-typesover inline shape literals Databasetable types are generated; domain types should narrow them rather than redeclare from scratchnoUncheckedIndexedAccess: truein shared packages — index access returnsT | undefined; handle the undefined branch explicitly- API enforces
verbatimModuleSyntax, soimport type { ... }is required for type-only imports — relevant for compiled API code
Reference files
biome.json— single source for lint + formatapps/main/tsconfig.app.json— canonical frontend TS configapps/api/tsconfig.json— canonical backend TS configpackages/shared-types/tsconfig.json— type-only package configCLAUDE.md— high-level conventions (this doc expands it with verified specifics)