Backend

Symptom. pnpm typecheck in the API reports errors such as Property 'someModel' does not exist on type 'PrismaClient', a missing queue name on PerformQueue, or a missing method on the SmartSend client. The code on disk looks correct.Cause. Stale dist folders. The API resolves @perform/* imports to package source, but @perform/db re-exports from @repo/database, which is consumed through its built dist. Other packages are also resolved through dist at runtime. After a pull that changed a schema or a package, the old build output is still there.Fix.
Or rebuild everything with pnpm build. These builds do not change tracked files.
Cause. A required variable is missing or invalid. defineEnv prints one line per failing key and exits with code 78.Fix. Read the printed keys. The required ones are DATABASE_URL, REDIS_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET, SERVICE_AUTH_SECRET and SMARTSEND_BASE_URL. The two secrets need at least 16 characters, and the two URLs must parse as URLs.Also check where the .env file is. dotenv loads it from the current working directory. pnpm dev:api runs in apps/core-api, so the file must be there, not only in the repository root. See Local setup.
Cause. PORT is not set. The schema default is 8000. Every script in the workspace expects 3031.Fix. Set PORT=3031 in the backend env.
Cause. An earlier API process is still running. tools/ensure-core-api.sh starts the backend in the background when you run pnpm dev in the web repository, and that process outlives the terminal.Fix.
Its output is in /tmp/perform-core-api-dev.log.
Cause. The packages were never built. A fresh clone has no dist folders.Fix. pnpm db:generate && pnpm build.
Cause. The schema files and the client were updated, but the migration was not applied to the database you are connected to.Fix. pnpm db:migrate:status, then pnpm db:migrate. Check which database DATABASE_URL points to before you run it. See Database migrations.
Cause. prisma migrate takes a session-level advisory lock. If an earlier run went through a connection pooler, the pooler can keep the backend connection, and the lock, after the process has exited.Fix. Run migrations over a direct connection. packages/db/prisma.config.ts uses DIRECT_URL for this. Set it explicitly if your pooler host does not follow the -pooler. naming the config strips. To clear a stuck lock, end the database session that holds it. Get agreement before terminating sessions on a shared database.
Cause. Redis is not reachable at REDIS_URL. Readiness treats Redis as optional, so the API still answers.Effect. Queues and schedulers do not run, and signed requests cannot be checked for replay.Fix. Start Redis, or correct the URL.
Cause. app.ts sets trust proxy to 1, which assumes exactly one reverse proxy in front of the API. With a different number of hops, req.ip is the proxy’s address or a spoofable value, and the /v1 limiter puts many clients in one bucket.Fix. Match the number of proxy hops to the setting. Locally there is no proxy and this does not occur.
Cause. lint-staged lints each staged file directly. prisma.config.ts sits at a package root and reads process.env on purpose, because the Prisma CLI has no access to @perform/config. pnpm lint only covers src, so it passes.Fix. There is no clean one in the repository yet. Avoid staging that file with unrelated work, and raise it with the team instead of weakening the rule ad hoc.
Cause. The backend bans the em dash character in code, JSON, YAML and Markdown.Fix. Replace it with a comma, a period, a colon or a hyphen. pnpm lint:em-dash lists the files.
Cause. The no-loose-version-pins rule. Backend manifests use exact versions.Fix. pnpm add --save-exact NAME@VERSION.
Cause. The login code is sent through SmartSend with the global key or the studio’s own key. Locally neither is set.Fix. Set TRAINEE_OTP_DEV_MODE=true in the backend env. The code is then logged and returned as devCode. Never do this in production.

Web app

Cause. The core API is not running or CORE_API_URL is wrong. The /api proxy forwards auth to the backend.Fix. Check curl http://127.0.0.1:3031/healthz. Start the API with pnpm dev at the workspace root, or pnpm dev:api in backend.
Cause. The script tried to start the backend in the background and it did not answer on /healthz within 60 seconds. The script prints the last 40 lines of the backend log.Fix. Read those lines. It is usually a config validation failure, a missing build, or an unreachable database. Start the backend in its own terminal to see the full output.
Cause. SERVICE_AUTH_SECRET is different in the two repositories, CORE_API_SERVICE_ID is not web, or the machine clock is far off. Signed requests carry a timestamp and the API rejects stale ones with stale or invalid timestamp.Fix. Copy the secret from the backend env. Check the system clock.
Cause. For a visitor with no session, proxy.ts rewrites / to the marketing dev server on port 3001. pnpm dev starts it together with the app. If you started only the saas app, nothing is listening there.Fix. Run pnpm dev from the repository root, or open /login directly.
Fix.
Stop the old process. The dev script pins the ports, so Next.js will not pick another one.
Cause. A stale development cache.Fix. Stop the dev server, delete apps/saas/.next, and start again. pnpm clean runs turbo clean for a full reset.
Cause. Next.js blocks dev requests from unknown origins, and auth rejects untrusted origins.Fix. Add the origin, for example http://192.168.1.20:3000, to AUTH_TRUSTED_ORIGINS. next.config.ts turns that list into allowedDevOrigins. The backend reads the same variable for auth and CORS.
Cause. A themed file uses a literal near-white, near-black or pale colour.Fix. Use the design tokens (bg-background, bg-card, text-foreground, text-muted-foreground, border-border). Run pnpm check:theme to list every hit. A file that paints the mobile app’s own palette belongs on the allow list at the top of tools/check-theme-tokens.mjs, with a comment saying why.
Cause. Body size limits on both sides. next.config.ts sets proxyClientMaxBodySize and the Server Action bodySizeLimit to 24mb. The API’s JSON parser accepts 24mb in general, 48mb on the two AI analyze routes, 4mb on the public signing route, and 12mb for raw application/octet-stream parts.Fix. Large media goes to object storage with a presigned URL or the chunked upload path, not through a JSON body.
Cause. A DndContext without a stable id generates different ids on the server and the client.Fix. Pass id={useId()} to the DndContext.

Mobile app

Cause. The device cannot resolve localhost. In development the app derives the host from Metro, which normally gives your machine’s LAN address.Fix. Look for the [perform] API ... line in the Metro output to see which URL is in use. Run pnpm use-lan-ip to write the LAN address into .env, then restart with pnpm start -- --clear. The phone and the computer must be on the same network, and the API must be listening on port 3031. A VPN on either device often breaks this.
Cause. Expo inlines these values at bundle time and Metro caches the result.Fix. pnpm start -- --clear.
Cause. Expo Go only contains the modules Expo ships with it. HealthKit, Health Connect, widgets and the local cardio notification module are not among them. Native RTL is also not applied in Expo Go.Fix. Use a development build: pnpm ios or pnpm android, then pnpm dev:device.
Cause. Typed routes are generated into .expo/types. The file is stale after a route was added or renamed.Fix. Start Metro once so the types regenerate. Do not cast the route string.
Cause. The wrong Node or pnpm version, or a dependency was upgraded without updating its patch in patches/.Fix. nvm use to get Node 24.14.0, and use the pnpm version from packageManager. If a patched dependency changed version, recreate the patch for the new version and update patchedDependencies in pnpm-workspace.yaml.
Cause. Often a VPN or proxy blocking the Maven repositories.Fix. Disconnect the VPN and retry. If native folders are out of date, run pnpm prebuild:clean and build again.
Cause. One of: the update is applied on the second launch after download, the update was published for a different runtime than the device’s binary, or the bundle fails to launch on that binary and the app fell back to its embedded bundle.Fix. Relaunch twice. Then follow the diagnosis steps in OTA updates.
Cause. The native direction flag is only read when a new surface starts. applyAppDirection() reloads the app once after a language change to pick it up.Fix. Relaunch the app. If the direction is wrong in Expo Go only, that is expected. Test on a development build.

All repositories

Symptom. Install warnings about engines, syntax errors from tools, or native module build failures.Fix. Use Node 24.14.0 for the whole workspace. It satisfies the backend (>=22), the web app (>=20) and the mobile app’s .nvmrc. Run nvm use inside backend or mobile to follow their .nvmrc.
Symptom. Lockfile changes you did not make, or --frozen-lockfile failing.Fix. Run corepack enable. Corepack reads packageManager in each repository and uses pnpm 11.5.0 in the backend and 10.28.2 in the web and mobile repositories.
Cause. Hooks are installed by the prepare script during install. A clone that was never installed, or an install with scripts disabled, has none.Fix. Run pnpm install in that repository.
Fix. Use type(scope): subject with one of the allowed types, a header of at most 100 characters, and a lower case or sentence case subject. See Branches and commits.
Cause. Several type-checks, test runs or builds at once. Each one uses many cores.Fix. Run one heavy task at a time. Cap test workers with --maxWorkers=2 for Vitest and --workers=2 for Playwright.