Browse documentation

Success envelope

Non-REST endpoints (auth, storage, admin, edge) wrap their payload (apps/server/helpers/api-response.helper.ts):

{ "status": 1, "message": "OK", "data": { } }

202 uses the same shape for accepted-but-async work. status is 1 on success and 0 on failure.

Error envelope

Every error — thrown, forwarded, or a route miss — exits through createApiErrorHandler (middlewares/api-error.middleware.ts):

{ "status": 0, "code": "NOT_FOUND", "message": "Resource not found", "requestId": "…" }

status is literally 0; the HTTP status is the response's own. requestId is present whenever the correlation middleware set one, and is echoed to the caller as x-request-id. Nothing else is added: no data, no cause chain, no schema names.

Warning

That is the production shape. When NODE_ENV is not production, an unrecognised error falls back to its internal message, which is error.stack — so in development the full stack ships to the client in message. That is deliberate (it is what makes a local failure debuggable from the network tab), and it is why the server binds to 127.0.0.1 unless NODE_ENV=production: the dev envelope is unreachable from another host by construction rather than by convention. Widening it with BIND_HOST outside production logs a warning saying so.

Error classes

apps/server/exceptions/application-error.ts:

new ApplicationError(statusCode, message, { code?, expose?, cause? })
FieldDefault
codeHTTP_<status>
exposestatusCode < 500 — i.e. client errors show their message, server errors do not

One subclass, in apps/server/exceptions/http-exception.ts:

new HttpException(message, statusCode, code?, { expose? })

It reorders the arguments so the message reads first, and is what almost every throw site uses. There are no per-status subclasses — pass the status: new HttpException('Page not found', 404, 'NOT_FOUND') is the catch-all 404 in app.ts.

How a message is chosen

normalizeApiError(error, isProduction):

  1. An ApplicationError with expose: true → its own message and code.
  2. A recognised database error → the fixed sentence for its SQLSTATE (below).
  3. Otherwise: the internal message in development, a generic message in production.

Errors are logged when unknown or >= 500, with the full stack as the internal message; the log line also lands in the shared buffer with source: 'http'.

Default codes and messages

StatusCodePublic message
400BAD_REQUESTBad request
401UNAUTHORIZEDUnauthorized
403FORBIDDENForbidden
404NOT_FOUNDResource not found
409CONFLICTConflict
413PAYLOAD_TOO_LARGEPayload too large
429RATE_LIMITEDToo many requests
5xxINTERNAL_ERRORInternal server error

A 4xx status not in that table falls back to HTTP_<status> as the code and Request failed as the message; 5xx falls back to INTERNAL_ERROR and Internal server error.

An error carrying name: 'UnauthorizedError' maps to 401. A numeric statusCode/status on an unknown error is honoured when it is an integer in [400, 599]; anything else becomes 500.

Database errors

Postgres SQLSTATEs are mapped to a status and to a fixed public sentence. The driver's own message — which contains the SQL, the offending value and the schema object names — never reaches the client, so these responses are safe for anonymous REST callers and identical in development and production.

SQLSTATEStatusMessage
22001400A value is longer than its column allows.
22P02400A value has the wrong type for the column it was written to.
23502400A required field was missing.
23503409A referenced row does not exist, or is still referenced by other rows.
23505409A record with these values already exists.
23514400A value failed a check constraint on the table.
23P01409This conflicts with an existing row that overlaps it.
40001409The write conflicted with another concurrent write. Retry the request.
40P01409The database detected a deadlock and cancelled this request. Retry it.
42501403Row level security or table privileges blocked this operation.
42703400A column in the request does not exist on this table.
42P01404That table or view does not exist.
42883400That database function does not exist, or the argument types do not match.
53300503The database has no free connections. Retry in a moment.
57014504The query ran past the statement timeout and was cancelled.
Note

42501 is what an RLS denial looks like from the outside: 403 with "Row level security or table privileges blocked this operation." If you get it unexpectedly, check your policies with the RLS simulator.

SQL editor hints

The SQL editor is a different audience: the caller is an operator, so the raw Postgres ERROR / DETAIL / HINT lines and the SQLSTATE are kept. Only Prisma's own wrapper prose (Invalid prisma.$executeRawUnsafe() invocation: Raw query failed. Code: …) is stripped, because it is an implementation detail of this backend.

On top of Postgres's own hint, Motherbase adds one for the mistakes somebody arriving from Supabase makes on day one:

SQLSTATEWhenHint
42804, 42P16a uuid / text mismatchauth.users.id is text here (Better Auth issues opaque string ids, not UUIDs) and auth.uid() returns text to match. Declare owner columns as user_id text references auth.users(id) on delete cascade
22P02the value is a user idauth.uid() returns text, so a user id is not castable to uuid
42883the function is a pgcrypto oneRun CREATE EXTENSION IF NOT EXISTS pgcrypto;, or enable it under Database → Extensions
3F000the statement references an extensions schemaSupabase installs extensions into extensions; Motherbase installs them into public. Drop the qualifier
42P01the missing table is auth.usersThe auth schema comes from the platform migrations, so the project database has not been migrated yet

Validation errors

validateRequest (express-validator) returns the failure list rather than a single message:

{ "status": 0, "message": "Validation Errors:\n …", "data": [ /* express-validator errors */ ] }

Client-side shape

@motherbase/client never throws for expected API errors. Every call resolves to { data, error }, where error is { message, status?, code? } — status is the HTTP status and code the backend code from the envelope above. See the SDK reference.