docs
/
AppEngine API

Conventions

Headers, response envelopes, paging, error shapes, limits — the rules that hold across every endpoint.

Headers

HeaderWhenNotes
orgidAlwaysThe tenant. Also accepted as a query param, a body field, or an orgid / orgId cookie.
Authorization: Bearer <jwt>Unless the route is publicFalls back to a token or jwt cookie.
apiKey or x-api-keyServer-to-serverAlternative to a bearer token. Resolved to its owning user.
x-client-authorizationService accountsA customer token, carried alongside a System user token. Required for System repository writes.
domainAsOrg: trueMulti-domain hostingUsed with shared-org-id and x-client-host to resolve the tenant from a domain.
shared-org-idMulti-domain hostingThe shared org that owns the session.
x-client-hostMulti-domain hostingThe visitor-facing domain to resolve.
x-client-orgidMulti-domain hostingSkips domain resolution and uses this org directly.
x-client-infoOptionalJSON client telemetry. When present with an orgid, a web_visit record is written fire-and-forget.
impersonate_user, impersonate_orgReservedRead by the middleware, but the handler is currently a no-op.

Request bodies

JSON, 4 MB maximum (express.json({ limit: '4mb' })). URL-encoded bodies share the limit.

The global ValidationPipe is configured:

new ValidationPipe({
  whitelist: true,
  transform: true,
  forbidNonWhitelisted: true,
  transformOptions: { enableImplicitConversion: true },
})
Extra properties are a 400, not a warning

forbidNonWhitelisted: true means any property the DTO does not declare rejects the whole request. If you round-trip a record — GET, edit one field, POST back — strip fields the write DTO does not accept first. enableImplicitConversion does coerce "5" to 5 where the DTO says number.

Response envelopes

Single record

A BaseModel<T> — platform fields at the top level, your payload under data.

{ "pk": "…", "sk": "6512ab…", "name": "Blue Widget", "datatype": "sf_product",
  "version": 3, "state": "published", "createdate": "…", "modifydate": "…",
  "data": { "price": 19.99, "sku": "BW-1" } }

List

A BaseModelDTO<T>.

{ "datatype": "sf_product", "total": 128, "page": 0, "pageSize": 25,
  "hasNext": true, "fromCache": false,
  "data": [ { "sk": "…", "data": { … } } ] }

Paging and query options

DataOptions fields are accepted on list and search endpoints:

FieldTypeDescription
pagenumberZero-based page index.
pageSizenumberRecords per page.
lastItemnumberCursor position, for continuation-style paging.
sortanyField or sort specification.
sortTypeSortType1 ascending, -1 descending.
modelState`ModelStateModelState[]`
includeFieldsstring[]Projection allow-list.
excludeFieldsstring[]Projection deny-list.
maskFieldsstring[]Fields returned masked rather than omitted.
enrichbooleanResolve references and inline the related records.
refreshbooleanBypass cache. Check fromCache on the response to see whether it mattered.
randombooleanReturn records in random order.

Errors

A global filter (AllExceptionsFilter, registered as APP_FILTER) normalizes what handlers throw into one envelope:

{
  "statusCode": 401,
  "error": "Token has expired. Refresh the access token or request a new one.",
  "path": "/repository/get/customer/6512ab",
  "method": "GET",
  "timeStamp": "2026-08-28T10:00:00.000Z"
}

Non-HTTP exceptions are categorized first — MongoDB errors, for instance, are mapped to appropriate status codes rather than surfacing as a bare 500.

Structured auth errors

Auth failures are thrown with a richer shape, carrying a stable code and sometimes a recommended action:

codeStatusMeaningaction
missing_orgid400No tenant could be resolved—
missing_authorization_header401No bearer token and no cookie—
token_expired401JWT past its expiryrefresh_token
invalid_token401Malformed or unverifiable JWTrefresh_token
user_not_found401Token valid, but the user is not in this orgrefresh_token
auth_error401Any other authentication failurerefresh_token
Write clients that accept either shape

Depending on where in the pipeline a failure happens, a client may receive the structured object or the normalized { statusCode, error, path, method, timeStamp } envelope. Branch on the HTTP status first, and read code when it is present rather than requiring it.

Other common statuses

StatusCause
400Validation failure, unknown body property, missing orgid, bad credentials
403Blocked device; requiredRole.read denied by ContentPermissionInterceptor
404Unknown org (Organization or Site Not Found), missing record, or a scanner-probe path
429Rate limited
503/health reporting an unhealthy dependency

Rate limiting

Fixed window, applied before routing:

  • Window: 15 minutes
  • Limit: RATE_LIMIT_MAX, default 10000 per IP
  • Headers: standard RateLimit-* (legacy X-RateLimit-* off)
  • Exempt: /monitoring/health
{ "statusCode": 429, "error": "Too Many Requests",
  "message": "Too many requests from this IP, please try again later" }

The client IP is derived with trust proxy set to exactly TRUST_PROXY_HOPS hops (default 2 — Cloudflare, then Traefik). A specific count rather than true is what stops a client from spoofing X-Forwarded-For to escape the limit. Change your proxy topology and you must change this value.

The 429 handler sets CORS headers itself, because Nest applies CORS during init() — after this middleware — so short-circuited responses would otherwise reach the browser without them and surface as an opaque network error.

CORS

  • Development: any origin, credentials allowed.
  • Production: origins from ALLOWED_ORIGINS (comma-separated); methods GET, POST, PUT, DELETE, PATCH, OPTIONS; credentials allowed; preflight cached 24 h.

Route aliasing

Two patterns produce multiple URLs for one handler, and both appear in the auth module:

  • Array controller prefixes — @Controller(['profile', 'user']) mounts everything under both.
  • Array method paths — @Post(['/user/signin', '/signin']).

Combined, signin answers on four URLs. They are the same handler; pick one form per client.

Some handler paths also begin with /, which produces a doubled separator in naive path concatenation. The router normalizes it — /profile/whoami is correct, /profile//whoami is what you see if you build the path by hand.

Timeouts

Keep-alive is 65 s with a 66 s headers timeout — deliberately above a typical 60 s load-balancer idle timeout, so the balancer closes idle connections rather than letting them accumulate on the server.

Health and version

GET/healthNo auth
GET/readinessNo auth
GET/versionNo auth

All three skip the tenant requirement. /health returns { isHealthy, services, timestamp } with 200 or 503, and sends Cache-Control: no-cache, no-store, must-revalidate.