Each repository has its own docs/CODING_STANDARDS.md. Those files are partly out of date. This page states what the tooling enforces today and what is convention.

Rules shared by all three repositories

  • TypeScript strict mode. Do not relax compiler options.
  • No any. Use unknown and narrow it, or model the type.
  • No hand-written TypeScript enum. Use an as const object with a union type, as StudioRole and ErrorCode do in @perform/types. Prisma’s generated enums are fine.
  • Use import type for type-only imports.
  • Add dependencies with pnpm add, never by editing package.json by hand.
  • Check whether a component or helper already exists before creating one.
  • No AI attribution in commits, pull requests or code.
  • User-facing text goes through the translation system, never a hardcoded string.
  • Hebrew and right-to-left layout are the default. Test both directions.

Comments

The written standard in each repository says to avoid comments and let names and types carry the meaning. In practice the code contains explanatory comments where a decision is not obvious, for example the trust proxy setting in app.ts or the memory ceiling in ecosystem.config.cjs. The enforced part, in the backend only, is: no comment may start with TODO, FIXME or XXX, comments must start with a capital letter, and there must be a space after the comment marker. Follow the pattern in the code you are editing. Do not add comments that restate the code. Do keep a short comment that records why something surprising is correct.

backend

Enforced by ESLint (eslint.config.mjs)

@typescript-eslint/no-explicit-any is switched off for the ported @repo/* packages, tests and scripts. It still applies to apps/core-api/src and the @perform/* packages.

Formatting

Prettier, configured in .prettierrc.json: 100 columns, single quotes, trailing commas everywhere, semicolons, two spaces, LF line endings.

Module structure

  • ESM only. Every relative import ends in .js so the compiled output resolves under NodeNext.
  • A domain module lives in apps/core-api/src/modules/<name> with five files: routes, controller, service, repository and schema. Larger modules add helper files next to them.
  • Dependencies point one way: routes -> controller -> service -> repository -> prisma.
  • Routes wire dependencies from AppContext and mount handlers.
  • Controllers parse input with Zod, call a service and shape the response. They never touch Prisma.
  • Services hold business rules and throw AppError(ErrorCode.X, message). They never touch Express objects.
  • Repositories are the only place that touches Prisma.
  • Everything is built with factory functions: createClientsService, createClientsRepository, createClientsController.
  • Prefer type over interface.
  • Import shared packages by name (@perform/errors), never by a deep path.

Tenancy

Every domain row belongs to a studio. Repositories filter by studioId and services take it as an argument. Never trust a studioId from a request body. Take it from the authenticated principal: req.auth.studioId on the web and bearer lanes, the trainee principal on the trainee lane, the API key’s studio on the partner and automation lanes.

Errors and logging

  • Zod parse failures become VALIDATION responses automatically through the error middleware.
  • Never swallow an error with an empty catch.
  • Log with createLogger from @perform/logger. Fields object first, message second: log.info({ studioId }, 'created').
  • Do not log request bodies. See Logging for what is redacted.

Schema changes

A model change touches two Prisma schemas and needs a hand-written migration. See Database migrations.

frontend

Enforced by tooling

Conventions

  • Server Components by default. Add "use client" only for interactivity or browser APIs, and keep the client boundary small.
  • Read domain data in a server page.tsx. Write in a Server Action in an actions.ts file. Both call the backend through the server-only client. The browser never calls the core API directly.
  • Use TanStack Query for client-side server state and the oRPC utilities for typed calls to /api/rpc.
  • Forms use react-hook-form with a Zod resolver.
  • Feature code lives in apps/saas/modules/<feature>. Shared UI primitives live in packages/ui.
  • Use the path aliases (@/, @shared/, @auth/ and the others in tsconfig.json), not long relative paths.
  • Add UI primitives with the shadcn CLI: pnpm dlx shadcn@latest add <component>.
  • Use logical CSS utilities (ms-*, me-*, text-start) instead of left and right ones, so layouts mirror in RTL.
  • No box shadows in the UI.
  • Icons come from lucide-react or react-icons. Do not hand-write SVG icons.
  • The Next.js request interceptor is apps/saas/proxy.ts and exports proxy. Next.js 16 renamed middleware to proxy. Do not add a middleware.ts.
  • Components are named function exports in PascalCase files. Folders are kebab-case.
  • Translations live in packages/i18n/translations/<locale>. The default locale is he. Add a key to every locale.

mobile

Enforced by tooling

  • ESLint with eslint-config-expo (eslint.config.js). The React Compiler lint rules (react-hooks/refs, set-state-in-effect, preserve-manual-memoization, immutability, purity) and exhaustive-deps are set to warn, not error.
  • Prettier on staged files.
  • tsc --noEmit in CI. Typed routes are on, so route strings are checked. If route types look stale, regenerate .expo/types by starting Metro. Do not cast route strings.

Conventions

  • Routes in src/app stay thin. A route file renders one screen from a feature.
  • A feature is a vertical slice in src/features/<name> with data (typed API functions), hooks (TanStack Query), components and presentation (screens). Some features add lib, store or controllers.
  • Server state is TanStack Query. Client state is Zustand. Do not copy server data into a store.
  • src/lib/api/client.ts is the only place that calls fetch for the API. Features call it through their data layer.
  • Use the @/ alias for imports from src.
  • Style with tokens from @/theme. Do not hardcode colours or spacing when a token exists. The brand colour is per studio and comes from the branding store.
  • Use the shared primitives in src/components/ui. Do not hand-roll buttons or text inputs.
  • Components and screens are PascalCase named exports. Hooks are useX.

Layout direction

src/lib/rtl.ts is the single source of truth for layout direction. It has one switch, MANUAL_RTL, which is currently false. That means the app uses native RTL: applyAppDirection() runs once at app entry, writes the native direction flag for the active language, and reloads the app once if the running surface started in the other direction. mobile/docs/CODING_STANDARDS.md still describes the older manual mode. The code is right.
  • The direction follows the app language the trainee picked, not the phone’s language. Hebrew and Arabic are RTL. Every other language is LTR.
  • Native RTL needs a real build. Expo Go does not apply it.
  • app.json passes no supportsRTL or forcesRTL option to the expo-localization plugin, on purpose. Those options make native code rewrite the direction from the OS locale on every cold start. Do not add them back.
  • Use the helpers exported from rtl.ts (rtlRow, rtlTextAlign, rtlInsetStart, rtlAlignItemsStart and the others) instead of hardcoding left, right or row-reverse. They return the correct value in both modes.
  • Use physicalLtr for blocks that must never mirror, such as number pickers and the Live Activity layout.
  • Directional icons never flip by themselves. Use iconBack, iconForward, iconChevronBack and iconChevronForward.
  • On web, applyAppDirection() sets dir and lang on the document element instead.

Native modules

Never import a native module at module scope in a file that runs at startup. Probe it first with requireOptionalNativeModule from expo and degrade when it is missing. The reason is explained in OTA updates. src/lib/media/shrinkImage.ts and src/features/movement/lib/cardioNotification.ts are the reference implementations.