Everything below is a real request against a running AppEngine. Production is https://appengine.appmint.io; a local dev server listens on SERVER_PORT (the mobile clients default to 3300).
1. Check the server is up
Health and readiness are the only routes that need neither auth nor an orgid.
/healthNo authcurl https://appengine.appmint.io/health{ "isHealthy": true, "services": { }, "timestamp": "2026-08-28T10:00:00.000Z" }It returns 503 with the same envelope when a dependency is down. /readiness is the Kubernetes readiness probe; /version reports the build.
2. Sign in as a user
/profile/signinNo authAlso mounted at /profile/user/signin, /user/signin, and /user/user/signin — the controller declares @Controller(['profile', 'user']) with @Post(['/user/signin', '/signin']), so all four resolve to the same handler.
curl -X POST https://appengine.appmint.io/profile/signin \
-H 'Content-Type: application/json' \
-H 'orgid: acme' \
-d '{ "email": "[email protected]", "password": "…" }'On success:
{
"user": { "pk": "…", "sk": "6512…", "datatype": "user", "data": { "email": "[email protected]", "roles": ["Owner"], "permissions": { … } } },
"orgId": "acme",
"rootOrg": "appmint",
"sharedOrg": "…",
"token": "eyJhbGciOi…",
"refreshToken": "eyJhbGciOi…"
}userAuth has three non-token exits. Handle all of them:
{ requiresPasswordChange: true, userId, email }— the caller used a temporary password.{ requiresTwoFactor: true, challengeToken, twoFactorMethod, message }— 2FA is on for the org or the user.twoFactorMethodisemail,smsorauthenticator; for the first two a code has already been sent.{ requiresTwoFactor: true, isNewDevice: true, … }— the org hasenableNewDeviceAuthenticationon and this device fingerprint is unrecognized.
400 Invalid username or password. A blocked device fingerprint returns 403.
3. Understand what the token is
The JWT payload is the signed user record — jwtService.sign(user). That is why middleware can read tokenInfo._id, tokenInfo.datatype and tokenInfo.data.roles straight out of it without a lookup.
Two lifetimes, both from config: JWT_EXPIRES_IN for token, JWT_REFRESH_EXPIRES_IN for refreshToken.
CurrentUserMiddleware re-fetches the user from the org database on every request and then copies permissions and roles from the token onto it. A revoked user is rejected with 401 user_not_found, but a role changed after issue still rides along until the token is refreshed.
4. Make an authenticated request
Two headers, always: the bearer token and the tenant.
/profile/whoamiJWTcurl https://appengine.appmint.io/profile/whoami \
-H 'Authorization: Bearer eyJhbGciOi…' \
-H 'orgid: acme'Drop the orgid and you get a 400 before any controller runs:
{ "code": "missing_orgid", "message": "Organization ID is required. Pass orgid as a header, query param, or body field." }5. Read some data
The repository controller is generic over datatype.
/repository/get/:datatype/:idJWTcurl https://appengine.appmint.io/repository/get/sf_product/6512abc… \
-H 'Authorization: Bearer …' -H 'orgid: acme'Searching takes a body:
/repository/search/:datatypeJWTcurl -X POST https://appengine.appmint.io/repository/search/customer \
-H 'Authorization: Bearer …' -H 'orgid: acme' \
-H 'Content-Type: application/json' \
-d '{ "keyword": "smith", "query": {}, "options": { "page": 0, "pageSize": 25 } }'The body is exactly three fields — keyword, query and options. An empty keyword skips full-text search and runs query as a plain filter.
Responses come back as a BaseModelDTO<T> — the records in data, plus paging:
{ "datatype": "customer", "total": 128, "page": 0, "pageSize": 25, "hasNext": true, "data": [ { "sk": "…", "data": { … } } ] }6. Refresh before it expires
/profile/user/refreshJWTCustomer tokens refresh at /profile/customer/refresh. The 401 bodies tell you which case you are in — token_expired, invalid_token, missing_authorization_header or auth_error — and expired/invalid both carry action: "refresh_token".
Authenticating without a user
For server-to-server work, mint an API key instead and send it as apiKey or x-api-key. The middleware resolves the key to its owning user and injects a bearer token for the rest of the request. See API keys.
Next
- Conventions — headers, envelopes, errors, rate limits.
- Working with data — the full repository surface.