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. Useunknownand narrow it, or model the type. - No hand-written TypeScript
enum. Use anas constobject with a union type, asStudioRoleandErrorCodedo in@perform/types. Prisma’s generated enums are fine. - Use
import typefor type-only imports. - Add dependencies with
pnpm add, never by editingpackage.jsonby 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 thetrust 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
.jsso the compiled output resolves underNodeNext. - A domain module lives in
apps/core-api/src/modules/<name>with five files:routes,controller,service,repositoryandschema. Larger modules add helper files next to them. - Dependencies point one way:
routes -> controller -> service -> repository -> prisma. - Routes wire dependencies from
AppContextand 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
typeoverinterface. - Import shared packages by name (
@perform/errors), never by a deep path.
Tenancy
Every domain row belongs to a studio. Repositories filter bystudioId 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
VALIDATIONresponses automatically through the error middleware. - Never swallow an error with an empty
catch. - Log with
createLoggerfrom@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 anactions.tsfile. 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-formwith a Zod resolver. - Feature code lives in
apps/saas/modules/<feature>. Shared UI primitives live inpackages/ui. - Use the path aliases (
@/,@shared/,@auth/and the others intsconfig.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-reactorreact-icons. Do not hand-write SVG icons. - The Next.js request interceptor is
apps/saas/proxy.tsand exportsproxy. Next.js 16 renamed middleware to proxy. Do not add amiddleware.ts. - Components are named function exports in PascalCase files. Folders are kebab-case.
- Translations live in
packages/i18n/translations/<locale>. The default locale ishe. 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) andexhaustive-depsare set to warn, not error. - Prettier on staged files.
tsc --noEmitin CI. Typed routes are on, so route strings are checked. If route types look stale, regenerate.expo/typesby starting Metro. Do not cast route strings.
Conventions
- Routes in
src/appstay thin. A route file renders one screen from a feature. - A feature is a vertical slice in
src/features/<name>withdata(typed API functions),hooks(TanStack Query),componentsandpresentation(screens). Some features addlib,storeorcontrollers. - Server state is TanStack Query. Client state is Zustand. Do not copy server data into a store.
src/lib/api/client.tsis the only place that callsfetchfor the API. Features call it through theirdatalayer.- Use the
@/alias for imports fromsrc. - 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.jsonpasses nosupportsRTLorforcesRTLoption to theexpo-localizationplugin, 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,rtlAlignItemsStartand the others) instead of hardcodingleft,rightorrow-reverse. They return the correct value in both modes. - Use
physicalLtrfor blocks that must never mirror, such as number pickers and the Live Activity layout. - Directional icons never flip by themselves. Use
iconBack,iconForward,iconChevronBackandiconChevronForward. - On web,
applyAppDirection()setsdirandlangon the document element instead.
Native modules
Never import a native module at module scope in a file that runs at startup. Probe it first withrequireOptionalNativeModule 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.