AppEngine is the whole backend. One NestJS process, 123 controllers, 2,482 route handlers, serving every tenant and every client in the platform.
Production: https://appengine.appmint.io · GraphQL: playground enabled · Version: GET /version
How all three fit together: API reference and discovery.
Finding your way around
The surface is large but regular. Three questions get you to the right place:
Is it generic data access? Then it is /repository/* — one controller, 92 handlers, generic over all 261 datatypes. Create, read, update, delete, search, files, settings, history, approval, import/export. See Working with data.
Is it identity? Then it is /profile/* (aliased as /user/*) for sign-in, sign-up, invitations, OAuth, magic links, blocklists; /profile/security/* for 2FA and devices; /api-key/* for keys.
Otherwise it is a domain module. Each owns a path prefix and its own datatypes.
The APIs
Every area above also has a live specification — every endpoint with parameters, responses and examples. The API directory lists all 161 of them.
Client-facing route pairs
Several modules expose a second controller under /client/* — a narrower surface intended for end-user (customer) tokens rather than operator tokens:
/client/affiliate · /client/broadcast · /client/community · /client/content-studio · /client/events · /client/finance · /client/forms · /client/giftcards · /client/logistics · /client/stowbo · /client-data · /affiliate/public
When you are building a customer-facing app, look for the /client/* variant first.
Global behavior
Four things are applied to every route, and they explain most surprises:
1. Authentication is opt-out
JwtAuthGuard is registered as APP_GUARD — globally. Every route requires a valid token unless it carries @PublicRoute(). 145 routes do.
2. A tenant is always required
CurrentUserMiddleware runs on * and rejects any request without a resolvable orgid, apart from a short exemption list.
3. Unknown body fields are rejected
The global ValidationPipe runs with forbidNonWhitelisted: true. Sending a property the DTO does not declare is a 400, not a silent drop.
4. Read permissions are enforced on the way out
ContentPermissionInterceptor inspects the response and, if the record carries requiredRole.read, checks the caller against it — returning 403 after the handler has already run.
For a list response the interceptor reads only the first element of data and applies its requiredRole to the whole payload. Lists that mix records with different requiredRole.read values are not filtered element by element.
Where to go next
- Conventions — headers, envelopes, paging, errors, limits.
- Authentication — every way to get a token.
- Working with data — the repository surface in full.