These two packages define the vocabulary every module uses. @perform/types has no dependencies. @perform/errors depends on it and on Express and Zod types.

@perform/types

packages/types/src/index.ts. Constants are as const objects with a union type of the same name, not TypeScript enums.

ErrorCode

Response types

These describe the /v1 envelope. Client code can import them to type responses.

StudioRole

The role on req.auth. See Roles and permissions.

Staff permission groups

organizationRoleForStaff returns admin when the permission group is super_admin or the coach role is HEAD_COACH, otherwise member. @repo/auth uses it when it creates the better-auth membership for a staff member. It lives here because both the auth package and core-api need the same answer.

STUDIO_ONBOARDING_WINDOW_MS

72 hours in milliseconds. How long after its creation a studio that the /start wizard bootstrapped still counts as in the wizard when its owner never finished it. Plan limits exempt the wizard’s own invites and import during that window. Read by billing.limits.ts in core-api and by studio-lifecycle.ts in @repo/auth.

@perform/errors

packages/errors/src/index.ts.

AppError

Default status per code: Examples from the codebase:
AppError extends Error, so instanceof AppError works across the workspace as long as there is one copy of the package, which the workspace guarantees.

toApiError(err, requestId?)

Turns anything thrown into an ApiError body. It is what the middleware sends, and it is exported so other code can build the same body. The order of checks:
  1. An AppError, or an error that mapPrismaError or mapBodyParserError can turn into one. details is copied when present.
  2. A ZodError: code VALIDATION, message validation failed, details as { path, message } per issue.
  3. Anything else: code INTERNAL, message internal server error.
The internal mappers are not exported:
  • mapPrismaError matches on err.name and err.code, so it does not need to import Prisma.
  • mapBodyParserError matches errors that have a string type, expose === true and a numeric 4xx status.
  • isConnectionError treats messages containing connection terminated, connection closed, econnrefused, connection refused or connection reset as a database outage.
The full mapping table is on Responses and errors.

errorMiddleware

Computes the status (AppError.status, 422 for a ZodError, else 500), logs one line with console.error for 5xx or console.warn for 4xx, and sends toApiError(err, requestId). Register it last. It must have the four argument signature Express uses to recognize an error handler, which the exported constant already has.

notFoundMiddleware

Answers 404 with route not found: METHOD /original/url. Register it after every router and before errorMiddleware.

Using them outside core-api

A new Express service in the workspace gets the same envelope by registering the two middlewares and throwing AppError:
Lanes that need a different wire format register their own error handler on the router, as the automation API and the signing lane do. Their handlers still receive AppError and read code, status and details from it.

Adding an error code

  1. Add the key to ErrorCode in @perform/types.
  2. Add its status to STATUS_BY_CODE in @perform/errors. The record type is keyed by ErrorCode, so the compiler reports a missing entry.
  3. Check the automation lane’s STATUS_BY_ERROR_CODE map and add the code there if it should not fall through to 400.
  4. Rebuild both packages.
  5. Tell the web and mobile teams. Clients switch on code, and an unknown code usually falls into a generic error path.
Prefer a details.reason on an existing code over a new code. A new sub reason is backward compatible for clients. A new top level code is not.