docs
/
Getting started

Quickstart

Sign in, read the token apart, and fetch records — with the exact payloads AppEngine returns.

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.

GET/healthNo auth
curl 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

POST/profile/signinNo auth

Also 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…"
}

A 200 does not always mean you are signed in

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. twoFactorMethod is email, sms or authenticator; for the first two a code has already been sent.
  • { requiresTwoFactor: true, isNewDevice: true, … } — the org has enableNewDeviceAuthentication on and this device fingerprint is unrecognized.
Wrong credentials return 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.

Roles in the token are not trusted blindly

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.

GET/profile/whoamiJWT
curl 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.

GET/repository/get/:datatype/:idJWT
curl https://appengine.appmint.io/repository/get/sf_product/6512abc… \
  -H 'Authorization: Bearer …' -H 'orgid: acme'

Searching takes a body:

POST/repository/search/:datatypeJWT
curl -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

POST/profile/user/refreshJWT

Customer 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